dorfl 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advance-ci-template.d.ts +73 -0
- package/dist/advance-ci-template.d.ts.map +1 -0
- package/dist/advance-ci-template.js +104 -0
- package/dist/advance-ci-template.js.map +1 -0
- package/dist/advance-classify.d.ts +132 -0
- package/dist/advance-classify.d.ts.map +1 -0
- package/dist/advance-classify.js +120 -0
- package/dist/advance-classify.js.map +1 -0
- package/dist/advance-drivers.d.ts +182 -0
- package/dist/advance-drivers.d.ts.map +1 -0
- package/dist/advance-drivers.js +231 -0
- package/dist/advance-drivers.js.map +1 -0
- package/dist/advance-isolated.d.ts +156 -0
- package/dist/advance-isolated.d.ts.map +1 -0
- package/dist/advance-isolated.js +256 -0
- package/dist/advance-isolated.js.map +1 -0
- package/dist/advance-lifecycle-template.d.ts +107 -0
- package/dist/advance-lifecycle-template.d.ts.map +1 -0
- package/dist/advance-lifecycle-template.js +668 -0
- package/dist/advance-lifecycle-template.js.map +1 -0
- package/dist/advance-loop-driver.d.ts +325 -0
- package/dist/advance-loop-driver.d.ts.map +1 -0
- package/dist/advance-loop-driver.js +437 -0
- package/dist/advance-loop-driver.js.map +1 -0
- package/dist/advance-treeless-publish.d.ts +108 -0
- package/dist/advance-treeless-publish.d.ts.map +1 -0
- package/dist/advance-treeless-publish.js +71 -0
- package/dist/advance-treeless-publish.js.map +1 -0
- package/dist/advance.d.ts +340 -0
- package/dist/advance.d.ts.map +1 -0
- package/dist/advance.js +1122 -0
- package/dist/advance.js.map +1 -0
- package/dist/advancing-lock.d.ts +294 -0
- package/dist/advancing-lock.d.ts.map +1 -0
- package/dist/advancing-lock.js +594 -0
- package/dist/advancing-lock.js.map +1 -0
- package/dist/agent-launch.d.ts +79 -0
- package/dist/agent-launch.d.ts.map +1 -0
- package/dist/agent-launch.js +61 -0
- package/dist/agent-launch.js.map +1 -0
- package/dist/agent-stop.d.ts +149 -0
- package/dist/agent-stop.d.ts.map +1 -0
- package/dist/agent-stop.js +307 -0
- package/dist/agent-stop.js.map +1 -0
- package/dist/apply-decide.d.ts +127 -0
- package/dist/apply-decide.d.ts.map +1 -0
- package/dist/apply-decide.js +176 -0
- package/dist/apply-decide.js.map +1 -0
- package/dist/apply-merge-action.d.ts +206 -0
- package/dist/apply-merge-action.d.ts.map +1 -0
- package/dist/apply-merge-action.js +307 -0
- package/dist/apply-merge-action.js.map +1 -0
- package/dist/apply-persist.d.ts +174 -0
- package/dist/apply-persist.d.ts.map +1 -0
- package/dist/apply-persist.js +359 -0
- package/dist/apply-persist.js.map +1 -0
- package/dist/arbiter.d.ts +120 -0
- package/dist/arbiter.d.ts.map +1 -0
- package/dist/arbiter.js +255 -0
- package/dist/arbiter.js.map +1 -0
- package/dist/brand.d.ts +70 -0
- package/dist/brand.d.ts.map +1 -0
- package/dist/brand.js +84 -0
- package/dist/brand.js.map +1 -0
- package/dist/buildable-body.d.ts +132 -0
- package/dist/buildable-body.d.ts.map +1 -0
- package/dist/buildable-body.js +131 -0
- package/dist/buildable-body.js.map +1 -0
- package/dist/categorise.d.ts +66 -0
- package/dist/categorise.d.ts.map +1 -0
- package/dist/categorise.js +106 -0
- package/dist/categorise.js.map +1 -0
- package/dist/claim-cas.d.ts +117 -0
- package/dist/claim-cas.d.ts.map +1 -0
- package/dist/claim-cas.js +312 -0
- package/dist/claim-cas.js.map +1 -0
- package/dist/cli-spinner.d.ts +112 -0
- package/dist/cli-spinner.d.ts.map +1 -0
- package/dist/cli-spinner.js +157 -0
- package/dist/cli-spinner.js.map +1 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +3094 -0
- package/dist/cli.js.map +1 -0
- package/dist/close-job-template.d.ts +70 -0
- package/dist/close-job-template.d.ts.map +1 -0
- package/dist/close-job-template.js +180 -0
- package/dist/close-job-template.js.map +1 -0
- package/dist/close-job.d.ts +95 -0
- package/dist/close-job.d.ts.map +1 -0
- package/dist/close-job.js +226 -0
- package/dist/close-job.js.map +1 -0
- package/dist/complete.d.ts +361 -0
- package/dist/complete.d.ts.map +1 -0
- package/dist/complete.js +885 -0
- package/dist/complete.js.map +1 -0
- package/dist/concurrency.d.ts +68 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +112 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/config-override.d.ts +76 -0
- package/dist/config-override.d.ts.map +1 -0
- package/dist/config-override.js +50 -0
- package/dist/config-override.js.map +1 -0
- package/dist/config.d.ts +668 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +241 -0
- package/dist/config.js.map +1 -0
- package/dist/continue-branch.d.ts +249 -0
- package/dist/continue-branch.d.ts.map +1 -0
- package/dist/continue-branch.js +389 -0
- package/dist/continue-branch.js.map +1 -0
- package/dist/cwd-section.d.ts +186 -0
- package/dist/cwd-section.d.ts.map +1 -0
- package/dist/cwd-section.js +209 -0
- package/dist/cwd-section.js.map +1 -0
- package/dist/decision-engine.d.ts +170 -0
- package/dist/decision-engine.d.ts.map +1 -0
- package/dist/decision-engine.js +136 -0
- package/dist/decision-engine.js.map +1 -0
- package/dist/detect.d.ts +17 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +118 -0
- package/dist/detect.js.map +1 -0
- package/dist/do-autopick.d.ts +85 -0
- package/dist/do-autopick.d.ts.map +1 -0
- package/dist/do-autopick.js +112 -0
- package/dist/do-autopick.js.map +1 -0
- package/dist/do-config.d.ts +312 -0
- package/dist/do-config.d.ts.map +1 -0
- package/dist/do-config.js +358 -0
- package/dist/do-config.js.map +1 -0
- package/dist/do-remote-auto.d.ts +75 -0
- package/dist/do-remote-auto.d.ts.map +1 -0
- package/dist/do-remote-auto.js +111 -0
- package/dist/do-remote-auto.js.map +1 -0
- package/dist/do.d.ts +621 -0
- package/dist/do.d.ts.map +1 -0
- package/dist/do.js +1882 -0
- package/dist/do.js.map +1 -0
- package/dist/drop-source.d.ts +96 -0
- package/dist/drop-source.d.ts.map +1 -0
- package/dist/drop-source.js +91 -0
- package/dist/drop-source.js.map +1 -0
- package/dist/eligibility.d.ts +46 -0
- package/dist/eligibility.d.ts.map +1 -0
- package/dist/eligibility.js +34 -0
- package/dist/eligibility.js.map +1 -0
- package/dist/env-config.d.ts +51 -0
- package/dist/env-config.d.ts.map +1 -0
- package/dist/env-config.js +272 -0
- package/dist/env-config.js.map +1 -0
- package/dist/failure-cause.d.ts +70 -0
- package/dist/failure-cause.d.ts.map +1 -0
- package/dist/failure-cause.js +126 -0
- package/dist/failure-cause.js.map +1 -0
- package/dist/format.d.ts +43 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +256 -0
- package/dist/format.js.map +1 -0
- package/dist/frontmatter.d.ts +215 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +345 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/gate-readiness.d.ts +84 -0
- package/dist/gate-readiness.d.ts.map +1 -0
- package/dist/gate-readiness.js +103 -0
- package/dist/gate-readiness.js.map +1 -0
- package/dist/gc.d.ts +165 -0
- package/dist/gc.d.ts.map +1 -0
- package/dist/gc.js +313 -0
- package/dist/gc.js.map +1 -0
- package/dist/gh-failure.d.ts +42 -0
- package/dist/gh-failure.d.ts.map +1 -0
- package/dist/gh-failure.js +49 -0
- package/dist/gh-failure.js.map +1 -0
- package/dist/git.d.ts +75 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +130 -0
- package/dist/git.js.map +1 -0
- package/dist/github.d.ts +187 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js +343 -0
- package/dist/github.js.map +1 -0
- package/dist/harness.d.ts +242 -0
- package/dist/harness.d.ts.map +1 -0
- package/dist/harness.js +157 -0
- package/dist/harness.js.map +1 -0
- package/dist/identity.d.ts +167 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +231 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +147 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/install-ci-branch-protection.d.ts +147 -0
- package/dist/install-ci-branch-protection.d.ts.map +1 -0
- package/dist/install-ci-branch-protection.js +166 -0
- package/dist/install-ci-branch-protection.js.map +1 -0
- package/dist/install-ci-capabilities/advance-lifecycle.d.ts +15 -0
- package/dist/install-ci-capabilities/advance-lifecycle.d.ts.map +1 -0
- package/dist/install-ci-capabilities/advance-lifecycle.js +28 -0
- package/dist/install-ci-capabilities/advance-lifecycle.js.map +1 -0
- package/dist/install-ci-capabilities/close-job.d.ts +13 -0
- package/dist/install-ci-capabilities/close-job.d.ts.map +1 -0
- package/dist/install-ci-capabilities/close-job.js +26 -0
- package/dist/install-ci-capabilities/close-job.js.map +1 -0
- package/dist/install-ci-capabilities/example-noop.d.ts +16 -0
- package/dist/install-ci-capabilities/example-noop.d.ts.map +1 -0
- package/dist/install-ci-capabilities/example-noop.js +23 -0
- package/dist/install-ci-capabilities/example-noop.js.map +1 -0
- package/dist/install-ci-capabilities/intake.d.ts +15 -0
- package/dist/install-ci-capabilities/intake.d.ts.map +1 -0
- package/dist/install-ci-capabilities/intake.js +28 -0
- package/dist/install-ci-capabilities/intake.js.map +1 -0
- package/dist/install-ci-capabilities/verify.d.ts +14 -0
- package/dist/install-ci-capabilities/verify.d.ts.map +1 -0
- package/dist/install-ci-capabilities/verify.js +27 -0
- package/dist/install-ci-capabilities/verify.js.map +1 -0
- package/dist/install-ci-core.d.ts +446 -0
- package/dist/install-ci-core.d.ts.map +1 -0
- package/dist/install-ci-core.js +760 -0
- package/dist/install-ci-core.js.map +1 -0
- package/dist/install-ci-github.d.ts +167 -0
- package/dist/install-ci-github.d.ts.map +1 -0
- package/dist/install-ci-github.js +315 -0
- package/dist/install-ci-github.js.map +1 -0
- package/dist/install-ci.d.ts +105 -0
- package/dist/install-ci.d.ts.map +1 -0
- package/dist/install-ci.js +363 -0
- package/dist/install-ci.js.map +1 -0
- package/dist/intake-event.d.ts +88 -0
- package/dist/intake-event.d.ts.map +1 -0
- package/dist/intake-event.js +66 -0
- package/dist/intake-event.js.map +1 -0
- package/dist/intake-marker.d.ts +95 -0
- package/dist/intake-marker.d.ts.map +1 -0
- package/dist/intake-marker.js +127 -0
- package/dist/intake-marker.js.map +1 -0
- package/dist/intake-triage.d.ts +48 -0
- package/dist/intake-triage.d.ts.map +1 -0
- package/dist/intake-triage.js +95 -0
- package/dist/intake-triage.js.map +1 -0
- package/dist/intake-trigger-template.d.ts +185 -0
- package/dist/intake-trigger-template.d.ts.map +1 -0
- package/dist/intake-trigger-template.js +449 -0
- package/dist/intake-trigger-template.js.map +1 -0
- package/dist/intake.d.ts +569 -0
- package/dist/intake.d.ts.map +1 -0
- package/dist/intake.js +1628 -0
- package/dist/intake.js.map +1 -0
- package/dist/integration-core.d.ts +539 -0
- package/dist/integration-core.d.ts.map +1 -0
- package/dist/integration-core.js +2195 -0
- package/dist/integration-core.js.map +1 -0
- package/dist/integrator.d.ts +343 -0
- package/dist/integrator.d.ts.map +1 -0
- package/dist/integrator.js +400 -0
- package/dist/integrator.js.map +1 -0
- package/dist/isolation.d.ts +219 -0
- package/dist/isolation.d.ts.map +1 -0
- package/dist/isolation.js +261 -0
- package/dist/isolation.js.map +1 -0
- package/dist/issue-provider.d.ts +349 -0
- package/dist/issue-provider.d.ts.map +1 -0
- package/dist/issue-provider.js +360 -0
- package/dist/issue-provider.js.map +1 -0
- package/dist/item-lock.d.ts +626 -0
- package/dist/item-lock.d.ts.map +1 -0
- package/dist/item-lock.js +1381 -0
- package/dist/item-lock.js.map +1 -0
- package/dist/item-path.d.ts +49 -0
- package/dist/item-path.d.ts.map +1 -0
- package/dist/item-path.js +66 -0
- package/dist/item-path.js.map +1 -0
- package/dist/ledger-lint.d.ts +129 -0
- package/dist/ledger-lint.d.ts.map +1 -0
- package/dist/ledger-lint.js +249 -0
- package/dist/ledger-lint.js.map +1 -0
- package/dist/ledger-read.d.ts +357 -0
- package/dist/ledger-read.d.ts.map +1 -0
- package/dist/ledger-read.js +442 -0
- package/dist/ledger-read.js.map +1 -0
- package/dist/ledger-write.d.ts +330 -0
- package/dist/ledger-write.d.ts.map +1 -0
- package/dist/ledger-write.js +411 -0
- package/dist/ledger-write.js.map +1 -0
- package/dist/lifecycle-gather.d.ts +30 -0
- package/dist/lifecycle-gather.d.ts.map +1 -0
- package/dist/lifecycle-gather.js +205 -0
- package/dist/lifecycle-gather.js.map +1 -0
- package/dist/lifecycle-pools.d.ts +180 -0
- package/dist/lifecycle-pools.d.ts.map +1 -0
- package/dist/lifecycle-pools.js +78 -0
- package/dist/lifecycle-pools.js.map +1 -0
- package/dist/merge-question-surfacer.d.ts +166 -0
- package/dist/merge-question-surfacer.d.ts.map +1 -0
- package/dist/merge-question-surfacer.js +297 -0
- package/dist/merge-question-surfacer.js.map +1 -0
- package/dist/mint-adr.d.ts +126 -0
- package/dist/mint-adr.d.ts.map +1 -0
- package/dist/mint-adr.js +257 -0
- package/dist/mint-adr.js.map +1 -0
- package/dist/mirror-pool-scan.d.ts +125 -0
- package/dist/mirror-pool-scan.d.ts.map +1 -0
- package/dist/mirror-pool-scan.js +104 -0
- package/dist/mirror-pool-scan.js.map +1 -0
- package/dist/needs-attention.d.ts +341 -0
- package/dist/needs-attention.d.ts.map +1 -0
- package/dist/needs-attention.js +900 -0
- package/dist/needs-attention.js.map +1 -0
- package/dist/orphan-sidecar.d.ts +79 -0
- package/dist/orphan-sidecar.d.ts.map +1 -0
- package/dist/orphan-sidecar.js +71 -0
- package/dist/orphan-sidecar.js.map +1 -0
- package/dist/output.d.ts +48 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +66 -0
- package/dist/output.js.map +1 -0
- package/dist/pi-harness.d.ts +179 -0
- package/dist/pi-harness.d.ts.map +1 -0
- package/dist/pi-harness.js +342 -0
- package/dist/pi-harness.js.map +1 -0
- package/dist/placement.d.ts +99 -0
- package/dist/placement.d.ts.map +1 -0
- package/dist/placement.js +67 -0
- package/dist/placement.js.map +1 -0
- package/dist/prd-to-spec.d.ts +315 -0
- package/dist/prd-to-spec.d.ts.map +1 -0
- package/dist/prd-to-spec.js +684 -0
- package/dist/prd-to-spec.js.map +1 -0
- package/dist/prepare.d.ts +121 -0
- package/dist/prepare.d.ts.map +1 -0
- package/dist/prepare.js +140 -0
- package/dist/prepare.js.map +1 -0
- package/dist/prompt.d.ts +360 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +499 -0
- package/dist/prompt.js.map +1 -0
- package/dist/protocol/ADR-FORMAT.md +47 -0
- package/dist/protocol/CLAIM-PROTOCOL.md +217 -0
- package/dist/protocol/REVIEW-PROTOCOL.md +119 -0
- package/dist/protocol/SURFACE-PROTOCOL.md +121 -0
- package/dist/protocol/TASKING-PROTOCOL.md +122 -0
- package/dist/protocol/WORK-CONTRACT.md +276 -0
- package/dist/protocol/spec-template.md +71 -0
- package/dist/protocol/task-template.md +65 -0
- package/dist/readiness.d.ts +66 -0
- package/dist/readiness.d.ts.map +1 -0
- package/dist/readiness.js +36 -0
- package/dist/readiness.js.map +1 -0
- package/dist/reap-branches.d.ts +102 -0
- package/dist/reap-branches.d.ts.map +1 -0
- package/dist/reap-branches.js +149 -0
- package/dist/reap-branches.js.map +1 -0
- package/dist/recover-isolated.d.ts +72 -0
- package/dist/recover-isolated.d.ts.map +1 -0
- package/dist/recover-isolated.js +188 -0
- package/dist/recover-isolated.js.map +1 -0
- package/dist/registry.d.ts +172 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +296 -0
- package/dist/registry.js.map +1 -0
- package/dist/repo-config.d.ts +201 -0
- package/dist/repo-config.d.ts.map +1 -0
- package/dist/repo-config.js +414 -0
- package/dist/repo-config.js.map +1 -0
- package/dist/repo-key.d.ts +20 -0
- package/dist/repo-key.d.ts.map +1 -0
- package/dist/repo-key.js +68 -0
- package/dist/repo-key.js.map +1 -0
- package/dist/repo-mirror.d.ts +177 -0
- package/dist/repo-mirror.d.ts.map +1 -0
- package/dist/repo-mirror.js +271 -0
- package/dist/repo-mirror.js.map +1 -0
- package/dist/retry-backoff.d.ts +90 -0
- package/dist/retry-backoff.d.ts.map +1 -0
- package/dist/retry-backoff.js +98 -0
- package/dist/retry-backoff.js.map +1 -0
- package/dist/review-gate.d.ts +173 -0
- package/dist/review-gate.d.ts.map +1 -0
- package/dist/review-gate.js +261 -0
- package/dist/review-gate.js.map +1 -0
- package/dist/review-verdict.d.ts +149 -0
- package/dist/review-verdict.d.ts.map +1 -0
- package/dist/review-verdict.js +332 -0
- package/dist/review-verdict.js.map +1 -0
- package/dist/run.d.ts +221 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +963 -0
- package/dist/run.js.map +1 -0
- package/dist/scan.d.ts +308 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +374 -0
- package/dist/scan.js.map +1 -0
- package/dist/select-order.d.ts +75 -0
- package/dist/select-order.d.ts.map +1 -0
- package/dist/select-order.js +108 -0
- package/dist/select-order.js.map +1 -0
- package/dist/select-priority.d.ts +188 -0
- package/dist/select-priority.d.ts.map +1 -0
- package/dist/select-priority.js +80 -0
- package/dist/select-priority.js.map +1 -0
- package/dist/select.d.ts +25 -0
- package/dist/select.d.ts.map +1 -0
- package/dist/select.js +43 -0
- package/dist/select.js.map +1 -0
- package/dist/session-path.d.ts +36 -0
- package/dist/session-path.d.ts.map +1 -0
- package/dist/session-path.js +129 -0
- package/dist/session-path.js.map +1 -0
- package/dist/sidecar-apply.d.ts +83 -0
- package/dist/sidecar-apply.d.ts.map +1 -0
- package/dist/sidecar-apply.js +111 -0
- package/dist/sidecar-apply.js.map +1 -0
- package/dist/sidecar.d.ts +245 -0
- package/dist/sidecar.d.ts.map +1 -0
- package/dist/sidecar.js +481 -0
- package/dist/sidecar.js.map +1 -0
- package/dist/slug-namespace.d.ts +204 -0
- package/dist/slug-namespace.d.ts.map +1 -0
- package/dist/slug-namespace.js +229 -0
- package/dist/slug-namespace.js.map +1 -0
- package/dist/spec-complete.d.ts +44 -0
- package/dist/spec-complete.d.ts.map +1 -0
- package/dist/spec-complete.js +69 -0
- package/dist/spec-complete.js.map +1 -0
- package/dist/start.d.ts +97 -0
- package/dist/start.d.ts.map +1 -0
- package/dist/start.js +633 -0
- package/dist/start.js.map +1 -0
- package/dist/status.d.ts +199 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +228 -0
- package/dist/status.js.map +1 -0
- package/dist/surface-gate.d.ts +162 -0
- package/dist/surface-gate.d.ts.map +1 -0
- package/dist/surface-gate.js +206 -0
- package/dist/surface-gate.js.map +1 -0
- package/dist/surface-persist.d.ts +86 -0
- package/dist/surface-persist.d.ts.map +1 -0
- package/dist/surface-persist.js +129 -0
- package/dist/surface-persist.js.map +1 -0
- package/dist/tasker-review-loop.d.ts +249 -0
- package/dist/tasker-review-loop.d.ts.map +1 -0
- package/dist/tasker-review-loop.js +369 -0
- package/dist/tasker-review-loop.js.map +1 -0
- package/dist/tasking-eligibility.d.ts +74 -0
- package/dist/tasking-eligibility.d.ts.map +1 -0
- package/dist/tasking-eligibility.js +52 -0
- package/dist/tasking-eligibility.js.map +1 -0
- package/dist/tasking-lock.d.ts +111 -0
- package/dist/tasking-lock.d.ts.map +1 -0
- package/dist/tasking-lock.js +256 -0
- package/dist/tasking-lock.js.map +1 -0
- package/dist/tasking.d.ts +275 -0
- package/dist/tasking.d.ts.map +1 -0
- package/dist/tasking.js +952 -0
- package/dist/tasking.js.map +1 -0
- package/dist/triage-gate.d.ts +127 -0
- package/dist/triage-gate.d.ts.map +1 -0
- package/dist/triage-gate.js +139 -0
- package/dist/triage-gate.js.map +1 -0
- package/dist/triage-persist.d.ts +163 -0
- package/dist/triage-persist.d.ts.map +1 -0
- package/dist/triage-persist.js +387 -0
- package/dist/triage-persist.js.map +1 -0
- package/dist/verdict-json.d.ts +32 -0
- package/dist/verdict-json.d.ts.map +1 -0
- package/dist/verdict-json.js +74 -0
- package/dist/verdict-json.js.map +1 -0
- package/dist/verify-workflow-template.d.ts +60 -0
- package/dist/verify-workflow-template.d.ts.map +1 -0
- package/dist/verify-workflow-template.js +126 -0
- package/dist/verify-workflow-template.js.map +1 -0
- package/dist/verify.d.ts +60 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +62 -0
- package/dist/verify.js.map +1 -0
- package/dist/watch-session.d.ts +112 -0
- package/dist/watch-session.d.ts.map +1 -0
- package/dist/watch-session.js +347 -0
- package/dist/watch-session.js.map +1 -0
- package/dist/work-layout.d.ts +198 -0
- package/dist/work-layout.d.ts.map +1 -0
- package/dist/work-layout.js +217 -0
- package/dist/work-layout.js.map +1 -0
- package/dist/work-on.d.ts +154 -0
- package/dist/work-on.d.ts.map +1 -0
- package/dist/work-on.js +387 -0
- package/dist/work-on.js.map +1 -0
- package/dist/workspace.d.ts +224 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +325 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +46 -2
- package/src/advance-ci-template.ts +203 -0
- package/src/advance-classify.ts +197 -0
- package/src/advance-drivers.ts +414 -0
- package/src/advance-isolated.ts +432 -0
- package/src/advance-lifecycle-template.ts +791 -0
- package/src/advance-loop-driver.ts +745 -0
- package/src/advance-treeless-publish.ts +177 -0
- package/src/advance.ts +1564 -0
- package/src/advancing-lock.ts +988 -0
- package/src/agent-launch.ts +137 -0
- package/src/agent-stop.ts +361 -0
- package/src/apply-decide.ts +242 -0
- package/src/apply-merge-action.ts +502 -0
- package/src/apply-persist.ts +518 -0
- package/src/arbiter.ts +372 -0
- package/src/brand.ts +111 -0
- package/src/buildable-body.ts +196 -0
- package/src/categorise.ts +158 -0
- package/src/claim-cas.ts +513 -0
- package/src/cli-spinner.ts +225 -0
- package/src/cli.ts +4369 -0
- package/src/close-job-template.ts +236 -0
- package/src/close-job.ts +319 -0
- package/src/complete.ts +1379 -0
- package/src/concurrency.ts +151 -0
- package/src/config-override.ts +116 -0
- package/src/config.ts +883 -0
- package/src/continue-branch.ts +542 -0
- package/src/cwd-section.ts +392 -0
- package/src/decision-engine.ts +272 -0
- package/src/detect.ts +124 -0
- package/src/do-autopick.ts +223 -0
- package/src/do-config.ts +589 -0
- package/src/do-remote-auto.ts +197 -0
- package/src/do.ts +2623 -0
- package/src/drop-source.ts +194 -0
- package/src/eligibility.ts +79 -0
- package/src/env-config.ts +305 -0
- package/src/failure-cause.ts +142 -0
- package/src/format.ts +313 -0
- package/src/frontmatter.ts +485 -0
- package/src/gate-readiness.ts +147 -0
- package/src/gc.ts +510 -0
- package/src/gh-failure.ts +53 -0
- package/src/git.ts +186 -0
- package/src/github.ts +468 -0
- package/src/harness.ts +355 -0
- package/src/identity.ts +322 -0
- package/src/index.ts +785 -0
- package/src/install-ci-branch-protection.ts +255 -0
- package/src/install-ci-capabilities/advance-lifecycle.ts +34 -0
- package/src/install-ci-capabilities/close-job.ts +32 -0
- package/src/install-ci-capabilities/example-noop.ts +24 -0
- package/src/install-ci-capabilities/intake.ts +34 -0
- package/src/install-ci-capabilities/verify.ts +33 -0
- package/src/install-ci-core.ts +1088 -0
- package/src/install-ci-github.ts +376 -0
- package/src/install-ci.ts +552 -0
- package/src/intake-event.ts +102 -0
- package/src/intake-marker.ts +195 -0
- package/src/intake-triage.ts +138 -0
- package/src/intake-trigger-template.ts +591 -0
- package/src/intake.ts +2445 -0
- package/src/integration-core.ts +3065 -0
- package/src/integrator.ts +771 -0
- package/src/isolation.ts +484 -0
- package/src/issue-provider.ts +733 -0
- package/src/item-lock.ts +1858 -0
- package/src/item-path.ts +75 -0
- package/src/ledger-lint.ts +332 -0
- package/src/ledger-read.ts +924 -0
- package/src/ledger-write.ts +865 -0
- package/src/lifecycle-gather.ts +298 -0
- package/src/lifecycle-pools.ts +250 -0
- package/src/merge-question-surfacer.ts +496 -0
- package/src/mint-adr.ts +362 -0
- package/src/mirror-pool-scan.ts +240 -0
- package/src/needs-attention.ts +1506 -0
- package/src/orphan-sidecar.ts +150 -0
- package/src/output.ts +89 -0
- package/src/pi-harness.ts +403 -0
- package/src/placement.ts +131 -0
- package/src/prd-to-spec.ts +1023 -0
- package/src/prepare.ts +230 -0
- package/src/prompt.ts +760 -0
- package/src/readiness.ts +98 -0
- package/src/reap-branches.ts +278 -0
- package/src/recover-isolated.ts +276 -0
- package/src/registry.ts +475 -0
- package/src/repo-config.ts +550 -0
- package/src/repo-key.ts +74 -0
- package/src/repo-mirror.ts +367 -0
- package/src/retry-backoff.ts +130 -0
- package/src/review-gate.ts +389 -0
- package/src/review-verdict.ts +422 -0
- package/src/run.ts +1430 -0
- package/src/scan.ts +611 -0
- package/src/select-order.ts +143 -0
- package/src/select-priority.ts +266 -0
- package/src/select.ts +62 -0
- package/src/session-path.ts +153 -0
- package/src/sidecar-apply.ts +216 -0
- package/src/sidecar.ts +700 -0
- package/src/slug-namespace.ts +367 -0
- package/src/spec-complete.ts +118 -0
- package/src/start.ts +974 -0
- package/src/status.ts +441 -0
- package/src/surface-gate.ts +337 -0
- package/src/surface-persist.ts +241 -0
- package/src/tasker-review-loop.ts +671 -0
- package/src/tasking-eligibility.ts +114 -0
- package/src/tasking-lock.ts +416 -0
- package/src/tasking.ts +1438 -0
- package/src/triage-gate.ts +248 -0
- package/src/triage-persist.ts +570 -0
- package/src/verdict-json.ts +73 -0
- package/src/verify-workflow-template.ts +159 -0
- package/src/verify.ts +123 -0
- package/src/watch-session.ts +397 -0
- package/src/work-layout.ts +262 -0
- package/src/work-on.ts +660 -0
- package/src/workspace.ts +502 -0
package/dist/intake.js
ADDED
|
@@ -0,0 +1,1628 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { runAsync } from './git.js';
|
|
4
|
+
import { workFolderRel, workItemRel } from './work-layout.js';
|
|
5
|
+
import { paramCase } from './brand.js';
|
|
6
|
+
import { performIntegration, } from './integration-core.js';
|
|
7
|
+
import { integrationFromFlags } from './complete.js';
|
|
8
|
+
import { placementFolder, resolvePlacement, } from './placement.js';
|
|
9
|
+
import { identityEnv, assertTransportAllowed, } from './identity.js';
|
|
10
|
+
import { NullHarness } from './harness.js';
|
|
11
|
+
import { workBranchRef } from './slug-namespace.js';
|
|
12
|
+
import { launchWithOptionalWatch } from './agent-launch.js';
|
|
13
|
+
import { GitHubIssueProvider, PROCESSING_LOCK_LABEL, } from './issue-provider.js';
|
|
14
|
+
import { extractJsonObjectSpan } from './verdict-json.js';
|
|
15
|
+
import { parseReviewVerdict, reviewDisciplinePrompt, verdictContractPrompt, } from './review-verdict.js';
|
|
16
|
+
import { stampIntakeMarker, computeSeenDelta, } from './intake-marker.js';
|
|
17
|
+
import { triageIntake } from './intake-triage.js';
|
|
18
|
+
import { renderTaskBody, renderSpecBody } from './buildable-body.js';
|
|
19
|
+
const DEFAULT_ARBITER = 'origin';
|
|
20
|
+
/**
|
|
21
|
+
* **The STAGED-prds dir** (prd `staging-pool-position-gate-and-trust-model`,
|
|
22
|
+
* task `pre-prd-staging-pool-split-and-untrusted-prd-placement`, governing
|
|
23
|
+
* ADR `placement-is-runner-deterministic-humanonly-is-agent-judgement`). When
|
|
24
|
+
* the runner-deterministic placement resolver picks the staging side for an
|
|
25
|
+
* `intake`-authored prd, the runner writes the prd file HERE instead of in
|
|
26
|
+
* `work/prds/ready/`. An item born in `prds/proposed/` is durable + readable but NOT in
|
|
27
|
+
* the tasking candidate POOL (`work/prds/ready/` is the pool). A runner/human-owned promotion
|
|
28
|
+
* ({@link promoteFromPreSpec} in `needs-attention.ts`) moves an approved prd
|
|
29
|
+
* `prds/proposed/ → prds/ready/` to make it taskable.
|
|
30
|
+
*/
|
|
31
|
+
export const STAGED_SPECS_DIR = workFolderRel('specs-proposed');
|
|
32
|
+
/**
|
|
33
|
+
* The POOL folder specs land in when the runner-deterministic placement
|
|
34
|
+
* resolver chooses the pool side (`specsLandIn: 'ready'` + a trusted origin, or
|
|
35
|
+
* an `--specs-land-in ready` operator override). This is `work/specs/ready/`,
|
|
36
|
+
* the tasking candidate pool.
|
|
37
|
+
*/
|
|
38
|
+
const POOL_SPECS_DIR = workFolderRel('specs-ready');
|
|
39
|
+
/** The placement slots for the spec lifecycle (folder names). */
|
|
40
|
+
const SPEC_PLACEMENT_SLOTS = {
|
|
41
|
+
staging: STAGED_SPECS_DIR,
|
|
42
|
+
pool: POOL_SPECS_DIR,
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Map the `specsLandIn` value spelling (`pre-proposed` | `ready`) onto the
|
|
46
|
+
* resolver's lifecycle-generic side enum (`staging` | `pool`). Returns
|
|
47
|
+
* `undefined` when no value is set, so the resolver's next precedence rung
|
|
48
|
+
* applies (the built-in floor). The spec twin of `landingToSide` on the
|
|
49
|
+
* tasker path — same shape, different slots.
|
|
50
|
+
*/
|
|
51
|
+
function specLandingToSide(landing) {
|
|
52
|
+
if (landing === 'pre-proposed')
|
|
53
|
+
return 'staging';
|
|
54
|
+
if (landing === 'ready')
|
|
55
|
+
return 'pool';
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
/** Default per-outcome integration mode when no flag selects one — propose (matches `do`). */
|
|
59
|
+
const DEFAULT_INTEGRATION = 'propose';
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the GRANULAR per-type axis (`--merge-<t>` / `--propose-<t>`) for ONE
|
|
62
|
+
* artifact type, REUSING {@link integrationFromFlags} for its mutual-exclusion +
|
|
63
|
+
* "mutually exclusive" error message (the same-type-both usage error) — so the
|
|
64
|
+
* granular axis is NOT a forked second resolver, just `integrationFromFlags`
|
|
65
|
+
* applied to the per-type pair. Returns the granular mode, or `undefined` when
|
|
66
|
+
* neither granular flag for this type was given (the aggregate/default then
|
|
67
|
+
* decides). The error message is reworded to name the granular flag pair.
|
|
68
|
+
*/
|
|
69
|
+
function granularFromFlags(type, merge, propose) {
|
|
70
|
+
try {
|
|
71
|
+
return integrationFromFlags({ merge, propose });
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new Error(`--merge-${type} and --propose-${type} are mutually exclusive; pass at most one.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The PURE per-outcome integration mode resolution (prd `issue-intake` US #9 —
|
|
79
|
+
* the canonical table). Given ONLY the flag set, resolve BOTH per-type modes in
|
|
80
|
+
* one eager pass (so a usage error is caught before the runtime verdict is even
|
|
81
|
+
* known). The rules, all decided in the prd:
|
|
82
|
+
*
|
|
83
|
+
* - **unset ⇒ propose for BOTH** (conservative default; matches `do`).
|
|
84
|
+
* - **aggregates:** `--merge` ⇒ merge both; `--propose` ⇒ propose both (this axis
|
|
85
|
+
* COMPOSES the existing {@link integrationFromFlags}, reusing its mutual
|
|
86
|
+
* exclusion + error message).
|
|
87
|
+
* - **granular routes per type:** `--merge-spec` merges a spec (and leaves a task at
|
|
88
|
+
* the aggregate/default), etc.
|
|
89
|
+
* - **GRANULAR OVERRIDES AGGREGATE:** `--merge --propose-task` ⇒ merge a spec,
|
|
90
|
+
* propose a task.
|
|
91
|
+
* - **same type + both modes is a usage ERROR:** `--merge-spec --propose-spec` (and
|
|
92
|
+
* `--merge-task --propose-task`), and the aggregate `--merge --propose`.
|
|
93
|
+
*
|
|
94
|
+
* Throws (a usage error) on any mutually-exclusive pair. The dispatcher picks the
|
|
95
|
+
* field matching the runtime verdict's type; ask/bounce never integrate, so the
|
|
96
|
+
* modes are no-ops for them.
|
|
97
|
+
*
|
|
98
|
+
* `defaultMode` is the FALLBACK when NEITHER a granular nor the aggregate flag
|
|
99
|
+
* selects a mode for a type — it defaults to `propose` (so the pure table reads
|
|
100
|
+
* "unset ⇒ propose for both"), but the CLI passes the per-repo/global
|
|
101
|
+
* config-resolved mode so the established precedence chain (flag > per-repo >
|
|
102
|
+
* global > default) is preserved, exactly as `do`/`complete` resolve it.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveIntakeIntegrationModes(flags, defaultMode = DEFAULT_INTEGRATION) {
|
|
105
|
+
// AGGREGATE axis — reuse the existing resolver (its mutual exclusion + the
|
|
106
|
+
// "--merge and --propose are mutually exclusive" message). `undefined` ⇒ unset.
|
|
107
|
+
const aggregate = integrationFromFlags({
|
|
108
|
+
merge: flags.merge,
|
|
109
|
+
propose: flags.propose,
|
|
110
|
+
});
|
|
111
|
+
// GRANULAR axes — `integrationFromFlags` per type (the same-type-both error).
|
|
112
|
+
const specGranular = granularFromFlags('spec', flags.mergeSpec, flags.proposeSpec);
|
|
113
|
+
const taskGranular = granularFromFlags('task', flags.mergeTask, flags.proposeTask);
|
|
114
|
+
// GRANULAR OVERRIDES AGGREGATE; aggregate over the (config/propose) default.
|
|
115
|
+
// The result KEYS carry the `spec` vocabulary (canonical); the per-type flag axes
|
|
116
|
+
// (`--merge-spec`/`--merge-task`) carry the same `spec` spelling (the cli-flag
|
|
117
|
+
// rename landed in batch 4f).
|
|
118
|
+
return {
|
|
119
|
+
spec: specGranular ?? aggregate ?? defaultMode,
|
|
120
|
+
task: taskGranular ?? aggregate ?? defaultMode,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Run `intake <N>` end-to-end (the LOCAL one-shot). Never throws for the expected
|
|
125
|
+
* agent-failed / stale / usage cases — those are returned with the corresponding
|
|
126
|
+
* exit code and outcome. The runner owns all git/seam side-effects; the agent only
|
|
127
|
+
* DRAFTS the verdict.
|
|
128
|
+
*/
|
|
129
|
+
export async function performIntake(options) {
|
|
130
|
+
const note = options.note ?? (() => { });
|
|
131
|
+
const arbiter = options.arbiter ?? DEFAULT_ARBITER;
|
|
132
|
+
const cwd = options.cwd;
|
|
133
|
+
// `env` is intake's GIT + provider env, scoped to the configured identity (the
|
|
134
|
+
// `gh` issue ops, the push, the PR). The intake AGENT launches (decision agent
|
|
135
|
+
// + lone-task review agent) read `options.env` directly — they stay AMBIENT
|
|
136
|
+
// (an agent must not act as the bot). Absent identity ⇒ `options.env` unchanged.
|
|
137
|
+
// A configured identity that cannot be resolved (e.g. `tokenEnv` names an unset
|
|
138
|
+
// env var) is a clean usage error, never a crash or a silent ambient fallback.
|
|
139
|
+
const issueNumber = options.issueNumber;
|
|
140
|
+
let env;
|
|
141
|
+
try {
|
|
142
|
+
env = identityEnv(options.identity, options.env ?? process.env);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
146
|
+
note(message);
|
|
147
|
+
return { exitCode: 1, outcome: 'usage-error', issueNumber, message };
|
|
148
|
+
}
|
|
149
|
+
const issueProvider = options.issueProvider ?? new GitHubIssueProvider();
|
|
150
|
+
// Push-time transport-coherence guard (identity): if a configured identity
|
|
151
|
+
// forbids the arbiter's transport, fail with a clear message rather than
|
|
152
|
+
// silently pushing under an ambient credential. Resolve the arbiter URL softly
|
|
153
|
+
// (a non-zero/unknown URL is skipped — the guard is a no-op without an identity
|
|
154
|
+
// or a resolvable URL).
|
|
155
|
+
if (options.identity !== undefined) {
|
|
156
|
+
const urlRes = await runAsync('git', ['remote', 'get-url', arbiter], cwd, {
|
|
157
|
+
env,
|
|
158
|
+
});
|
|
159
|
+
if (urlRes.status === 0) {
|
|
160
|
+
try {
|
|
161
|
+
assertTransportAllowed(options.identity, urlRes.stdout.trim());
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
165
|
+
note(message);
|
|
166
|
+
return { exitCode: 1, outcome: 'usage-error', issueNumber, message };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// 1. READ the issue + thread via the seam (the core never imports `gh`; only the
|
|
171
|
+
// adapter shells out). A read failure surfaces as a usage error — `intake`
|
|
172
|
+
// cannot decide without the issue.
|
|
173
|
+
let issue;
|
|
174
|
+
let comments;
|
|
175
|
+
try {
|
|
176
|
+
issue = await issueProvider.getIssue({ cwd, issueNumber, env });
|
|
177
|
+
comments = await issueProvider.listComments({ cwd, issueNumber, env });
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
181
|
+
const message = `Could not read issue #${issueNumber}: ${detail}`;
|
|
182
|
+
note(message);
|
|
183
|
+
return { exitCode: 1, outcome: 'usage-error', issueNumber, message };
|
|
184
|
+
}
|
|
185
|
+
// 2. ACQUIRE the `processing` LOCK (prd `issue-intake` US #10): a TRANSIENT concurrency mutex
|
|
186
|
+
// that serialises two concurrent runs on the SAME issue. Read the labels; if
|
|
187
|
+
// the lock is ALREADY present, BACK OFF (do nothing — another run owns it). The
|
|
188
|
+
// winner ADDS the label and proceeds; the label is REMOVED on finish (success
|
|
189
|
+
// OR handled failure, in the `finally` below). It is NOT a `work/` CAS and NOT a
|
|
190
|
+
// label state-machine (ADR §12) — ONE transient lock label.
|
|
191
|
+
//
|
|
192
|
+
// Fail-vs-degrade (maintainer decision): a lock that is MEANINGFUL but cannot
|
|
193
|
+
// be taken must NOT silently proceed lock-less. Only a genuinely-UNSUPPORTED
|
|
194
|
+
// provider (no label concept at all) legitimately degrades to best-effort (the
|
|
195
|
+
// spec's provider-pluggability; CI's per-issue concurrency group is then the
|
|
196
|
+
// only serialiser — out of scope here). A real FAILURE on a label-supporting
|
|
197
|
+
// provider (e.g. `gh` unauthenticated) FAILS the run with the REAL cause
|
|
198
|
+
// surfaced, rather than misattributing it or proceeding without serialisation.
|
|
199
|
+
const labels = await issueProvider.getLabels({ cwd, issueNumber, env });
|
|
200
|
+
if (labels.outcome === 'failed') {
|
|
201
|
+
// The provider HAS labels but we could not READ the lock state — we cannot tell
|
|
202
|
+
// whether another run holds it, so guessing "free" could let two runs proceed.
|
|
203
|
+
// FAIL with the real cause (the actual `gh` stderr), not a hard-coded guess.
|
|
204
|
+
const message = `Intake of issue #${issueNumber} could not acquire the ` +
|
|
205
|
+
`\`${PROCESSING_LOCK_LABEL}\` lock: ${labels.instruction}`;
|
|
206
|
+
note(message);
|
|
207
|
+
return { exitCode: 1, outcome: 'lock-failed', issueNumber, message };
|
|
208
|
+
}
|
|
209
|
+
if (labels.outcome === 'ok' &&
|
|
210
|
+
labels.labels.includes(PROCESSING_LOCK_LABEL)) {
|
|
211
|
+
const message = `Intake of issue #${issueNumber} backed off: the \`${PROCESSING_LOCK_LABEL}\` ` +
|
|
212
|
+
`lock is already held by a concurrent run; doing nothing.`;
|
|
213
|
+
note(message);
|
|
214
|
+
return { exitCode: 0, outcome: 'locked', issueNumber, message };
|
|
215
|
+
}
|
|
216
|
+
let locked = false;
|
|
217
|
+
if (labels.outcome === 'ok') {
|
|
218
|
+
const acquired = await issueProvider.addLabel({
|
|
219
|
+
cwd,
|
|
220
|
+
issueNumber,
|
|
221
|
+
label: PROCESSING_LOCK_LABEL,
|
|
222
|
+
env,
|
|
223
|
+
});
|
|
224
|
+
if (acquired.outcome === 'failed') {
|
|
225
|
+
// The provider HAS labels but the ACQUIRE failed for a real reason (e.g. `gh`
|
|
226
|
+
// lost auth, or the label could not be created on a fresh repo). The lock is
|
|
227
|
+
// meaningful but unacquirable → FAIL with the real cause, do NOT proceed
|
|
228
|
+
// lock-less (which would let a concurrent run race us).
|
|
229
|
+
const message = `Intake of issue #${issueNumber} could not acquire the ` +
|
|
230
|
+
`\`${PROCESSING_LOCK_LABEL}\` lock: ${acquired.instruction}`;
|
|
231
|
+
note(message);
|
|
232
|
+
return { exitCode: 1, outcome: 'lock-failed', issueNumber, message };
|
|
233
|
+
}
|
|
234
|
+
locked = acquired.applied;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// Non-label provider (genuinely UNSUPPORTED) → the ONLY legitimate degrade:
|
|
238
|
+
// proceed without the lock, surfaced honestly.
|
|
239
|
+
note(`Processing lock degraded: ${labels.instruction}`);
|
|
240
|
+
}
|
|
241
|
+
// INTERRUPTION-SAFETY (maintainer point 3): the `finally` below releases on every
|
|
242
|
+
// EXCEPTION path, but a SIGINT/SIGTERM (Ctrl-C, kill) unwinds the process WITHOUT
|
|
243
|
+
// running `finally` — which would LEAK the lock label and block all future intake
|
|
244
|
+
// runs on this issue. While the lock is held we install signal handlers that
|
|
245
|
+
// release it best-effort before the process exits. A leaked lock must ALSO be
|
|
246
|
+
// recoverable by hand and that recovery must be DISCOVERABLE, so we surface the
|
|
247
|
+
// exact manual command (`gh issue edit <N> --remove-label <label>`) whenever the
|
|
248
|
+
// best-effort release does not confirm.
|
|
249
|
+
const manualRecovery = `If the \`${PROCESSING_LOCK_LABEL}\` lock is left behind, release it with: ` +
|
|
250
|
+
`gh issue edit ${issueNumber} --remove-label '${PROCESSING_LOCK_LABEL}'`;
|
|
251
|
+
const releaseLock = createLockReleaser({
|
|
252
|
+
locked,
|
|
253
|
+
issueProvider,
|
|
254
|
+
cwd,
|
|
255
|
+
issueNumber,
|
|
256
|
+
env,
|
|
257
|
+
note,
|
|
258
|
+
manualRecovery,
|
|
259
|
+
});
|
|
260
|
+
const onSignal = (signal) => {
|
|
261
|
+
// Synchronous best-effort release on interruption, then re-raise the default
|
|
262
|
+
// disposition so the process still exits with the conventional signal code.
|
|
263
|
+
if (locked) {
|
|
264
|
+
note(`Received ${signal}; releasing the \`${PROCESSING_LOCK_LABEL}\` lock on issue #${issueNumber} before exit.`);
|
|
265
|
+
}
|
|
266
|
+
releaseLock.releaseSync();
|
|
267
|
+
process.removeListener('SIGINT', onSignal);
|
|
268
|
+
process.removeListener('SIGTERM', onSignal);
|
|
269
|
+
process.kill(process.pid, signal);
|
|
270
|
+
};
|
|
271
|
+
if (locked) {
|
|
272
|
+
process.once('SIGINT', onSignal);
|
|
273
|
+
process.once('SIGTERM', onSignal);
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
return await decideAndDispatch(options, cwd, issue, comments, {
|
|
277
|
+
arbiter,
|
|
278
|
+
issueProvider,
|
|
279
|
+
note,
|
|
280
|
+
// The identity-scoped GIT/provider env (the `gh` ops, push, PR). The AGENT
|
|
281
|
+
// launches inside dispatch read `options.env` (ambient) — not this.
|
|
282
|
+
gitEnv: env,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
// RELEASE the lock on FINISH (success OR handled failure). Only the winner that
|
|
287
|
+
// actually acquired it releases it — a degraded/best-effort run holds nothing.
|
|
288
|
+
process.removeListener('SIGINT', onSignal);
|
|
289
|
+
process.removeListener('SIGTERM', onSignal);
|
|
290
|
+
await releaseLock.release();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Build the lock RELEASER for {@link performIntake}: one `release()` (the normal
|
|
295
|
+
* async finish path) and one `releaseSync()` (the signal-handler path — a
|
|
296
|
+
* best-effort synchronous release that must run inside a signal handler). Both are
|
|
297
|
+
* no-ops when the run never held the lock (a degraded/unsupported run holds
|
|
298
|
+
* nothing). When a release does not CONFIRM, the manual-recovery hint is surfaced
|
|
299
|
+
* so a leaked lock stays recoverable AND discoverable (maintainer point 3).
|
|
300
|
+
*/
|
|
301
|
+
function createLockReleaser(params) {
|
|
302
|
+
const { locked, issueProvider, cwd, issueNumber, env, note, manualRecovery } = params;
|
|
303
|
+
let released = false;
|
|
304
|
+
const surfaceFailure = (instruction) => {
|
|
305
|
+
note(`Processing lock release degraded: ${instruction}`);
|
|
306
|
+
note(manualRecovery);
|
|
307
|
+
};
|
|
308
|
+
return {
|
|
309
|
+
async release() {
|
|
310
|
+
if (!locked || released) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
released = true;
|
|
314
|
+
const result = await issueProvider.removeLabel({
|
|
315
|
+
cwd,
|
|
316
|
+
issueNumber,
|
|
317
|
+
label: PROCESSING_LOCK_LABEL,
|
|
318
|
+
env,
|
|
319
|
+
});
|
|
320
|
+
if (!result.applied) {
|
|
321
|
+
surfaceFailure(result.instruction);
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
releaseSync() {
|
|
325
|
+
if (!locked || released) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
released = true;
|
|
329
|
+
// A signal handler cannot await. The GitHub adapter's `removeLabel` shells out
|
|
330
|
+
// SYNCHRONOUSLY (spawnSync) inside its async wrapper, so firing it here still
|
|
331
|
+
// runs the `gh` call before the process exits — but we cannot READ the result
|
|
332
|
+
// synchronously through the async seam, so we ALWAYS surface the manual-recovery
|
|
333
|
+
// hint too. That keeps a leaked lock both recoverable AND discoverable even if
|
|
334
|
+
// the in-handler release did not complete (maintainer point 3).
|
|
335
|
+
void issueProvider.removeLabel({
|
|
336
|
+
cwd,
|
|
337
|
+
issueNumber,
|
|
338
|
+
label: PROCESSING_LOCK_LABEL,
|
|
339
|
+
env,
|
|
340
|
+
});
|
|
341
|
+
note(manualRecovery);
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* The DECIDE (prompt → verdict) + DISPATCH (the four-outcome table) band, run
|
|
347
|
+
* INSIDE the `processing` lock {@link performIntake} acquires/releases around it.
|
|
348
|
+
* Split out so the lock release is a clean `try`/`finally` in the caller (the lock
|
|
349
|
+
* MUST release on every terminal path — success or handled failure). The agent
|
|
350
|
+
* DRAFTS only; the runner owns every git/seam side-effect here.
|
|
351
|
+
*/
|
|
352
|
+
async function decideAndDispatch(options, cwd, issue, comments, ctx) {
|
|
353
|
+
const { arbiter, issueProvider, note } = ctx;
|
|
354
|
+
const issueNumber = issue.number;
|
|
355
|
+
// `env` here is the identity-scoped GIT/provider env (the runner's `gh`/git
|
|
356
|
+
// ops). The AGENT launches (decision agent, lone-task review) use the AMBIENT
|
|
357
|
+
// `options.env` — an agent must not act as the bot.
|
|
358
|
+
const env = ctx.gitEnv;
|
|
359
|
+
// TRIAGE (deterministic, under the lock, BEFORE the prompt): decide whether to run
|
|
360
|
+
// the decision at all, built ENTIRELY on intake's own MARKER on the thread (no
|
|
361
|
+
// sidecar/cursor/bot-identity). It SKIPS when intake has the last word
|
|
362
|
+
// (`no-new-input`) or the issue is already terminal (`already-terminal`), and runs
|
|
363
|
+
// the prompt ONLY on genuine new human input. This is also the COMPLETE fix for the
|
|
364
|
+
// self-trigger hazard: intake's own freshly-posted comment carries a marker, so it
|
|
365
|
+
// is excluded from the human-comment check by construction.
|
|
366
|
+
const triage = triageIntake(comments);
|
|
367
|
+
if (triage.action === 'skip') {
|
|
368
|
+
const message = triage.outcome === 'no-new-input'
|
|
369
|
+
? `Intake of issue #${issueNumber} found nothing new: it has the last word ` +
|
|
370
|
+
`on the thread and has already seen every human comment up to it; doing ` +
|
|
371
|
+
`nothing (the decision prompt did not run).`
|
|
372
|
+
: `Intake of issue #${issueNumber} skipped: the issue was already ` +
|
|
373
|
+
`transformed (a terminal intake marker is on the thread); a later human ` +
|
|
374
|
+
`comment does not re-open it (the decision prompt did not run).`;
|
|
375
|
+
note(message);
|
|
376
|
+
return { exitCode: 0, outcome: triage.outcome, issueNumber, message };
|
|
377
|
+
}
|
|
378
|
+
// DECIDE: prompt → VERDICT. The agent DRAFTS only (no git, no seam ops). Tests
|
|
379
|
+
// inject a canned verdict (the dispatcher's testable seam); production wires the
|
|
380
|
+
// harness. The prompt's judgement is not unit-tested — only the dispatch.
|
|
381
|
+
const prompt = buildIntakeDecisionSpec(issue, comments, triage);
|
|
382
|
+
let verdict;
|
|
383
|
+
try {
|
|
384
|
+
verdict = await runDecision(options, cwd, issue, comments, prompt);
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
388
|
+
const message = `Intake decision failed for issue #${issueNumber}: ${detail}`;
|
|
389
|
+
note(message);
|
|
390
|
+
return { exitCode: 1, outcome: 'agent-failed', issueNumber, message };
|
|
391
|
+
}
|
|
392
|
+
// DISPATCH on the verdict — the FULL four-outcome decision table (prd
|
|
393
|
+
// `issue-intake`). The agent only DRAFTED the verdict; the runner owns every
|
|
394
|
+
// git/seam side-effect below (the in-band boundary): the write + integrate
|
|
395
|
+
// (task/prd) and the `postIssueComment` (ask/bounce).
|
|
396
|
+
//
|
|
397
|
+
// PER-OUTCOME integration (prd `issue-intake` US #9): the resolved mode is keyed on the runtime
|
|
398
|
+
// artifact TYPE — a `task` verdict integrates with the task mode, a `spec`
|
|
399
|
+
// verdict with the spec mode. Unset ⇒ propose for both. ask/bounce never
|
|
400
|
+
// integrate, so the modes are no-ops for them.
|
|
401
|
+
const modes = options.integration ?? { task: 'propose', spec: 'propose' };
|
|
402
|
+
// The per-run `seen=` DELTA (the HUMAN comment ids intake READ this run, excluding
|
|
403
|
+
// its own marker-comments + already-seen ids) the marker records on every comment
|
|
404
|
+
// intake posts — the chain-model primitive the TRIAGE unions into `seenSet`.
|
|
405
|
+
const seenDelta = computeSeenDelta(comments);
|
|
406
|
+
switch (verdict.outcome) {
|
|
407
|
+
case 'task':
|
|
408
|
+
return dispatchTask({
|
|
409
|
+
verdict,
|
|
410
|
+
issueNumber,
|
|
411
|
+
cwd,
|
|
412
|
+
arbiter,
|
|
413
|
+
integration: modes.task,
|
|
414
|
+
// The origin-trust STAMP, passed IN (not resolved here): the emitted task
|
|
415
|
+
// carries `origin: issue` + this verdict so the becomes-code checkpoint is
|
|
416
|
+
// not laundered. Unset ⇒ unstamped (a local intake ⇒ human/trusted).
|
|
417
|
+
originTrust: options.originTrust,
|
|
418
|
+
noPR: options.noPR,
|
|
419
|
+
providerInstance: options.providerInstance,
|
|
420
|
+
issueProvider,
|
|
421
|
+
// The bounded lone-task review seam (tests inject a canned verdict;
|
|
422
|
+
// production wires the harness via the default below).
|
|
423
|
+
reviewTask: resolveLoneTaskReviewGate(options),
|
|
424
|
+
seen: seenDelta,
|
|
425
|
+
env,
|
|
426
|
+
// The lone-task review AGENT launches AMBIENT (an agent must not act as
|
|
427
|
+
// the bot); `env` above is the identity-scoped git/provider env.
|
|
428
|
+
agentEnv: options.env,
|
|
429
|
+
note,
|
|
430
|
+
});
|
|
431
|
+
// The `spec` outcome is the parent-spec verdict; it dispatches through
|
|
432
|
+
// `modes.spec`. HARD CUTOVER: the legacy `prd` outcome case is GONE.
|
|
433
|
+
case 'spec':
|
|
434
|
+
return dispatchSpec({
|
|
435
|
+
verdict,
|
|
436
|
+
issueNumber,
|
|
437
|
+
cwd,
|
|
438
|
+
arbiter,
|
|
439
|
+
integration: modes.spec,
|
|
440
|
+
// Same origin-trust stamp on the prd outcome (propagated onto its tasks
|
|
441
|
+
// later by the tasker). Passed IN; not resolved here.
|
|
442
|
+
originTrust: options.originTrust,
|
|
443
|
+
noPR: options.noPR,
|
|
444
|
+
// RUNNER-DETERMINISTIC PLACEMENT (task
|
|
445
|
+
// `pre-prd-staging-pool-split-and-untrusted-prd-placement`): the
|
|
446
|
+
// configured-default + explicit-flag rungs, fed into the SHARED placement
|
|
447
|
+
// resolver alongside the `originTrust` stamp above. The resolver decides
|
|
448
|
+
// `prds/proposed/` (staging) vs `prds/ready/` (the tasking pool); `intake` never
|
|
449
|
+
// places itself.
|
|
450
|
+
specsLandIn: options.specsLandIn,
|
|
451
|
+
explicitSpecsLandIn: options.explicitSpecsLandIn,
|
|
452
|
+
providerInstance: options.providerInstance,
|
|
453
|
+
issueProvider,
|
|
454
|
+
seen: seenDelta,
|
|
455
|
+
env,
|
|
456
|
+
note,
|
|
457
|
+
});
|
|
458
|
+
case 'ask':
|
|
459
|
+
return dispatchComment({
|
|
460
|
+
outcome: 'asked',
|
|
461
|
+
cwd,
|
|
462
|
+
issueNumber,
|
|
463
|
+
issueProvider,
|
|
464
|
+
// The drafted clarifying question; a thin fallback keeps the comment
|
|
465
|
+
// non-empty if the agent left it blank.
|
|
466
|
+
body: verdict.question && verdict.question.trim() !== ''
|
|
467
|
+
? verdict.question
|
|
468
|
+
: `Could you clarify issue #${issueNumber} so it can be acted on?`,
|
|
469
|
+
// STAMP the MARKER recording `kind=ask` (non-terminal — the TRIAGE owns
|
|
470
|
+
// that) + the `seen=` delta, so a re-run recognises this as intake's own
|
|
471
|
+
// turn and resumes only on genuine new human input.
|
|
472
|
+
markerKind: 'ask',
|
|
473
|
+
seen: seenDelta,
|
|
474
|
+
env,
|
|
475
|
+
note,
|
|
476
|
+
});
|
|
477
|
+
case 'bounce':
|
|
478
|
+
return dispatchComment({
|
|
479
|
+
outcome: 'bounced',
|
|
480
|
+
cwd,
|
|
481
|
+
issueNumber,
|
|
482
|
+
issueProvider,
|
|
483
|
+
// The drafted bounce message; a thin fallback restates the "file separate
|
|
484
|
+
// issues" ask. A bounce is TERMINAL: the issue is CLOSED atomically (this
|
|
485
|
+
// text as the closing comment + reason not planned).
|
|
486
|
+
body: verdict.bounceMessage && verdict.bounceMessage.trim() !== ''
|
|
487
|
+
? verdict.bounceMessage
|
|
488
|
+
: `This issue looks like multiple unrelated concerns — please file ` +
|
|
489
|
+
`separate issues so each can be intaken on its own.`,
|
|
490
|
+
// STAMP `kind=bounced` (TERMINAL — the TRIAGE then SKIPS `already-terminal`
|
|
491
|
+
// on a later human comment) + the `seen=` delta.
|
|
492
|
+
markerKind: 'bounced',
|
|
493
|
+
seen: seenDelta,
|
|
494
|
+
env,
|
|
495
|
+
note,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* DISPATCH the `ask` / `bounce` outcomes — the SHARED comment band, which now
|
|
501
|
+
* BRANCHES on the outcome:
|
|
502
|
+
*
|
|
503
|
+
* - **ask** (non-terminal): `postIssueComment` the drafted question, emit NOTHING,
|
|
504
|
+
* and LEAVE THE ISSUE OPEN — it waits for the thread to be answered (a later run
|
|
505
|
+
* resumes from it). The task/prd path also never closes (CI's close-job does,
|
|
506
|
+
* via the `issue:` field). Intake closes ONLY on BOUNCE.
|
|
507
|
+
* - **bounce** (TERMINAL): the asks are unrelated and must be re-filed, so an OPEN
|
|
508
|
+
* issue is a dishonest "still in play" signal. Intake CLOSES the issue
|
|
509
|
+
* ATOMICALLY via a single `closeIssue` carrying the bounce text as the closing
|
|
510
|
+
* comment + `reason: not planned` (one call — no post-then-close partial-failure
|
|
511
|
+
* window). The result's `closed` reflects it.
|
|
512
|
+
*
|
|
513
|
+
* Both the comment poster and the atomic close are advisory and DEGRADE (a
|
|
514
|
+
* missing/unauthenticated `gh` never throws — the text/real cause is surfaced via
|
|
515
|
+
* `ghFailureReason`, never a hard-coded guess), so the terminal outcome is
|
|
516
|
+
* unchanged (`asked`/`bounced`, exit 0) and the run still terminates cleanly.
|
|
517
|
+
*/
|
|
518
|
+
async function dispatchComment(params) {
|
|
519
|
+
const { outcome, cwd, issueNumber, issueProvider, body, markerKind, seen, env, note, } = params;
|
|
520
|
+
// STAMP the intake MARKER onto the body so a re-run recognises this as intake's
|
|
521
|
+
// own comment (the SOLE self-recognition signal — no author identity). Hidden HTML
|
|
522
|
+
// comment; renders as nothing, present in the raw markdown the TRIAGE parses.
|
|
523
|
+
const stamped = stampIntakeMarker(body, { kind: markerKind, seen });
|
|
524
|
+
if (outcome === 'bounced') {
|
|
525
|
+
// BOUNCE is TERMINAL: CLOSE the issue ATOMICALLY (bounce text as the closing
|
|
526
|
+
// comment + reason not planned) in ONE call — no separate postIssueComment, no
|
|
527
|
+
// post-then-close window. The close DEGRADES (never throws) on a missing/
|
|
528
|
+
// unauthenticated `gh`, surfacing the REAL cause; the terminal outcome stays
|
|
529
|
+
// `bounced`/exit 0 regardless.
|
|
530
|
+
const close = await issueProvider.closeIssue({
|
|
531
|
+
cwd,
|
|
532
|
+
issueNumber,
|
|
533
|
+
comment: stamped,
|
|
534
|
+
reason: 'not planned',
|
|
535
|
+
env,
|
|
536
|
+
});
|
|
537
|
+
const tail = close.closed
|
|
538
|
+
? 'the issue was closed (as not planned) with the bounce comment'
|
|
539
|
+
: `the issue could NOT be closed (${close.instruction})`;
|
|
540
|
+
const message = `Intake bounced issue #${issueNumber}; emitted no artifact and closed the ` +
|
|
541
|
+
`issue as not planned — ${tail}.`;
|
|
542
|
+
note(message);
|
|
543
|
+
return {
|
|
544
|
+
exitCode: 0,
|
|
545
|
+
outcome,
|
|
546
|
+
issueNumber,
|
|
547
|
+
commented: close.closed,
|
|
548
|
+
closed: close.closed,
|
|
549
|
+
message,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
// ASK (non-terminal): post the clarifying question and LEAVE THE ISSUE OPEN.
|
|
553
|
+
const posted = await issueProvider.postIssueComment({
|
|
554
|
+
cwd,
|
|
555
|
+
issueNumber,
|
|
556
|
+
body: stamped,
|
|
557
|
+
env,
|
|
558
|
+
});
|
|
559
|
+
const tail = posted.posted
|
|
560
|
+
? 'the comment was posted'
|
|
561
|
+
: `the comment could NOT be posted (${posted.instruction})`;
|
|
562
|
+
const message = `Intake asked a clarifying question on issue #${issueNumber}; emitted no ` +
|
|
563
|
+
`artifact and left the issue open — ${tail}.`;
|
|
564
|
+
note(message);
|
|
565
|
+
return {
|
|
566
|
+
exitCode: 0,
|
|
567
|
+
outcome,
|
|
568
|
+
issueNumber,
|
|
569
|
+
commented: posted.posted,
|
|
570
|
+
message,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* DISPATCH the `task` outcome: derive a content-derived slug, write
|
|
575
|
+
* `work/backlog/<slug>.md` (`covers: []`, NO `prd:`) carrying `issue: N` (the
|
|
576
|
+
* lone-task closure link, NOT `Fixes #N`), and integrate via {@link
|
|
577
|
+
* performIntegration}. The runner owns the git: it onboards a
|
|
578
|
+
* `work/<slug>` branch off fresh `<arbiter>/main`, then the lifecycle `stage`
|
|
579
|
+
* writes + stages the task and the band commits + rebases + integrates it. The
|
|
580
|
+
* agent did NO git/seam ops.
|
|
581
|
+
*/
|
|
582
|
+
async function dispatchTask(params) {
|
|
583
|
+
const { verdict, issueNumber, cwd, arbiter, integration, originTrust, noPR, providerInstance, issueProvider, reviewTask, seen, env, agentEnv, note, } = params;
|
|
584
|
+
// A content-derived slug — NEVER a counter (prd `issue-intake` US #8). Prefer the drafted
|
|
585
|
+
// `taskSlug`, else derive from the drafted title; sanitise either through
|
|
586
|
+
// `paramCase` so the filename + frontmatter slug are well-formed.
|
|
587
|
+
const slug = resolveSlug(verdict);
|
|
588
|
+
if (slug === '') {
|
|
589
|
+
const message = `Intake produced a 'task' verdict for issue #${issueNumber} with no usable ` +
|
|
590
|
+
`slug/title to derive a content-derived slug from (never a counter).`;
|
|
591
|
+
note(message);
|
|
592
|
+
return { exitCode: 1, outcome: 'usage-error', issueNumber, message };
|
|
593
|
+
}
|
|
594
|
+
const relPath = workItemRel('tasks-ready', `${slug}.md`);
|
|
595
|
+
// BOUNDED INTERNAL REVIEW (observation
|
|
596
|
+
// `intake-lone-task-skips-adversarial-review-the-prd-path-gets`, rulings A/B/C):
|
|
597
|
+
// the `do prd:` path gets `runTaskReviewLoop`; the lone-TASK path got NOTHING.
|
|
598
|
+
// AFTER the `task` verdict and BEFORE the write/integrate, run a bounded (3-round,
|
|
599
|
+
// HARD-CAPPED) adversarial self-review on the SINGLE drafted task. It mutates the
|
|
600
|
+
// candidate body IN MEMORY (no `work/backlog/` write pre-convergence). A launch/
|
|
601
|
+
// parse failure THROWS — `decideAndDispatch`'s try/catch maps it onto `agent-failed`
|
|
602
|
+
// (never a silent emit of the un-reviewed task).
|
|
603
|
+
let review;
|
|
604
|
+
try {
|
|
605
|
+
review = await runLoneTaskReview({
|
|
606
|
+
slug,
|
|
607
|
+
issueNumber,
|
|
608
|
+
draftTitle: verdict.taskTitle ?? slug,
|
|
609
|
+
draftBody: verdict.taskBody,
|
|
610
|
+
gate: reviewTask,
|
|
611
|
+
cwd,
|
|
612
|
+
// The review AGENT launches AMBIENT (never the identity-scoped env).
|
|
613
|
+
env: agentEnv,
|
|
614
|
+
note,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
catch (err) {
|
|
618
|
+
// A review-agent launch/parse FAILURE DEGRADES honestly onto the EXISTING
|
|
619
|
+
// `agent-failed` outcome (exit 1) — NEVER a silent emit of the un-reviewed
|
|
620
|
+
// task. The SAME try/catch discipline the decision step uses.
|
|
621
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
622
|
+
const message = `Intake lone-task review failed for issue #${issueNumber}: ${detail}`;
|
|
623
|
+
note(message);
|
|
624
|
+
return { exitCode: 1, outcome: 'agent-failed', issueNumber, message };
|
|
625
|
+
}
|
|
626
|
+
if (review.outcome === 'non-converge') {
|
|
627
|
+
// NON-CONVERGE (ruling C): FLIP the verdict TASK→ASK, reusing the EXISTING
|
|
628
|
+
// `asked` outcome + `kind=ask` marker. The ASK comment carries BOTH the proposed
|
|
629
|
+
// task DRAFT and the open question(s) in its BODY (NOT a new marker kind) — the
|
|
630
|
+
// human reacts to a concrete draft, strictly richer than a blank-question ask.
|
|
631
|
+
// NEVER write `work/backlog/<slug>.md`; NEVER silently emit the under-refined
|
|
632
|
+
// task. The next intake run resumes via the already-built triage gate.
|
|
633
|
+
note(`Intake's lone-task review did not converge for issue #${issueNumber} ` +
|
|
634
|
+
`(${review.passes} round(s)); flipping TASK→ASK with the draft + open ` +
|
|
635
|
+
`question(s) in the comment body.`);
|
|
636
|
+
return dispatchComment({
|
|
637
|
+
outcome: 'asked',
|
|
638
|
+
cwd,
|
|
639
|
+
issueNumber,
|
|
640
|
+
issueProvider,
|
|
641
|
+
body: composeLoneTaskAskComment({
|
|
642
|
+
issueNumber,
|
|
643
|
+
slug,
|
|
644
|
+
draftTitle: review.title,
|
|
645
|
+
draftBody: review.body,
|
|
646
|
+
questions: review.questions,
|
|
647
|
+
}),
|
|
648
|
+
markerKind: 'ask',
|
|
649
|
+
seen,
|
|
650
|
+
env,
|
|
651
|
+
note,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
// CONVERGED: the (possibly edited) task is emitted via the EXISTING write/integrate
|
|
655
|
+
// path below + the existing `task created` completion comment. The refined body
|
|
656
|
+
// replaces the agent's first draft.
|
|
657
|
+
const reviewedBody = review.body;
|
|
658
|
+
// ONBOARD the task write onto a `work/intake-task-<slug>` branch cut from the
|
|
659
|
+
// freshly-fetched `<arbiter>/main` (the SAME runner-owns-git discipline the
|
|
660
|
+
// tasking path uses): the lifecycle `stage` writes the file ON THIS BRANCH and
|
|
661
|
+
// the shared integrate core (`--propose` PR / `--merge` main) lands it. The
|
|
662
|
+
// intake- producer prefix keeps it distinct from a later `do task:<slug>`
|
|
663
|
+
// build branch for the same slug. The agent ran no git.
|
|
664
|
+
await switchToWorkBranch(cwd, arbiter, 'task', slug, env);
|
|
665
|
+
const taskContent = renderBacklogTask({
|
|
666
|
+
slug,
|
|
667
|
+
title: review.title,
|
|
668
|
+
body: reviewedBody,
|
|
669
|
+
issueNumber,
|
|
670
|
+
originTrust,
|
|
671
|
+
});
|
|
672
|
+
const core = await performIntegration({
|
|
673
|
+
cwd,
|
|
674
|
+
arbiter,
|
|
675
|
+
slug,
|
|
676
|
+
// `source`/`recovering` are task-shaped and IGNORED when `lifecycle` is set.
|
|
677
|
+
source: 'in-progress',
|
|
678
|
+
recovering: false,
|
|
679
|
+
// An intake-emitted task has no `verify` floor of its own (it is a new
|
|
680
|
+
// backlog item, not a build); skip the acceptance gate, exactly as the
|
|
681
|
+
// tasking transition does.
|
|
682
|
+
skipVerify: true,
|
|
683
|
+
// Default `propose` (the per-outcome KNOBS are a later task). The
|
|
684
|
+
// EXPLICITLY-chosen mode proceeds as-is: a future `--merge-task` lands on main
|
|
685
|
+
// (`merge` IS the auto-land mode, never downgraded).
|
|
686
|
+
mode: integration,
|
|
687
|
+
noPR,
|
|
688
|
+
providerInstance,
|
|
689
|
+
type: 'feat',
|
|
690
|
+
lifecycle: {
|
|
691
|
+
// The emitted task IS the title source. Pass the DRAFTED title EXPLICITLY
|
|
692
|
+
// (not a read-from-path): `stage()` WRITES `work/backlog/<slug>.md` AFTER the
|
|
693
|
+
// core reads the title, so a `titlePath` read would race the write and degrade
|
|
694
|
+
// the commit subject / PR title to the generic fallback. `titlePath` stays set
|
|
695
|
+
// (the lifecycle contract requires it) but is IGNORED while `title` is present.
|
|
696
|
+
titlePath: join(cwd, relPath),
|
|
697
|
+
title: review.title,
|
|
698
|
+
commitTag: 'intake',
|
|
699
|
+
stage: () => stageIntakeContent({ cwd, relPath, content: taskContent, env }),
|
|
700
|
+
},
|
|
701
|
+
env,
|
|
702
|
+
note,
|
|
703
|
+
});
|
|
704
|
+
return integrationToIntakeResult(core, {
|
|
705
|
+
issueNumber,
|
|
706
|
+
slug,
|
|
707
|
+
relPath,
|
|
708
|
+
cwd,
|
|
709
|
+
issueProvider,
|
|
710
|
+
seen,
|
|
711
|
+
env,
|
|
712
|
+
note,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* DISPATCH the `spec` outcome (canonical; the legacy `prd` outcome routes here too
|
|
717
|
+
* through the cutover): derive a content-derived slug, write the spec file
|
|
718
|
+
* (`work/specs/ready/<slug>.md`) carrying `issue: N` (the loop-closure linkage the
|
|
719
|
+
* close JOB reaches via `task.spec: → spec issue:`; on a fanned spec the number
|
|
720
|
+
* lives ONLY on the spec — a fanned task uses `spec:`, NOT its own `issue:`, which
|
|
721
|
+
* is the lone-task outcome's link) + the gate axes the prompt JUDGED, integrate it
|
|
722
|
+
* via {@link performIntegration}, then STOP. Tasking the emitted spec is the
|
|
723
|
+
* SEPARATE `do spec:` step (NOT done here). A coupled-but-SMALL pair lands here too
|
|
724
|
+
* (the spec vs BOUNCE line is SHARED VISION, not size — the over-bounce guard). The
|
|
725
|
+
* runner owns the git exactly as the task branch does; the agent did NO git/seam ops.
|
|
726
|
+
*/
|
|
727
|
+
async function dispatchSpec(params) {
|
|
728
|
+
const { verdict, issueNumber, cwd, arbiter, integration, originTrust, noPR, specsLandIn, explicitSpecsLandIn, providerInstance, issueProvider, seen, env, note, } = params;
|
|
729
|
+
// A content-derived slug — NEVER a counter (prd `issue-intake` US #8). Prefer the drafted
|
|
730
|
+
// `specSlug`, else derive from the drafted title.
|
|
731
|
+
const slug = resolveSpecSlug(verdict);
|
|
732
|
+
if (slug === '') {
|
|
733
|
+
const message = `Intake produced a 'spec' verdict for issue #${issueNumber} with no usable ` +
|
|
734
|
+
`slug/title to derive a content-derived slug from (never a counter).`;
|
|
735
|
+
note(message);
|
|
736
|
+
return { exitCode: 1, outcome: 'usage-error', issueNumber, message };
|
|
737
|
+
}
|
|
738
|
+
// RUNNER-DETERMINISTIC PLACEMENT (task
|
|
739
|
+
// `pre-prd-staging-pool-split-and-untrusted-prd-placement`, governing ADR
|
|
740
|
+
// `placement-is-runner-deterministic-humanonly-is-agent-judgement`). Resolve
|
|
741
|
+
// which folder the runner writes the intake-authored prd into BEFORE handing
|
|
742
|
+
// it to the shared integrate band: the SAME precedence chain the tasker uses
|
|
743
|
+
// (`explicit > untrusted-origin ⇒ staging > specsLandIn > built-in (staging)`),
|
|
744
|
+
// the SAME shared resolver — only the lifecycle SLOTS differ. The agent
|
|
745
|
+
// (the intake decider) never influences placement; it returns the verdict and
|
|
746
|
+
// the runner computes the destination from unforgeable inputs.
|
|
747
|
+
const placementDecision = resolvePlacement({
|
|
748
|
+
explicit: specLandingToSide(explicitSpecsLandIn),
|
|
749
|
+
originTrust,
|
|
750
|
+
configuredDefault: specLandingToSide(specsLandIn),
|
|
751
|
+
});
|
|
752
|
+
const placementDir = placementFolder(SPEC_PLACEMENT_SLOTS, placementDecision.choice);
|
|
753
|
+
const relPath = `${placementDir}/${slug}.md`;
|
|
754
|
+
// ONBOARD onto a `work/intake-spec-<slug>` branch off fresh `<arbiter>/main` —
|
|
755
|
+
// the SAME runner-owns-git discipline the task branch uses; the intake-
|
|
756
|
+
// producer prefix keeps it distinct from a `do spec:<slug>` tasking branch.
|
|
757
|
+
await switchToWorkBranch(cwd, arbiter, 'spec', slug, env);
|
|
758
|
+
const specContent = renderSpec({
|
|
759
|
+
slug,
|
|
760
|
+
title: verdict.specTitle ?? slug,
|
|
761
|
+
body: verdict.specBody,
|
|
762
|
+
issueNumber,
|
|
763
|
+
humanOnly: verdict.specHumanOnly,
|
|
764
|
+
needsAnswers: verdict.specNeedsAnswers,
|
|
765
|
+
originTrust,
|
|
766
|
+
});
|
|
767
|
+
const core = await performIntegration({
|
|
768
|
+
cwd,
|
|
769
|
+
arbiter,
|
|
770
|
+
slug,
|
|
771
|
+
source: 'in-progress',
|
|
772
|
+
recovering: false,
|
|
773
|
+
// An intake-emitted prd has no `verify` floor of its own (it is a new spec,
|
|
774
|
+
// not a build), exactly as the task branch + the tasking transition skip it.
|
|
775
|
+
skipVerify: true,
|
|
776
|
+
mode: integration,
|
|
777
|
+
noPR,
|
|
778
|
+
providerInstance,
|
|
779
|
+
type: 'feat',
|
|
780
|
+
lifecycle: {
|
|
781
|
+
// The emitted prd IS the title source. Pass the DRAFTED title EXPLICITLY (same
|
|
782
|
+
// race as the task path: `stage()` writes the prd file AFTER the title
|
|
783
|
+
// read). `titlePath` stays set but is IGNORED while `title` is present.
|
|
784
|
+
titlePath: join(cwd, relPath),
|
|
785
|
+
title: verdict.specTitle ?? slug,
|
|
786
|
+
commitTag: 'intake',
|
|
787
|
+
stage: () => stageIntakeContent({ cwd, relPath, content: specContent, env }),
|
|
788
|
+
},
|
|
789
|
+
env,
|
|
790
|
+
note,
|
|
791
|
+
});
|
|
792
|
+
return integrationToIntakeResult(core, {
|
|
793
|
+
issueNumber,
|
|
794
|
+
slug,
|
|
795
|
+
relPath,
|
|
796
|
+
kind: 'spec',
|
|
797
|
+
cwd,
|
|
798
|
+
issueProvider,
|
|
799
|
+
seen,
|
|
800
|
+
env,
|
|
801
|
+
note,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Map the shared integrate band's {@link IntegrationCoreResult} onto the intake
|
|
806
|
+
* {@link IntakeResult}. On `completed` the artifact was written + integrated; a
|
|
807
|
+
* `rebase-conflict` against an advanced `main` maps to `stale` (the analogue of
|
|
808
|
+
* "the backlog moved under us"); everything else maps defensively to a usage error
|
|
809
|
+
* (the intake task path passes `skipVerify` + has no review gate, so neither
|
|
810
|
+
* `gate-failed` nor `review-blocked` can occur).
|
|
811
|
+
*/
|
|
812
|
+
async function integrationToIntakeResult(core, ctx) {
|
|
813
|
+
const { issueNumber, slug, relPath, cwd, issueProvider, seen, env, note } = ctx;
|
|
814
|
+
const kind = ctx.kind ?? 'task';
|
|
815
|
+
const artifact = kind === 'spec' ? 'spec' : 'task';
|
|
816
|
+
if (core.outcome === 'completed') {
|
|
817
|
+
const landed = core.integration?.mode === 'merge'
|
|
818
|
+
? 'landed it on the arbiter main'
|
|
819
|
+
: 'opened a PR carrying it (main untouched)';
|
|
820
|
+
// Both a lone task and a prd carry `issue: N` as their closure link (the task
|
|
821
|
+
// closes its own issue; a prd is reached via `task.prd: → prd issue:`). On the
|
|
822
|
+
// task/prd path `intake` never closes the issue (CI's close-job does, via the
|
|
823
|
+
// `issue:` field; intake closes ONLY on BOUNCE) and emits no `Fixes #N` (a
|
|
824
|
+
// deferred GitHub-only optimisation).
|
|
825
|
+
const link = `issue: ${issueNumber}`;
|
|
826
|
+
const message = `Intake of issue #${issueNumber} → wrote ${relPath} (${link}); ` +
|
|
827
|
+
`the runner integrated it through the shared core and ${landed}.`;
|
|
828
|
+
// CLOSE THE LOOP (this task): post ONE INFORMATIONAL completion comment back on
|
|
829
|
+
// the issue for the SUCCESSFUL outcome — the confirmation the ASK/BOUNCE comments
|
|
830
|
+
// already give the author. It reports `task created` / `spec created` (NEVER
|
|
831
|
+
// "issue resolved"; intake never closes on task/spec — CI's close-job does, via
|
|
832
|
+
// the `issue:` field) and links the artifact by integration mode: the PR `url` in
|
|
833
|
+
// propose, the landed `commit` in merge. The marker carries `kind=created` (the
|
|
834
|
+
// TRIAGE treats it as TERMINAL → `already-terminal`), so the comment cannot
|
|
835
|
+
// re-trigger intake. ADVISORY — it DEGRADES (a missing/unauthenticated `gh` never
|
|
836
|
+
// throws), so a degrade leaves the run's success outcome unchanged.
|
|
837
|
+
const posted = await postCompletionComment({
|
|
838
|
+
issueProvider,
|
|
839
|
+
issueNumber,
|
|
840
|
+
kind,
|
|
841
|
+
slug,
|
|
842
|
+
integration: core.integration,
|
|
843
|
+
seen,
|
|
844
|
+
cwd,
|
|
845
|
+
env,
|
|
846
|
+
note,
|
|
847
|
+
});
|
|
848
|
+
return {
|
|
849
|
+
exitCode: 0,
|
|
850
|
+
outcome: kind === 'spec' ? 'spec-written' : 'tasked',
|
|
851
|
+
issueNumber,
|
|
852
|
+
emittedSlug: slug,
|
|
853
|
+
emitted: relPath,
|
|
854
|
+
commented: posted,
|
|
855
|
+
message,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
if (core.outcome === 'rebase-conflict') {
|
|
859
|
+
return {
|
|
860
|
+
exitCode: 4,
|
|
861
|
+
outcome: 'stale',
|
|
862
|
+
issueNumber,
|
|
863
|
+
message: core.reason ??
|
|
864
|
+
`Integrating the intake ${artifact} for issue #${issueNumber} conflicted ` +
|
|
865
|
+
`against the latest main; re-run intake.`,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
return {
|
|
869
|
+
exitCode: 1,
|
|
870
|
+
outcome: 'usage-error',
|
|
871
|
+
issueNumber,
|
|
872
|
+
message: core.reason ??
|
|
873
|
+
`Integrating the intake ${artifact} for issue #${issueNumber} failed unexpectedly.`,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Build the INFORMATIONAL completion-comment BODY (with its FULL `created` marker)
|
|
878
|
+
* for a SUCCESSFUL `task` / `spec` outcome — the PURE, seam-free core of
|
|
879
|
+
* {@link postCompletionComment}, exported so both link variants are unit-testable
|
|
880
|
+
* without a live seam. The comment:
|
|
881
|
+
*
|
|
882
|
+
* - reports `task created` / `spec created` framed as CREATED — NEVER "issue
|
|
883
|
+
* resolved/closed" (intake never closes on the task/spec path).
|
|
884
|
+
* - LINKS the artifact by INTEGRATION MODE: the PR `url` in propose, the landed
|
|
885
|
+
* `commit` (the additive {@link IntegrateResult.commit}) in merge. A degraded
|
|
886
|
+
* propose (no `url`) or a failed merge-tip read (no `commit`) simply OMITS the
|
|
887
|
+
* link — the comment still confirms what was created (the artifact is safe on the
|
|
888
|
+
* branch/main regardless). No prd link beyond the slug.
|
|
889
|
+
* - carries the FULL intake MARKER via the SHARED {@link stampIntakeMarker} helper
|
|
890
|
+
* (`kind=created slug=<slug> seen=<id>,…`) so the triage's `already-terminal`
|
|
891
|
+
* branch consumes it — the comment cannot re-trigger intake.
|
|
892
|
+
*/
|
|
893
|
+
export function composeIntakeCompletionComment(params) {
|
|
894
|
+
const { kind, slug, integration, seen } = params;
|
|
895
|
+
const artifact = kind === 'spec' ? 'spec' : 'task';
|
|
896
|
+
const link = integration?.mode === 'merge'
|
|
897
|
+
? integration.commit !== undefined
|
|
898
|
+
? `\n\nIt landed on \`main\` in commit ${integration.commit}.`
|
|
899
|
+
: ''
|
|
900
|
+
: integration?.url !== undefined
|
|
901
|
+
? `\n\nIt is carried by the PR: ${integration.url}`
|
|
902
|
+
: '';
|
|
903
|
+
const body = `Created ${artifact} \`${slug}\` from this issue.${link}\n\n` +
|
|
904
|
+
`This is an informational update — the issue stays open (it remains in play ` +
|
|
905
|
+
`until the ${artifact} lands; intake does not change the issue's state).`;
|
|
906
|
+
// STAMP the FULL marker (incl. `seen=`) via the SHARED helper, so the triage's
|
|
907
|
+
// `already-terminal` branch recognises this terminal `created` comment.
|
|
908
|
+
return stampIntakeMarker(body, { kind: 'created', seen, slug });
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Post the INFORMATIONAL completion comment for a SUCCESSFUL `task` / `spec`
|
|
912
|
+
* outcome (this task). It closes the loop the ASK/BOUNCE comments already close
|
|
913
|
+
* for the other outcomes: the issue author gets a confirmation when intake did the
|
|
914
|
+
* useful thing. The comment:
|
|
915
|
+
*
|
|
916
|
+
* - reports `task created` / `spec created` — NEVER "issue resolved/closed".
|
|
917
|
+
* Intake never closes the issue on the task/spec path (CI's future close-job
|
|
918
|
+
* does, via the `issue:` field); this comment changes NO issue state.
|
|
919
|
+
* - LINKS the artifact by INTEGRATION MODE: the PR `url` in propose, the landed
|
|
920
|
+
* `commit` (the additive {@link IntegrateResult.commit} this task surfaces) in
|
|
921
|
+
* merge. No prd link beyond the slug.
|
|
922
|
+
* - carries the FULL intake MARKER via the SHARED {@link stampIntakeMarker} helper
|
|
923
|
+
* (`kind=created slug=<slug> seen=<id>,…`). `kind=created` is TERMINAL, so the
|
|
924
|
+
* triage's `already-terminal` branch then treats the issue as already-transformed
|
|
925
|
+
* — the completion comment cannot re-trigger intake.
|
|
926
|
+
*
|
|
927
|
+
* ADVISORY — it DEGRADES (a missing/unauthenticated `gh` surfaces the text, never
|
|
928
|
+
* throws), so a degrade does NOT change the run's success outcome. Returns whether
|
|
929
|
+
* a comment was actually posted (for {@link IntakeResult.commented}).
|
|
930
|
+
*/
|
|
931
|
+
async function postCompletionComment(params) {
|
|
932
|
+
const { issueProvider, issueNumber, kind, slug, integration, seen, cwd, env, note, } = params;
|
|
933
|
+
const artifact = kind === 'spec' ? 'spec' : 'task';
|
|
934
|
+
// Build the full stamped body (CREATED wording + mode-keyed link + the FULL
|
|
935
|
+
// `created` marker) via the exported pure builder — unit-tested directly for both
|
|
936
|
+
// link variants (propose `url` / merge `commit`).
|
|
937
|
+
const stamped = composeIntakeCompletionComment({
|
|
938
|
+
kind,
|
|
939
|
+
slug,
|
|
940
|
+
integration,
|
|
941
|
+
seen,
|
|
942
|
+
});
|
|
943
|
+
const posted = await issueProvider.postIssueComment({
|
|
944
|
+
cwd,
|
|
945
|
+
issueNumber,
|
|
946
|
+
body: stamped,
|
|
947
|
+
env,
|
|
948
|
+
});
|
|
949
|
+
note(posted.posted
|
|
950
|
+
? `Posted a '${artifact} created' completion comment on issue #${issueNumber}.`
|
|
951
|
+
: `Could not post the completion comment on issue #${issueNumber} ` +
|
|
952
|
+
`(${posted.instruction}); the ${artifact} was still created.`);
|
|
953
|
+
return posted.posted;
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Resolve a content-derived slug from the verdict — NEVER a counter (prd `issue-intake` US #8).
|
|
957
|
+
* Prefer the drafted `taskSlug`, else derive from the drafted title; both go
|
|
958
|
+
* through `paramCase` (the brand case-transform) so the result is a clean
|
|
959
|
+
* lowercase-`-`-joined slug. An empty result (no slug AND no title) signals the
|
|
960
|
+
* caller to refuse (a counter fallback is forbidden).
|
|
961
|
+
*/
|
|
962
|
+
function resolveSlug(verdict) {
|
|
963
|
+
const candidate = verdict.taskSlug && verdict.taskSlug.trim() !== ''
|
|
964
|
+
? verdict.taskSlug
|
|
965
|
+
: (verdict.taskTitle ?? '');
|
|
966
|
+
return paramCase(candidate);
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Resolve a content-derived slug for the spec outcome — NEVER a counter (prd `issue-intake` US #8).
|
|
970
|
+
* Prefer the drafted `specSlug`, else derive from the drafted spec title; both go
|
|
971
|
+
* through `paramCase`. An empty result signals the caller to refuse.
|
|
972
|
+
*/
|
|
973
|
+
function resolveSpecSlug(verdict) {
|
|
974
|
+
const candidate = verdict.specSlug && verdict.specSlug.trim() !== ''
|
|
975
|
+
? verdict.specSlug
|
|
976
|
+
: (verdict.specTitle ?? '');
|
|
977
|
+
return paramCase(candidate);
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Render the backlog task file: the frontmatter (`title`/`slug`/`covers: []`, NO
|
|
981
|
+
* `prd:` — its own source of truth, prd `issue-intake` decision table) carrying the lone-task
|
|
982
|
+
* `issue: N` closure link + the drafted body. The task closes its source issue
|
|
983
|
+
* via its `issue:` field (the provider-agnostic link a FUTURE CI close-job reads
|
|
984
|
+
* from folder + field state); it carries NO `Fixes #N` (a deferred GitHub-only
|
|
985
|
+
* optimisation, structurally unplaceable on the `--merge` path). The number is
|
|
986
|
+
* the task's own closure path — `issue:` XOR `prd:`; a lone task never carries a
|
|
987
|
+
* `prd:` (prd `issue-intake` decision table). When the agent drafted no body, a thin default
|
|
988
|
+
* scaffold keeps the file a valid task.
|
|
989
|
+
*/
|
|
990
|
+
export function renderBacklogTask(params) {
|
|
991
|
+
const { slug, title, body, issueNumber, originTrust } = params;
|
|
992
|
+
const lines = [
|
|
993
|
+
'---',
|
|
994
|
+
`title: ${title}`,
|
|
995
|
+
`slug: ${slug}`,
|
|
996
|
+
`issue: ${issueNumber}`,
|
|
997
|
+
];
|
|
998
|
+
if (originTrust !== undefined) {
|
|
999
|
+
lines.push('origin: issue', `originTrust: ${originTrust}`);
|
|
1000
|
+
}
|
|
1001
|
+
lines.push('covers: []', 'blockedBy: []', '---');
|
|
1002
|
+
const frontmatter = lines.join('\n');
|
|
1003
|
+
// The drafted body (agent-authored, headings and all) is wrapped VERBATIM.
|
|
1004
|
+
// Only the empty-body DEFAULT SCAFFOLD is sourced from the shared section
|
|
1005
|
+
// skeleton owner (`renderTaskBody`, prd
|
|
1006
|
+
// `centralize-buildable-task-renderer-shared-by-intake-and-promotion` US #2),
|
|
1007
|
+
// so intake's fallback and promotion's body cannot drift on section
|
|
1008
|
+
// names/order. The shared renderer ends its body with a trailing newline (its
|
|
1009
|
+
// last line is a blank); intake owns the single trailing `\n` in the join
|
|
1010
|
+
// below, so we `trimEnd()` the renderer output to stay byte-for-byte identical
|
|
1011
|
+
// to the pre-rewire literal.
|
|
1012
|
+
const drafted = body && body.trim() !== ''
|
|
1013
|
+
? body.trim()
|
|
1014
|
+
: renderTaskBody({
|
|
1015
|
+
whatToBuild: title,
|
|
1016
|
+
acceptanceCriteria: '- [ ] the issue is resolved',
|
|
1017
|
+
prompt: `Resolve issue #${issueNumber}: ${title}`,
|
|
1018
|
+
}).trimEnd();
|
|
1019
|
+
return `${frontmatter}\n\n${drafted}\n`;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Render the emitted prd file: the frontmatter (`title`/`slug` + the loop-closure
|
|
1023
|
+
* `issue: N` + the gate axes the prompt JUDGED) followed by the drafted prd body.
|
|
1024
|
+
* For a FANNED prd the `issue: N` lives ONLY on the prd — never duplicated across
|
|
1025
|
+
* the N fanned tasks, which reach it via `task.prd: → prd issue:` (a fanned
|
|
1026
|
+
* task carries `prd:`, NOT its own `issue:`; the lone-task outcome is the only
|
|
1027
|
+
* one that puts `issue:` on a task). The close JOB reaches the prd's number via
|
|
1028
|
+
* `task.prd: → prd issue:`. The gate axes (`humanOnly`/`needsAnswers`) are emitted ONLY when the
|
|
1029
|
+
* verdict declared them `true` — an omitted axis is `undefined` (undeclared), the
|
|
1030
|
+
* same convention `frontmatter.ts` parses. When the agent drafted no body, a thin
|
|
1031
|
+
* default scaffold keeps the file a valid prd that `do prd:` can later task.
|
|
1032
|
+
*/
|
|
1033
|
+
export function renderSpec(params) {
|
|
1034
|
+
const { slug, title, body, issueNumber, humanOnly, needsAnswers, originTrust } = params;
|
|
1035
|
+
const lines = [
|
|
1036
|
+
'---',
|
|
1037
|
+
`title: ${title}`,
|
|
1038
|
+
`slug: ${slug}`,
|
|
1039
|
+
`issue: ${issueNumber}`,
|
|
1040
|
+
];
|
|
1041
|
+
// The origin-trust stamp (only when passed IN from the CI shell): the
|
|
1042
|
+
// becomes-code checkpoint that survives the merge boundary. A local intake
|
|
1043
|
+
// leaves it unset ⇒ no stamp ⇒ human/trusted.
|
|
1044
|
+
if (originTrust !== undefined) {
|
|
1045
|
+
lines.push('origin: issue', `originTrust: ${originTrust}`);
|
|
1046
|
+
}
|
|
1047
|
+
// Surface the gate axes AS THE PROMPT JUDGED THEM (prd `issue-intake` US #8). Only emit a `true`
|
|
1048
|
+
// axis — an undeclared axis stays absent (parsed as `undefined`).
|
|
1049
|
+
if (humanOnly === true) {
|
|
1050
|
+
lines.push('humanOnly: true');
|
|
1051
|
+
}
|
|
1052
|
+
if (needsAnswers === true) {
|
|
1053
|
+
lines.push('needsAnswers: true');
|
|
1054
|
+
}
|
|
1055
|
+
lines.push('---');
|
|
1056
|
+
const frontmatter = lines.join('\n');
|
|
1057
|
+
// As with `renderBacklogTask`: the drafted PRD body is wrapped VERBATIM; only
|
|
1058
|
+
// the empty-body DEFAULT SCAFFOLD is sourced from the shared section skeleton
|
|
1059
|
+
// owner (`renderSpecBody` with `solution` + `userStories`, prd
|
|
1060
|
+
// `centralize-buildable-task-renderer-shared-by-intake-and-promotion` US #2),
|
|
1061
|
+
// so intake's PRD fallback and promotion's PRD body cannot drift. `trimEnd()`
|
|
1062
|
+
// drops the renderer's trailing blank line so intake's single trailing `\n`
|
|
1063
|
+
// (owned by the join below) keeps the bytes identical to the pre-rewire literal.
|
|
1064
|
+
const drafted = body && body.trim() !== ''
|
|
1065
|
+
? body.trim()
|
|
1066
|
+
: renderSpecBody({
|
|
1067
|
+
problemStatement: `Transformed from issue #${issueNumber}: ${title}`,
|
|
1068
|
+
solution: '(to be detailed; this prd needs tasking via `do prd:`).',
|
|
1069
|
+
userStories: `1. As a user, I want issue #${issueNumber} addressed.`,
|
|
1070
|
+
}).trimEnd();
|
|
1071
|
+
return `${frontmatter}\n\n${drafted}\n`;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* STAGE the intake artifact content into the index on the `work/<slug>` branch (the
|
|
1075
|
+
* {@link performIntegration} lifecycle seam): write the `work/backlog/<slug>.md`
|
|
1076
|
+
* file (runner-owned; the agent never writes git-visible files) and `git add` it.
|
|
1077
|
+
* The band's subsequent `git add -A` + atomic commit folds it into ONE runner-owned
|
|
1078
|
+
* commit.
|
|
1079
|
+
*/
|
|
1080
|
+
async function stageIntakeContent(params) {
|
|
1081
|
+
const { cwd, relPath, content, env } = params;
|
|
1082
|
+
const abs = join(cwd, relPath);
|
|
1083
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
1084
|
+
writeFileSync(abs, content);
|
|
1085
|
+
await gitHard(['add', '--', relPath], cwd, env);
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* ONBOARD the intake write onto a NAMESPACED, INTAKE-PRODUCED branch
|
|
1089
|
+
* (`work/intake-task-<slug>` / `work/intake-prd-<slug>`) cut from the freshly-
|
|
1090
|
+
* fetched `<arbiter>/main` (the SAME discipline `tasking.ts` uses). The
|
|
1091
|
+
* `intake-` PRODUCER prefix keeps this short-lived "create the item" branch
|
|
1092
|
+
* DISTINCT from the later build branch (`work/task-<slug>`) for the same slug
|
|
1093
|
+
* — the firing `intake` × `do task:` collision the observation traced. The
|
|
1094
|
+
* task-emit path passes `'task'`, the prd-emit path `'prd'`. A pre-existing
|
|
1095
|
+
* local branch (a re-run) is force-recreated off fresh main.
|
|
1096
|
+
*/
|
|
1097
|
+
async function switchToWorkBranch(cwd, arbiter, type, slug, env) {
|
|
1098
|
+
const branch = workBranchRef(type, slug, { producer: 'intake' });
|
|
1099
|
+
await gitHard(['fetch', '--quiet', arbiter], cwd, env);
|
|
1100
|
+
await gitHard(['switch', '--quiet', '-C', branch, `${arbiter}/main`], cwd, env);
|
|
1101
|
+
}
|
|
1102
|
+
/** Run git; throw on non-zero (genuinely unexpected plumbing failures). */
|
|
1103
|
+
async function gitHard(args, cwd, env) {
|
|
1104
|
+
const result = await runAsync('git', args, cwd, { env });
|
|
1105
|
+
if (result.status !== 0) {
|
|
1106
|
+
throw new Error(`git ${args.join(' ')} failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
1107
|
+
}
|
|
1108
|
+
return result;
|
|
1109
|
+
}
|
|
1110
|
+
/** Run the decision step. Prefers the injected decider; else the harness seam. */
|
|
1111
|
+
async function runDecision(options, cwd, issue, comments, prompt) {
|
|
1112
|
+
if (options.decide) {
|
|
1113
|
+
return options.decide({ cwd, issue, comments, prompt, env: options.env });
|
|
1114
|
+
}
|
|
1115
|
+
// PRODUCTION: launch the harness with the decision prd, then PARSE the verdict
|
|
1116
|
+
// the agent emitted out of its ANSWER channel (`launched.output`) — the SAME wire
|
|
1117
|
+
// the review gate runs (launch → `parseReviewVerdict(readOutput(launched.output))`;
|
|
1118
|
+
// `harnessReviewGate`). The agent emits a single fenced ```json block (the OUTPUT
|
|
1119
|
+
// CONTRACT {@link buildIntakeDecisionSpec} appends); {@link parseIntakeVerdict}
|
|
1120
|
+
// extracts + validates it. The model's JUDGEMENT is not unit-tested — only the
|
|
1121
|
+
// parse + dispatch — exactly as the review prompt's judgement is not.
|
|
1122
|
+
const harness = options.harness ?? new NullHarness();
|
|
1123
|
+
const launched = await launchWithOptionalWatch({
|
|
1124
|
+
harness,
|
|
1125
|
+
dir: cwd,
|
|
1126
|
+
slug: `intake-${issue.number}`,
|
|
1127
|
+
command: options.agentCmd ?? '',
|
|
1128
|
+
prompt,
|
|
1129
|
+
model: options.model,
|
|
1130
|
+
sessionId: `intake-${issue.number}`,
|
|
1131
|
+
sessionsDir: options.sessionsDir,
|
|
1132
|
+
env: options.env,
|
|
1133
|
+
});
|
|
1134
|
+
if (!launched.ok) {
|
|
1135
|
+
throw new Error(launched.detail ?? 'the intake decision agent failed.');
|
|
1136
|
+
}
|
|
1137
|
+
// Read the verdict from the agent's ANSWER channel (`output`), NOT `detail` (the
|
|
1138
|
+
// failure channel, empty on success) — the SAME `output ?? ''` normalisation the
|
|
1139
|
+
// review gate's `readOutput` default applies. A malformed/absent verdict throws,
|
|
1140
|
+
// which `decideAndDispatch`'s try/catch already maps onto `agent-failed` (exit 1).
|
|
1141
|
+
return parseIntakeVerdict(launched.output ?? '');
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Parse the decision agent's emitted VERDICT out of its (possibly prose-wrapped /
|
|
1145
|
+
* fenced) textual output into an {@link IntakeVerdict} — the PRODUCTION wire
|
|
1146
|
+
* between the launched agent and the already-built dispatcher, modeled 1:1 on the
|
|
1147
|
+
* review gate's `parseReviewVerdict` twin (`review-gate.ts`). It pulls the first
|
|
1148
|
+
* JSON object carrying an `"outcome"` field via the SHARED
|
|
1149
|
+
* {@link extractJsonObjectSpan} (NOT a forked second "first JSON object in agent
|
|
1150
|
+
* prose" extractor — the review gates anchor on `"verdict"`, intake on
|
|
1151
|
+
* `"outcome"`; same need, one implementation — coherence), `JSON.parse`s it, and
|
|
1152
|
+
* validates the shape: `outcome ∈ {ask,task,spec,bounce}`.
|
|
1153
|
+
*
|
|
1154
|
+
* The per-outcome fields map 1:1 onto {@link IntakeVerdict} (`task` →
|
|
1155
|
+
* taskSlug?/taskTitle/taskBody, `spec` →
|
|
1156
|
+
* specSlug?/specTitle/specBody/specHumanOnly?/specNeedsAnswers?, `ask` → question,
|
|
1157
|
+
* `bounce` → bounceMessage). Missing OPTIONALS are tolerated — the dispatcher
|
|
1158
|
+
* already has fallbacks (slug-from-title, the thin comment/scaffold defaults).
|
|
1159
|
+
*
|
|
1160
|
+
* THROWS a clear error on: no JSON object present, invalid JSON, or an `outcome`
|
|
1161
|
+
* not in the set. The caller (`decideAndDispatch`) maps any throw onto the
|
|
1162
|
+
* `agent-failed` outcome (exit 1) — a malformed verdict degrades honestly, never
|
|
1163
|
+
* a crash and never a silent dispatch.
|
|
1164
|
+
*/
|
|
1165
|
+
export function parseIntakeVerdict(output) {
|
|
1166
|
+
const span = extractJsonObjectSpan(output, 'outcome');
|
|
1167
|
+
if (span === undefined) {
|
|
1168
|
+
throw new Error('intake decision agent produced no parseable {outcome, …} verdict.');
|
|
1169
|
+
}
|
|
1170
|
+
let parsed;
|
|
1171
|
+
try {
|
|
1172
|
+
parsed = JSON.parse(output.slice(span.start, span.end));
|
|
1173
|
+
}
|
|
1174
|
+
catch (err) {
|
|
1175
|
+
throw new Error(`intake verdict was not valid JSON: ${err.message}`);
|
|
1176
|
+
}
|
|
1177
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
1178
|
+
throw new Error('intake verdict was not an object.');
|
|
1179
|
+
}
|
|
1180
|
+
const obj = parsed;
|
|
1181
|
+
const outcome = obj.outcome;
|
|
1182
|
+
if (outcome !== 'ask' &&
|
|
1183
|
+
outcome !== 'task' &&
|
|
1184
|
+
outcome !== 'spec' &&
|
|
1185
|
+
outcome !== 'bounce') {
|
|
1186
|
+
// The prompt teaches the LLM to emit `spec`, so the accepted outcome set is
|
|
1187
|
+
// `ask|task|spec|bounce`. HARD CUTOVER: the legacy `prd` outcome token is
|
|
1188
|
+
// fully gone (rejected here + removed from the `IntakeOutcome` type + the
|
|
1189
|
+
// dispatch `case`).
|
|
1190
|
+
throw new Error(`intake verdict 'outcome' was not one of ask|task|spec|bounce (got ` +
|
|
1191
|
+
`${JSON.stringify(outcome)}).`);
|
|
1192
|
+
}
|
|
1193
|
+
// Map the per-outcome fields onto the verdict shape, keeping ONLY the strings/
|
|
1194
|
+
// booleans the dispatcher consumes (a missing optional stays absent — the
|
|
1195
|
+
// dispatcher's fallbacks cover it). Every field is optional on the type, so the
|
|
1196
|
+
// `task`/`spec` content + the `ask`/`bounce` text are carried verbatim when present.
|
|
1197
|
+
const str = (v) => typeof v === 'string' ? v : undefined;
|
|
1198
|
+
const bool = (v) => typeof v === 'boolean' ? v : undefined;
|
|
1199
|
+
return {
|
|
1200
|
+
outcome,
|
|
1201
|
+
...(str(obj.taskSlug) !== undefined ? { taskSlug: str(obj.taskSlug) } : {}),
|
|
1202
|
+
...(str(obj.taskTitle) !== undefined
|
|
1203
|
+
? { taskTitle: str(obj.taskTitle) }
|
|
1204
|
+
: {}),
|
|
1205
|
+
...(str(obj.taskBody) !== undefined ? { taskBody: str(obj.taskBody) } : {}),
|
|
1206
|
+
...(str(obj.question) !== undefined ? { question: str(obj.question) } : {}),
|
|
1207
|
+
...(str(obj.specSlug) !== undefined ? { specSlug: str(obj.specSlug) } : {}),
|
|
1208
|
+
...(str(obj.specTitle) !== undefined
|
|
1209
|
+
? { specTitle: str(obj.specTitle) }
|
|
1210
|
+
: {}),
|
|
1211
|
+
...(str(obj.specBody) !== undefined ? { specBody: str(obj.specBody) } : {}),
|
|
1212
|
+
...(bool(obj.specHumanOnly) !== undefined
|
|
1213
|
+
? { specHumanOnly: bool(obj.specHumanOnly) }
|
|
1214
|
+
: {}),
|
|
1215
|
+
...(bool(obj.specNeedsAnswers) !== undefined
|
|
1216
|
+
? { specNeedsAnswers: bool(obj.specNeedsAnswers) }
|
|
1217
|
+
: {}),
|
|
1218
|
+
...(str(obj.bounceMessage) !== undefined
|
|
1219
|
+
? { bounceMessage: str(obj.bounceMessage) }
|
|
1220
|
+
: {}),
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
// ---------------------------------------------------------------------------
|
|
1224
|
+
// The LONE-TASK bounded internal review (observation
|
|
1225
|
+
// `intake-lone-task-skips-adversarial-review-the-prd-path-gets`, rulings A/B/C).
|
|
1226
|
+
//
|
|
1227
|
+
// Give intake's lone-TASK outcome the adversarial refinement the `do prd:` path
|
|
1228
|
+
// already gets — but as a small intake-NATIVE bounded review, NOT by integrating
|
|
1229
|
+
// the tasker loop. This is a NEW prompt + a small loop + an injectable gate seam,
|
|
1230
|
+
// MIRRORING the tasker loop's verdict/output CONVENTIONS (fenced JSON
|
|
1231
|
+
// `{verdict, findings, edit}` parsed via the shared `extractJsonObjectSpan`) WITHOUT
|
|
1232
|
+
// importing or calling `runTaskReviewLoop`. The differences are load-bearing: this
|
|
1233
|
+
// reviews ONE drafted task (N=1 — the SET/graph/overlap lenses are OFF), it never
|
|
1234
|
+
// touches disk pre-convergence (the task has not been emitted yet), and its only
|
|
1235
|
+
// non-converge sink is the EXISTING `asked` outcome (verdict flips TASK→ASK with
|
|
1236
|
+
// the draft + question(s) in the comment body — ruling C).
|
|
1237
|
+
// ---------------------------------------------------------------------------
|
|
1238
|
+
/** The HARD-CODED round cap for the lone-task review (ruling A — a literal, no config/flag). */
|
|
1239
|
+
const LONE_TASK_REVIEW_MAX_ROUNDS = 3;
|
|
1240
|
+
/**
|
|
1241
|
+
* Run the BOUNDED lone-task adversarial self-review over the SINGLE drafted task.
|
|
1242
|
+
* Each round runs the gate (the `review` skill's per-task + destination lenses on
|
|
1243
|
+
* the ONE task); a round may propose an EDIT (full replacement body) applied IN
|
|
1244
|
+
* MEMORY and re-reviewed. The cap is the HARD-CODED literal
|
|
1245
|
+
* {@link LONE_TASK_REVIEW_MAX_ROUNDS} = 3 (ruling A — no config/flag).
|
|
1246
|
+
*
|
|
1247
|
+
* - CONVERGE — a round returns `approve` with no NEW blocking issue → emit the
|
|
1248
|
+
* improved task (the caller's existing write/integrate + completion comment).
|
|
1249
|
+
* - NON-CONVERGE — a round `block`s with an open question (no clear thread answer)
|
|
1250
|
+
* OR the cap is hit with an unresolved blocker → flip TASK→ASK carrying the
|
|
1251
|
+
* draft + the open question(s) (ruling C).
|
|
1252
|
+
*
|
|
1253
|
+
* A gate launch/parse failure THROWS (the caller's try/catch maps it onto
|
|
1254
|
+
* `agent-failed`) — never a silent emit of the un-reviewed task.
|
|
1255
|
+
*/
|
|
1256
|
+
async function runLoneTaskReview(params) {
|
|
1257
|
+
const { slug, issueNumber, draftTitle, draftBody, gate, cwd, env, note } = params;
|
|
1258
|
+
let body = draftBody;
|
|
1259
|
+
let lastVerdict = { verdict: 'block', findings: [] };
|
|
1260
|
+
let passes = 0;
|
|
1261
|
+
for (let round = 1; round <= LONE_TASK_REVIEW_MAX_ROUNDS; round++) {
|
|
1262
|
+
const verdict = await gate({
|
|
1263
|
+
slug,
|
|
1264
|
+
issueNumber,
|
|
1265
|
+
title: draftTitle,
|
|
1266
|
+
// The body the reviewer sees this round (after any prior in-memory edit);
|
|
1267
|
+
// fall back to the rendered scaffold-input the emit path also tolerates.
|
|
1268
|
+
body: body ?? '',
|
|
1269
|
+
round,
|
|
1270
|
+
cwd,
|
|
1271
|
+
env,
|
|
1272
|
+
});
|
|
1273
|
+
passes = round;
|
|
1274
|
+
lastVerdict = verdict;
|
|
1275
|
+
// APPLY the proposed EDIT IN MEMORY (no `work/backlog/` write pre-convergence).
|
|
1276
|
+
if (verdict.edit !== undefined && verdict.edit.trim() !== '') {
|
|
1277
|
+
body = verdict.edit;
|
|
1278
|
+
}
|
|
1279
|
+
if (verdict.verdict === 'approve') {
|
|
1280
|
+
return {
|
|
1281
|
+
outcome: 'converge',
|
|
1282
|
+
title: draftTitle,
|
|
1283
|
+
body,
|
|
1284
|
+
questions: [],
|
|
1285
|
+
passes,
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
// EARLY FLIP → ASK (the non-converge trigger the source observation names
|
|
1289
|
+
// FIRST: "a blocking question with NO clear answer in the issue thread"). When a
|
|
1290
|
+
// round BLOCKS, carries open `questions`, and proposes NO `edit`, the agent is
|
|
1291
|
+
// saying "this needs the HUMAN, I have nothing left to tighten" — so flip to ASK
|
|
1292
|
+
// NOW rather than burning the remaining rounds (which cannot resolve a question
|
|
1293
|
+
// only the human can answer). Symmetric with the early CONVERGE return above; it
|
|
1294
|
+
// retires the "flips only at the cap" behaviour (PR #62 review nit #1). A `block`
|
|
1295
|
+
// that DID propose an `edit` is still iterated — the edit may converge it; only a
|
|
1296
|
+
// no-edit blocking question short-circuits.
|
|
1297
|
+
const hasEdit = verdict.edit !== undefined && verdict.edit.trim() !== '';
|
|
1298
|
+
const earlyQuestions = loneTaskBlockingQuestions(verdict);
|
|
1299
|
+
if (!hasEdit && earlyQuestions.length > 0) {
|
|
1300
|
+
note(`Intake lone-task review round ${round}/${LONE_TASK_REVIEW_MAX_ROUNDS} ` +
|
|
1301
|
+
`surfaced a blocking question with no edit to apply; flipping TASK→ASK ` +
|
|
1302
|
+
`early (no clear thread answer — the human must decide).`);
|
|
1303
|
+
return {
|
|
1304
|
+
outcome: 'non-converge',
|
|
1305
|
+
title: draftTitle,
|
|
1306
|
+
body,
|
|
1307
|
+
questions: earlyQuestions,
|
|
1308
|
+
passes,
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
note(`Intake lone-task review round ${round}/${LONE_TASK_REVIEW_MAX_ROUNDS} ` +
|
|
1312
|
+
`found ${loneTaskBlockingCount(verdict)} blocking issue(s)` +
|
|
1313
|
+
`${hasEdit ? ' (an edit was applied)' : ''}.`);
|
|
1314
|
+
}
|
|
1315
|
+
// The cap was hit with a still-`block` verdict → NON-CONVERGE (flip TASK→ASK).
|
|
1316
|
+
return {
|
|
1317
|
+
outcome: 'non-converge',
|
|
1318
|
+
title: draftTitle,
|
|
1319
|
+
body,
|
|
1320
|
+
questions: loneTaskBlockingQuestions(lastVerdict),
|
|
1321
|
+
passes,
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
/** Count blocking findings in a lone-task review verdict. */
|
|
1325
|
+
function loneTaskBlockingCount(verdict) {
|
|
1326
|
+
return verdict.findings.filter((f) => f.severity === 'blocking').length;
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* The open question(s) for the ASK comment body on a non-converge: prefer the
|
|
1330
|
+
* verdict's explicit `questions`, else fall back to the blocking findings'
|
|
1331
|
+
* questions (so the human always gets a concrete question, never a blank ask).
|
|
1332
|
+
*/
|
|
1333
|
+
function loneTaskBlockingQuestions(verdict) {
|
|
1334
|
+
if (verdict.questions && verdict.questions.length > 0) {
|
|
1335
|
+
return verdict.questions;
|
|
1336
|
+
}
|
|
1337
|
+
const blocking = verdict.findings.filter((f) => f.severity === 'blocking');
|
|
1338
|
+
const source = blocking.length > 0 ? blocking : verdict.findings;
|
|
1339
|
+
return source.map((f) => f.context ? `${f.question} (${f.context})` : f.question);
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Compose the NON-CONVERGE ASK comment BODY (ruling C): it carries BOTH the proposed
|
|
1343
|
+
* task DRAFT and the open question(s) that arose, so the human reacts to a concrete
|
|
1344
|
+
* draft ("yes, yes, but…"), strictly richer than a blank-question ask. The draft
|
|
1345
|
+
* rides in the comment BODY — NOT a new marker kind; {@link dispatchComment} stamps
|
|
1346
|
+
* the EXISTING `kind=ask` marker around it.
|
|
1347
|
+
*/
|
|
1348
|
+
function composeLoneTaskAskComment(params) {
|
|
1349
|
+
const { issueNumber, slug, draftTitle, draftBody, questions } = params;
|
|
1350
|
+
const draft = renderBacklogTask({
|
|
1351
|
+
slug,
|
|
1352
|
+
title: draftTitle,
|
|
1353
|
+
body: draftBody,
|
|
1354
|
+
issueNumber,
|
|
1355
|
+
});
|
|
1356
|
+
const questionLines = questions.length > 0
|
|
1357
|
+
? questions.map((q) => `- ${q}`).join('\n')
|
|
1358
|
+
: '- (the draft below needs a clarification before it can be built)';
|
|
1359
|
+
return [
|
|
1360
|
+
`I drafted a task for issue #${issueNumber} but the internal review surfaced`,
|
|
1361
|
+
`open question(s) it could not resolve from the thread. Please weigh in on the`,
|
|
1362
|
+
`draft below — once the question(s) are answered, a later run can emit it.`,
|
|
1363
|
+
'',
|
|
1364
|
+
'## Open question(s)',
|
|
1365
|
+
'',
|
|
1366
|
+
questionLines,
|
|
1367
|
+
'',
|
|
1368
|
+
'## Proposed task draft',
|
|
1369
|
+
'',
|
|
1370
|
+
'```markdown',
|
|
1371
|
+
draft.trimEnd(),
|
|
1372
|
+
'```',
|
|
1373
|
+
].join('\n');
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Resolve the lone-task review GATE from the intake options: the injected
|
|
1377
|
+
* {@link PerformIntakeOptions.reviewTask} (tests' canned seam) when present, else
|
|
1378
|
+
* the production harness-backed gate ({@link harnessLoneTaskReviewGate}) wired to
|
|
1379
|
+
* the same harness/agent the decision step uses. Mirrors how {@link runDecision}
|
|
1380
|
+
* prefers the injected `decide`.
|
|
1381
|
+
*/
|
|
1382
|
+
function resolveLoneTaskReviewGate(options) {
|
|
1383
|
+
if (options.reviewTask) {
|
|
1384
|
+
return options.reviewTask;
|
|
1385
|
+
}
|
|
1386
|
+
return harnessLoneTaskReviewGate({
|
|
1387
|
+
harness: options.harness,
|
|
1388
|
+
agentCmd: options.agentCmd,
|
|
1389
|
+
model: options.model,
|
|
1390
|
+
sessionsDir: options.sessionsDir,
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* The PRODUCTION lone-task review gate: launch the `review` SKILL as an agent
|
|
1395
|
+
* through the EXISTING harness seam (the SAME wire {@link runDecision} uses), then
|
|
1396
|
+
* PARSE the emitted `{verdict, findings, edit, questions}` via
|
|
1397
|
+
* {@link parseLoneTaskReviewVerdict}. The agent makes the review JUDGEMENT (the
|
|
1398
|
+
* per-task + destination lenses on the ONE task); this gate launches it and parses
|
|
1399
|
+
* its verdict. A launch failure THROWS (the dispatcher's try/catch maps it onto
|
|
1400
|
+
* `agent-failed`). MIRRORS {@link harnessTaskReviewGate} WITHOUT importing it.
|
|
1401
|
+
*/
|
|
1402
|
+
export function harnessLoneTaskReviewGate(options = {}) {
|
|
1403
|
+
const harness = options.harness ?? new NullHarness();
|
|
1404
|
+
return async (input) => {
|
|
1405
|
+
const launched = await launchWithOptionalWatch({
|
|
1406
|
+
harness,
|
|
1407
|
+
dir: input.cwd,
|
|
1408
|
+
slug: `intake-task-review-${input.slug}`,
|
|
1409
|
+
command: options.agentCmd ?? '',
|
|
1410
|
+
prompt: buildLoneTaskReviewPrompt(input),
|
|
1411
|
+
model: options.model,
|
|
1412
|
+
// A DISTINCT session id per round so launches never collide.
|
|
1413
|
+
sessionId: `intake-task-review-${input.slug}-r${input.round}`,
|
|
1414
|
+
sessionsDir: options.sessionsDir,
|
|
1415
|
+
env: input.env,
|
|
1416
|
+
});
|
|
1417
|
+
if (!launched.ok) {
|
|
1418
|
+
throw new Error(`intake lone-task review agent launch failed${launched.detail ? `: ${launched.detail}` : ''}`);
|
|
1419
|
+
}
|
|
1420
|
+
return parseReviewVerdict(launched.output ?? '');
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Build the LONE-TASK review PROMPT: instruct a fresh-context agent to apply
|
|
1425
|
+
* the **review discipline** (`work/protocol/REVIEW-PROTOCOL.md`) to the SINGLE
|
|
1426
|
+
* drafted task — per-task well-formedness + the destination check ("if this
|
|
1427
|
+
* task is built exactly as written, do we end up with the behaviour issue #N
|
|
1428
|
+
* asks for?"). The SET / graph / overlap lenses are N=1 and EXPLICITLY OFF.
|
|
1429
|
+
* A round may propose an `edit` (the FULL replacement task body) the runner
|
|
1430
|
+
* applies IN MEMORY and re-reviews; converge when a round finds NO new blocking
|
|
1431
|
+
* issue, else carry the open `questions` into the ASK comment for the human.
|
|
1432
|
+
*
|
|
1433
|
+
* The discipline body and the JSON-emitted-shape contract are SHARED helpers
|
|
1434
|
+
* (task `review-protocol-doc-and-shared-machinery`); this builder owns ONLY
|
|
1435
|
+
* the lone-task-specific framing.
|
|
1436
|
+
*/
|
|
1437
|
+
export function buildLoneTaskReviewPrompt(input) {
|
|
1438
|
+
return [
|
|
1439
|
+
`You are a FRESH-CONTEXT reviewer in intake's BOUNDED lone-task review. A`,
|
|
1440
|
+
`single task has just been drafted from GitHub issue #${input.issueNumber}.`,
|
|
1441
|
+
`Review THIS ONE task adversarially (round ${input.round} of at most ${LONE_TASK_REVIEW_MAX_ROUNDS}).`,
|
|
1442
|
+
'',
|
|
1443
|
+
reviewDisciplinePrompt(),
|
|
1444
|
+
'',
|
|
1445
|
+
`Drafted task slug: ${input.slug}`,
|
|
1446
|
+
`Drafted task title: ${input.title}`,
|
|
1447
|
+
'',
|
|
1448
|
+
'Drafted task body (the markdown AFTER the frontmatter):',
|
|
1449
|
+
'```markdown',
|
|
1450
|
+
input.body.trim() === ''
|
|
1451
|
+
? '(empty — only a scaffold was drafted)'
|
|
1452
|
+
: input.body,
|
|
1453
|
+
'```',
|
|
1454
|
+
'',
|
|
1455
|
+
'## Which lenses apply (N=1 — this is ONE task, not a SET)',
|
|
1456
|
+
'',
|
|
1457
|
+
'Apply ONLY the per-task lenses, ENDING in the destination check:',
|
|
1458
|
+
'- **Per-task well-formedness** — is it a single tracer-bullet vertical task',
|
|
1459
|
+
' (one thin end-to-end path)? Are the `## What to build`, `## Acceptance',
|
|
1460
|
+
' criteria`, and `## Prompt` present, concrete, and self-contained (an AFK',
|
|
1461
|
+
' agent could start from the file alone)? Are claims/paths/“reuse X” real?',
|
|
1462
|
+
'- **The DESTINATION check** — if this task is built EXACTLY as written, do we',
|
|
1463
|
+
` end up with the behaviour issue #${input.issueNumber} asks for? A hole here is`,
|
|
1464
|
+
' the highest-value thing to flag.',
|
|
1465
|
+
'',
|
|
1466
|
+
'The SET / graph / overlap / goal-COMPOSITION lenses are OFF: there is only ONE',
|
|
1467
|
+
'task (N=1), so there is no dependency graph, no set-level gap, and no',
|
|
1468
|
+
'duplicate/overlap to assess. Do NOT invent a decomposition.',
|
|
1469
|
+
'',
|
|
1470
|
+
'## How to iterate',
|
|
1471
|
+
'',
|
|
1472
|
+
'You do NOT edit files or run git — you EMIT a verdict and the runner applies it',
|
|
1473
|
+
'in memory, then re-reviews. If a finding can be FIXED by tightening the draft,',
|
|
1474
|
+
'propose an `edit` (the FULL replacement task body — the markdown AFTER the',
|
|
1475
|
+
'frontmatter; the runner writes the frontmatter + the issue link). CONVERGE',
|
|
1476
|
+
'(`approve`, no blocking findings) when a round finds NO new blocking issue.',
|
|
1477
|
+
'When a BLOCKING question has NO clear answer in the issue thread — it needs the',
|
|
1478
|
+
'human, not another edit — `block` and put it in `questions`: the runner asks the',
|
|
1479
|
+
'human, carrying this draft. Flag, do not guess.',
|
|
1480
|
+
'',
|
|
1481
|
+
verdictContractPrompt(),
|
|
1482
|
+
'',
|
|
1483
|
+
'Fill the channels appropriate to THIS caller (the lone-task review):',
|
|
1484
|
+
' - `edit` — a single full-replacement task body (NOT a path; the task is',
|
|
1485
|
+
' not yet emitted) when tightening the draft fixes the finding.',
|
|
1486
|
+
' - `questions` — the open question(s) for the human when a blocking issue',
|
|
1487
|
+
' has no clear thread answer.',
|
|
1488
|
+
'Do NOT fill `review` / `edits` / `uncertainTasks` / `decompositionUnclear`',
|
|
1489
|
+
"— those are other callers' channels.",
|
|
1490
|
+
].join('\n');
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Backwards-compatible alias for the unified {@link parseReviewVerdict}
|
|
1494
|
+
* (task `review-protocol-doc-and-shared-machinery`). The lone-task review
|
|
1495
|
+
* verdict is now the unified {@link ReviewVerdict}; the alias keeps existing
|
|
1496
|
+
* tests/callers compiling.
|
|
1497
|
+
*/
|
|
1498
|
+
export const parseLoneTaskReviewVerdict = parseReviewVerdict;
|
|
1499
|
+
/**
|
|
1500
|
+
* Build the intake decision PRD (an inline prompt builder, like `buildTaskingPrd`
|
|
1501
|
+
* in `tasking.ts` / the reviewer prompts in `review-gate.ts` — NOT a standalone
|
|
1502
|
+
* asset/`.md` file; no such convention exists in this package). It encodes the FULL
|
|
1503
|
+
* four-outcome decision table (prd `issue-intake` — the source of truth) and the
|
|
1504
|
+
* three DECISION AIDS stated once there:
|
|
1505
|
+
*
|
|
1506
|
+
* 1. the **"clear?" bar** = `to-task`/`needsAnswers`' "would I build the wrong
|
|
1507
|
+
* thing if I guessed?" — if a material requirement/scope/acceptance question is
|
|
1508
|
+
* unanswered, ASK (never guess a spec from a vague issue);
|
|
1509
|
+
* 2. the **"one task?" bar** = `to-task`' tracer-bullet test (one thin end-to-end
|
|
1510
|
+
* path, demoable on its own) — fits → TASK, needs splitting → PRD;
|
|
1511
|
+
* 3. **PRD vs BOUNCE** turns on a **SHARED VISION**: coupled (even if small) → PRD;
|
|
1512
|
+
* genuinely unrelated → BOUNCE. Size NEVER forces a bounce — only unrelatedness
|
|
1513
|
+
* (the over-bounce guard: a coupled-but-small pair gets a light PRD, never a
|
|
1514
|
+
* bounce).
|
|
1515
|
+
*
|
|
1516
|
+
* The prompt anchors to `to-task`/`to-spec` for the task/spec SHAPES it drafts. Its
|
|
1517
|
+
* JUDGEMENT is NOT unit-tested (exactly as the review prompt's is not) — only the
|
|
1518
|
+
* dispatch is. The agent only DRAFTS the verdict + its content; it does NO git/seam
|
|
1519
|
+
* ops (the runner owns every postComment / write / integrate — the in-band boundary).
|
|
1520
|
+
*/
|
|
1521
|
+
export function buildIntakeDecisionSpec(issue, comments, triage) {
|
|
1522
|
+
const thread = comments.length === 0
|
|
1523
|
+
? '(no comments yet)'
|
|
1524
|
+
: comments
|
|
1525
|
+
.map((c, i) => `#${i + 1} ${c.author ? `@${c.author}` : '(unknown)'}: ${c.body}`)
|
|
1526
|
+
.join('\n\n');
|
|
1527
|
+
// TRIAGE ENRICHMENT (prd `issue-intake`): on the raced PROCEED path the prompt is
|
|
1528
|
+
// told which comment(s) PRE-DATE intake's last turn (context for a prior state,
|
|
1529
|
+
// not necessarily a fresh answer) and — only then — how many previously-SEEN
|
|
1530
|
+
// comments were DELETED (a flag + count; the bodies are gone, so do not name them).
|
|
1531
|
+
const triageNotes = [];
|
|
1532
|
+
if (triage?.action === 'proceed' && triage.predatingIds.length > 0) {
|
|
1533
|
+
triageNotes.push('', '## Triage note — raced comment(s) that PRE-DATE intake’s last turn', '', `${triage.predatingIds.length} comment(s) landed AFTER intake last read the`, 'thread but BEFORE it posted its last turn, so they pre-date that turn', '(possibly concurrent). Treat them as possibly-already-addressed context for a', 'PRIOR state — NOT necessarily a direct answer to intake’s latest question.');
|
|
1534
|
+
if (triage.deletedSeenCount > 0) {
|
|
1535
|
+
triageNotes.push('', `ALSO: ${triage.deletedSeenCount} previously-seen comment(s) were DELETED since`, 'intake last read the thread. Their content is gone and not recoverable; do', 'NOT assume your prior reasoning’s premises still hold — reassess from the', 'current thread.');
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
return [
|
|
1539
|
+
`You are the dorfl INTAKE agent. Decide what to do with GitHub issue`,
|
|
1540
|
+
`#${issue.number}: "${issue.title}". You read the issue + its full comment`,
|
|
1541
|
+
`thread and return ONE verdict (the runner DISPATCHES on it deterministically).`,
|
|
1542
|
+
'',
|
|
1543
|
+
'Issue body:',
|
|
1544
|
+
issue.body.trim() === '' ? '(empty)' : issue.body,
|
|
1545
|
+
'',
|
|
1546
|
+
'Comment thread (oldest first):',
|
|
1547
|
+
thread,
|
|
1548
|
+
...triageNotes,
|
|
1549
|
+
'',
|
|
1550
|
+
'## The decision — classify the issue into exactly ONE of four verdicts',
|
|
1551
|
+
'',
|
|
1552
|
+
'- **ASK** — the issue is NOT clear enough to act on: a material requirement,',
|
|
1553
|
+
' scope, or acceptance question is unanswered. Use the same bar `to-task`',
|
|
1554
|
+
' uses for `needsAnswers`: "would I build the WRONG thing if I guessed now?"',
|
|
1555
|
+
' If yes → ASK. Draft the SINGLE next clarifying question (do NOT guess a spec',
|
|
1556
|
+
' from a vague issue). The runner posts it and stops; a later run resumes from',
|
|
1557
|
+
' the updated thread.',
|
|
1558
|
+
'',
|
|
1559
|
+
'- **TASK** — the issue is CLEAR *and* it fits ONE tracer-bullet vertical task',
|
|
1560
|
+
' (a single thin end-to-end path, demoable on its own — `to-task`’ criterion).',
|
|
1561
|
+
' Draft that ONE task in the `to-task` shape (a `## What to build`,',
|
|
1562
|
+
' `## Acceptance criteria`, and `## Prompt`). The runner writes',
|
|
1563
|
+
' `work/backlog/<slug>.md` (`covers: []`, NO `prd:`) carrying `issue: N` (the',
|
|
1564
|
+
' lone-task closure link, NOT `Fixes #N`) and integrates it.',
|
|
1565
|
+
'',
|
|
1566
|
+
'- **PRD** — the issue is CLEAR *and* coherent but needs MORE THAN ONE task (it',
|
|
1567
|
+
' cannot be one tracer-bullet path — it splits for scope/architecture). >1 task',
|
|
1568
|
+
' ⟺ a shared vision worth recording ⟺ a spec. Draft a spec in the `to-spec` shape',
|
|
1569
|
+
' (`## Problem Statement`, `## Solution`, `## User Stories`, `## Out of Scope`).',
|
|
1570
|
+
' The runner writes the prd file (`work/prds/ready/<slug>.md`) with `issue: N` and integrates it;',
|
|
1571
|
+
' TASKING the prd is a SEPARATE later step (`do prd:`) — do not task it here.',
|
|
1572
|
+
' **INCLUDES a coupled-but-SMALL pair: if two asks share a vision they get a',
|
|
1573
|
+
' (light) prd — they are NEVER bounced.**',
|
|
1574
|
+
'',
|
|
1575
|
+
'- **BOUNCE** — the issue is really MULTIPLE UNRELATED concerns wearing one issue:',
|
|
1576
|
+
' you cannot articulate a SINGLE shared vision tying them together. Draft a short',
|
|
1577
|
+
' message asking the author to file separate issues. A bounce is TERMINAL, so the',
|
|
1578
|
+
' runner CLOSES the issue ATOMICALLY — your message as the closing comment +',
|
|
1579
|
+
' reason "not planned" (the honest signal that the asks must be re-filed). Intake',
|
|
1580
|
+
' closes on BOUNCE only; never on task/prd (CI’s close-job) / ask.',
|
|
1581
|
+
'',
|
|
1582
|
+
'## The three decision aids (apply them in order)',
|
|
1583
|
+
'',
|
|
1584
|
+
'1. **"clear?"** (ASK vs the rest): the `needsAnswers` bar — would acting now risk',
|
|
1585
|
+
' building the wrong thing? If yes → ASK. Otherwise it is clear; continue.',
|
|
1586
|
+
'2. **"one task?"** (TASK vs PRD): the `to-task` tracer-bullet test — one thin',
|
|
1587
|
+
' end-to-end path, demoable alone? Fits → TASK; needs splitting → PRD.',
|
|
1588
|
+
'3. **"shared vision?"** (PRD vs BOUNCE): coupled (even if small) → PRD; genuinely',
|
|
1589
|
+
' unrelated → BOUNCE. SIZE NEVER forces a bounce — only UNRELATEDNESS does. Do',
|
|
1590
|
+
' not over-bounce a small coupled pair: it is a light prd.',
|
|
1591
|
+
'',
|
|
1592
|
+
'## Boundary',
|
|
1593
|
+
'',
|
|
1594
|
+
'You only DRAFT the verdict + its content (the task/prd body, or the comment',
|
|
1595
|
+
'text). You do NOT perform ANY git operation and you do NOT post any comment — the',
|
|
1596
|
+
'runner owns every git/seam side-effect (write, integrate, postComment). For a prd',
|
|
1597
|
+
'verdict, also judge its gate axes (humanOnly / needsAnswers) so the runner can',
|
|
1598
|
+
'surface them on the emitted prd.',
|
|
1599
|
+
'',
|
|
1600
|
+
'## Output — hand the verdict back as ONE fenced JSON block',
|
|
1601
|
+
'',
|
|
1602
|
+
'Emit your verdict as a SINGLE fenced ```json block (and nothing else that looks',
|
|
1603
|
+
'like JSON). Its keys map 1:1 onto the verdict the runner dispatches on — always an',
|
|
1604
|
+
'`"outcome"` plus ONLY the fields for that outcome:',
|
|
1605
|
+
'',
|
|
1606
|
+
'```json',
|
|
1607
|
+
'{"outcome": "task", "taskSlug": "<content-derived-slug>", "taskTitle": "<title>", "taskBody": "<the markdown AFTER the frontmatter>"}',
|
|
1608
|
+
'```',
|
|
1609
|
+
'',
|
|
1610
|
+
'- **task** → `taskTitle` + `taskBody` (the `## What to build` / `## Acceptance',
|
|
1611
|
+
' criteria` / `## Prompt` markdown — NOT the frontmatter; the runner writes the',
|
|
1612
|
+
' frontmatter + the `issue: N` link) and an optional `taskSlug` (the runner',
|
|
1613
|
+
' derives one from the title if you omit it — never a counter).',
|
|
1614
|
+
'- **spec** → `specTitle` + `specBody` (the `## Problem Statement` / `## Solution` / …',
|
|
1615
|
+
' markdown AFTER the frontmatter; the runner writes the frontmatter + `issue: N`),',
|
|
1616
|
+
' an optional `specSlug`, and the gate axes `specHumanOnly` / `specNeedsAnswers`',
|
|
1617
|
+
' (booleans — set `true` when a human should drive the TASKING and/or open',
|
|
1618
|
+
' questions remain; omit otherwise).',
|
|
1619
|
+
'- **ask** → `question` (the single next clarifying question).',
|
|
1620
|
+
'- **bounce** → `bounceMessage` (the “file separate issues” message).',
|
|
1621
|
+
'',
|
|
1622
|
+
'`outcome` MUST be exactly one of `ask` | `task` | `spec` | `bounce`. Strings are',
|
|
1623
|
+
'plain text inside the JSON (escape newlines as \\n). Do not wrap the JSON in any',
|
|
1624
|
+
'other structure — the runner pulls the first `{"outcome": …}` object out and',
|
|
1625
|
+
'dispatches on it.',
|
|
1626
|
+
].join('\n');
|
|
1627
|
+
}
|
|
1628
|
+
//# sourceMappingURL=intake.js.map
|