dorfl 0.14.2 → 0.15.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 +17 -16
- package/dist/advance-ci-template.d.ts.map +1 -1
- package/dist/advance-ci-template.js +42 -53
- package/dist/advance-ci-template.js.map +1 -1
- package/dist/advance-drivers.d.ts +8 -0
- package/dist/advance-drivers.d.ts.map +1 -1
- package/dist/advance-drivers.js +28 -4
- package/dist/advance-drivers.js.map +1 -1
- package/dist/advance-isolated.d.ts.map +1 -1
- package/dist/advance-isolated.js +3 -2
- package/dist/advance-isolated.js.map +1 -1
- package/dist/advance-lifecycle-template.d.ts +41 -40
- package/dist/advance-lifecycle-template.d.ts.map +1 -1
- package/dist/advance-lifecycle-template.js +309 -489
- package/dist/advance-lifecycle-template.js.map +1 -1
- package/dist/advance-loop-driver.d.ts.map +1 -1
- package/dist/advance-loop-driver.js +3 -2
- package/dist/advance-loop-driver.js.map +1 -1
- package/dist/advance-treeless-publish.d.ts +31 -0
- package/dist/advance-treeless-publish.d.ts.map +1 -1
- package/dist/advance-treeless-publish.js +40 -0
- package/dist/advance-treeless-publish.js.map +1 -1
- package/dist/advance.d.ts +63 -3
- package/dist/advance.d.ts.map +1 -1
- package/dist/advance.js +80 -12
- package/dist/advance.js.map +1 -1
- package/dist/advancing-lock.d.ts +9 -0
- package/dist/advancing-lock.d.ts.map +1 -1
- package/dist/advancing-lock.js +21 -1
- package/dist/advancing-lock.js.map +1 -1
- package/dist/agent-env.d.ts +61 -0
- package/dist/agent-env.d.ts.map +1 -0
- package/dist/agent-env.js +144 -0
- package/dist/agent-env.js.map +1 -0
- package/dist/agent-stop.d.ts +35 -0
- package/dist/agent-stop.d.ts.map +1 -1
- package/dist/agent-stop.js +62 -0
- package/dist/agent-stop.js.map +1 -1
- package/dist/apply-merge-action.d.ts +76 -34
- package/dist/apply-merge-action.d.ts.map +1 -1
- package/dist/apply-merge-action.js +339 -129
- package/dist/apply-merge-action.js.map +1 -1
- package/dist/apply-stuck-action.js +2 -2
- package/dist/apply-stuck-action.js.map +1 -1
- package/dist/ci-agent-result.d.ts +151 -0
- package/dist/ci-agent-result.d.ts.map +1 -0
- package/dist/ci-agent-result.js +314 -0
- package/dist/ci-agent-result.js.map +1 -0
- package/dist/ci-handoff-apply.d.ts +162 -0
- package/dist/ci-handoff-apply.d.ts.map +1 -0
- package/dist/ci-handoff-apply.js +497 -0
- package/dist/ci-handoff-apply.js.map +1 -0
- package/dist/ci-handoff-format.d.ts +369 -0
- package/dist/ci-handoff-format.d.ts.map +1 -0
- package/dist/ci-handoff-format.js +646 -0
- package/dist/ci-handoff-format.js.map +1 -0
- package/dist/ci-handoff-lfs.d.ts +146 -0
- package/dist/ci-handoff-lfs.d.ts.map +1 -0
- package/dist/ci-handoff-lfs.js +397 -0
- package/dist/ci-handoff-lfs.js.map +1 -0
- package/dist/ci-handoff.d.ts +99 -0
- package/dist/ci-handoff.d.ts.map +1 -0
- package/dist/ci-handoff.js +277 -0
- package/dist/ci-handoff.js.map +1 -0
- package/dist/ci-lock-outputs.d.ts +93 -0
- package/dist/ci-lock-outputs.d.ts.map +1 -0
- package/dist/ci-lock-outputs.js +223 -0
- package/dist/ci-lock-outputs.js.map +1 -0
- package/dist/ci-phase-build.d.ts +178 -0
- package/dist/ci-phase-build.d.ts.map +1 -0
- package/dist/ci-phase-build.js +989 -0
- package/dist/ci-phase-build.js.map +1 -0
- package/dist/ci-phase-driver.d.ts +247 -0
- package/dist/ci-phase-driver.d.ts.map +1 -0
- package/dist/ci-phase-driver.js +370 -0
- package/dist/ci-phase-driver.js.map +1 -0
- package/dist/ci-phase-intake.d.ts +172 -0
- package/dist/ci-phase-intake.d.ts.map +1 -0
- package/dist/ci-phase-intake.js +555 -0
- package/dist/ci-phase-intake.js.map +1 -0
- package/dist/ci-phase-tasking.d.ts +205 -0
- package/dist/ci-phase-tasking.d.ts.map +1 -0
- package/dist/ci-phase-tasking.js +906 -0
- package/dist/ci-phase-tasking.js.map +1 -0
- package/dist/ci-phase-treeless.d.ts +224 -0
- package/dist/ci-phase-treeless.d.ts.map +1 -0
- package/dist/ci-phase-treeless.js +1482 -0
- package/dist/ci-phase-treeless.js.map +1 -0
- package/dist/ci-read-token.d.ts +47 -0
- package/dist/ci-read-token.d.ts.map +1 -0
- package/dist/ci-read-token.js +80 -0
- package/dist/ci-read-token.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +447 -22
- package/dist/cli.js.map +1 -1
- package/dist/close-job-template.d.ts +10 -1
- package/dist/close-job-template.d.ts.map +1 -1
- package/dist/close-job-template.js +46 -14
- package/dist/close-job-template.js.map +1 -1
- package/dist/close-job.d.ts +20 -5
- package/dist/close-job.d.ts.map +1 -1
- package/dist/close-job.js +65 -12
- package/dist/close-job.js.map +1 -1
- package/dist/complete.d.ts.map +1 -1
- package/dist/complete.js +20 -1
- package/dist/complete.js.map +1 -1
- package/dist/config.d.ts +41 -44
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -4
- package/dist/config.js.map +1 -1
- package/dist/continue-branch.d.ts +63 -15
- package/dist/continue-branch.d.ts.map +1 -1
- package/dist/continue-branch.js +75 -8
- package/dist/continue-branch.js.map +1 -1
- package/dist/crashed-run-save.d.ts +73 -0
- package/dist/crashed-run-save.d.ts.map +1 -0
- package/dist/crashed-run-save.js +86 -0
- package/dist/crashed-run-save.js.map +1 -0
- package/dist/cwd-section.d.ts.map +1 -1
- package/dist/cwd-section.js +10 -2
- package/dist/cwd-section.js.map +1 -1
- package/dist/decision-engine.d.ts +10 -0
- package/dist/decision-engine.d.ts.map +1 -1
- package/dist/decision-engine.js.map +1 -1
- package/dist/do-config.d.ts +2 -24
- package/dist/do-config.d.ts.map +1 -1
- package/dist/do-config.js +1 -33
- package/dist/do-config.js.map +1 -1
- package/dist/do.d.ts +149 -1
- package/dist/do.d.ts.map +1 -1
- package/dist/do.js +115 -25
- package/dist/do.js.map +1 -1
- package/dist/dorfl-item-template.d.ts +98 -0
- package/dist/dorfl-item-template.d.ts.map +1 -0
- package/dist/dorfl-item-template.js +504 -0
- package/dist/dorfl-item-template.js.map +1 -0
- package/dist/env-config.d.ts.map +1 -1
- package/dist/env-config.js +7 -5
- package/dist/env-config.js.map +1 -1
- package/dist/frontmatter.d.ts +52 -0
- package/dist/frontmatter.d.ts.map +1 -1
- package/dist/frontmatter.js +136 -2
- package/dist/frontmatter.js.map +1 -1
- package/dist/gc.d.ts +13 -0
- package/dist/gc.d.ts.map +1 -1
- package/dist/gc.js +16 -2
- package/dist/gc.js.map +1 -1
- package/dist/git.d.ts +18 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +70 -5
- package/dist/git.js.map +1 -1
- package/dist/harness.d.ts +44 -5
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +24 -1
- package/dist/harness.js.map +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/install-ci-action-pins.d.ts +123 -0
- package/dist/install-ci-action-pins.d.ts.map +1 -0
- package/dist/install-ci-action-pins.js +190 -0
- package/dist/install-ci-action-pins.js.map +1 -0
- package/dist/install-ci-actions-pr-creation.d.ts +70 -0
- package/dist/install-ci-actions-pr-creation.d.ts.map +1 -0
- package/dist/install-ci-actions-pr-creation.js +106 -0
- package/dist/install-ci-actions-pr-creation.js.map +1 -0
- package/dist/install-ci-capabilities/dorfl-item.d.ts +12 -0
- package/dist/install-ci-capabilities/dorfl-item.d.ts.map +1 -0
- package/dist/install-ci-capabilities/dorfl-item.js +26 -0
- package/dist/install-ci-capabilities/dorfl-item.js.map +1 -0
- package/dist/install-ci-core.d.ts +155 -37
- package/dist/install-ci-core.d.ts.map +1 -1
- package/dist/install-ci-core.js +304 -200
- package/dist/install-ci-core.js.map +1 -1
- package/dist/install-ci-github.d.ts +47 -2
- package/dist/install-ci-github.d.ts.map +1 -1
- package/dist/install-ci-github.js +113 -1
- package/dist/install-ci-github.js.map +1 -1
- package/dist/install-ci.d.ts +25 -6
- package/dist/install-ci.d.ts.map +1 -1
- package/dist/install-ci.js +72 -36
- package/dist/install-ci.js.map +1 -1
- package/dist/intake-marker.d.ts +6 -1
- package/dist/intake-marker.d.ts.map +1 -1
- package/dist/intake-marker.js.map +1 -1
- package/dist/intake-triage.d.ts.map +1 -1
- package/dist/intake-triage.js +6 -2
- package/dist/intake-triage.js.map +1 -1
- package/dist/intake-trigger-template.d.ts +31 -25
- package/dist/intake-trigger-template.d.ts.map +1 -1
- package/dist/intake-trigger-template.js +107 -292
- package/dist/intake-trigger-template.js.map +1 -1
- package/dist/intake.d.ts +186 -4
- package/dist/intake.d.ts.map +1 -1
- package/dist/intake.js +533 -416
- package/dist/intake.js.map +1 -1
- package/dist/integration-core.d.ts +154 -1
- package/dist/integration-core.d.ts.map +1 -1
- package/dist/integration-core.js +759 -486
- package/dist/integration-core.js.map +1 -1
- package/dist/integrator.d.ts +12 -0
- package/dist/integrator.d.ts.map +1 -1
- package/dist/integrator.js +21 -0
- package/dist/integrator.js.map +1 -1
- package/dist/isolation.d.ts +12 -2
- package/dist/isolation.d.ts.map +1 -1
- package/dist/isolation.js +19 -8
- package/dist/isolation.js.map +1 -1
- package/dist/item-lock.d.ts +93 -11
- package/dist/item-lock.d.ts.map +1 -1
- package/dist/item-lock.js +233 -65
- package/dist/item-lock.js.map +1 -1
- package/dist/land-question-residue.d.ts +58 -0
- package/dist/land-question-residue.d.ts.map +1 -0
- package/dist/land-question-residue.js +95 -0
- package/dist/land-question-residue.js.map +1 -0
- package/dist/lifecycle-gather.d.ts +16 -0
- package/dist/lifecycle-gather.d.ts.map +1 -1
- package/dist/lifecycle-gather.js +54 -6
- package/dist/lifecycle-gather.js.map +1 -1
- package/dist/merge-answer.d.ts +41 -0
- package/dist/merge-answer.d.ts.map +1 -0
- package/dist/merge-answer.js +60 -0
- package/dist/merge-answer.js.map +1 -0
- package/dist/merge-question-surfacer.d.ts +105 -30
- package/dist/merge-question-surfacer.d.ts.map +1 -1
- package/dist/merge-question-surfacer.js +202 -91
- package/dist/merge-question-surfacer.js.map +1 -1
- package/dist/merge-question-tick.d.ts +64 -0
- package/dist/merge-question-tick.d.ts.map +1 -0
- package/dist/merge-question-tick.js +96 -0
- package/dist/merge-question-tick.js.map +1 -0
- package/dist/mint-adr.d.ts.map +1 -1
- package/dist/mint-adr.js +20 -2
- package/dist/mint-adr.js.map +1 -1
- package/dist/needs-attention.d.ts +72 -2
- package/dist/needs-attention.d.ts.map +1 -1
- package/dist/needs-attention.js +275 -38
- package/dist/needs-attention.js.map +1 -1
- package/dist/phase-recorder.d.ts +168 -0
- package/dist/phase-recorder.d.ts.map +1 -0
- package/dist/phase-recorder.js +218 -0
- package/dist/phase-recorder.js.map +1 -0
- package/dist/phase.d.ts +94 -0
- package/dist/phase.d.ts.map +1 -0
- package/dist/phase.js +148 -0
- package/dist/phase.js.map +1 -0
- package/dist/pi-harness.d.ts.map +1 -1
- package/dist/pi-harness.js +19 -6
- package/dist/pi-harness.js.map +1 -1
- package/dist/prepare.d.ts.map +1 -1
- package/dist/prepare.js +4 -0
- package/dist/prepare.js.map +1 -1
- package/dist/prompt.d.ts +4 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +48 -1
- package/dist/prompt.js.map +1 -1
- package/dist/protocol/CLAIM-PROTOCOL.md +3 -1
- package/dist/protocol/WORK-CONTRACT.md +1 -1
- package/dist/reap-branches.d.ts +48 -0
- package/dist/reap-branches.d.ts.map +1 -1
- package/dist/reap-branches.js +64 -0
- package/dist/reap-branches.js.map +1 -1
- package/dist/ref-write.d.ts +198 -0
- package/dist/ref-write.d.ts.map +1 -0
- package/dist/ref-write.js +79 -0
- package/dist/ref-write.js.map +1 -0
- package/dist/repo-config.js +8 -8
- package/dist/repo-config.js.map +1 -1
- package/dist/repo-mirror.d.ts +36 -0
- package/dist/repo-mirror.d.ts.map +1 -1
- package/dist/repo-mirror.js +91 -1
- package/dist/repo-mirror.js.map +1 -1
- package/dist/review-verdict.d.ts +1 -1
- package/dist/review-verdict.js +1 -1
- package/dist/run.d.ts +7 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +28 -4
- package/dist/run.js.map +1 -1
- package/dist/scan.d.ts +19 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +21 -8
- package/dist/scan.js.map +1 -1
- package/dist/sidecar.d.ts +14 -0
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/sidecar.js +21 -0
- package/dist/sidecar.js.map +1 -1
- package/dist/single-job-warning.d.ts +63 -0
- package/dist/single-job-warning.d.ts.map +1 -0
- package/dist/single-job-warning.js +110 -0
- package/dist/single-job-warning.js.map +1 -0
- package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +3 -1
- package/dist/skills/setup/protocol/WORK-CONTRACT.md +1 -1
- package/dist/spec-complete.d.ts +13 -6
- package/dist/spec-complete.d.ts.map +1 -1
- package/dist/spec-complete.js +23 -11
- package/dist/spec-complete.js.map +1 -1
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +47 -14
- package/dist/start.js.map +1 -1
- package/dist/status.d.ts +12 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +11 -3
- package/dist/status.js.map +1 -1
- package/dist/tasking.d.ts +246 -1
- package/dist/tasking.d.ts.map +1 -1
- package/dist/tasking.js +452 -358
- package/dist/tasking.js.map +1 -1
- package/dist/triage-persist.d.ts.map +1 -1
- package/dist/triage-persist.js +21 -2
- package/dist/triage-persist.js.map +1 -1
- package/dist/verify-workflow-template.d.ts.map +1 -1
- package/dist/verify-workflow-template.js +8 -4
- package/dist/verify-workflow-template.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +4 -0
- package/dist/verify.js.map +1 -1
- package/dist/watch-session.d.ts +42 -13
- package/dist/watch-session.d.ts.map +1 -1
- package/dist/watch-session.js +52 -23
- package/dist/watch-session.js.map +1 -1
- package/dist/work-layout.d.ts +10 -4
- package/dist/work-layout.d.ts.map +1 -1
- package/dist/work-layout.js +11 -3
- package/dist/work-layout.js.map +1 -1
- package/dist/workspace.d.ts +65 -4
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +159 -11
- package/dist/workspace.js.map +1 -1
- package/package.json +1 -1
- package/src/advance-ci-template.ts +80 -81
- package/src/advance-drivers.ts +45 -6
- package/src/advance-isolated.ts +3 -5
- package/src/advance-lifecycle-template.ts +375 -538
- package/src/advance-loop-driver.ts +3 -5
- package/src/advance-treeless-publish.ts +61 -0
- package/src/advance.ts +145 -15
- package/src/advancing-lock.ts +36 -1
- package/src/agent-env.ts +174 -0
- package/src/agent-stop.ts +81 -0
- package/src/apply-merge-action.ts +401 -154
- package/src/apply-stuck-action.ts +2 -2
- package/src/ci-agent-result.ts +421 -0
- package/src/ci-handoff-apply.ts +737 -0
- package/src/ci-handoff-format.ts +926 -0
- package/src/ci-handoff-lfs.ts +519 -0
- package/src/ci-handoff.ts +392 -0
- package/src/ci-lock-outputs.ts +273 -0
- package/src/ci-phase-build.ts +1328 -0
- package/src/ci-phase-driver.ts +548 -0
- package/src/ci-phase-intake.ts +861 -0
- package/src/ci-phase-tasking.ts +1255 -0
- package/src/ci-phase-treeless.ts +2028 -0
- package/src/ci-read-token.ts +90 -0
- package/src/cli.ts +591 -29
- package/src/close-job-template.ts +52 -15
- package/src/close-job.ts +77 -12
- package/src/complete.ts +19 -1
- package/src/config.ts +43 -48
- package/src/continue-branch.ts +121 -16
- package/src/crashed-run-save.ts +162 -0
- package/src/cwd-section.ts +10 -0
- package/src/decision-engine.ts +10 -0
- package/src/do-config.ts +2 -57
- package/src/do.ts +170 -28
- package/src/dorfl-item-template.ts +556 -0
- package/src/env-config.ts +7 -5
- package/src/frontmatter.ts +155 -2
- package/src/gc.ts +27 -2
- package/src/git.ts +83 -5
- package/src/harness.ts +58 -6
- package/src/index.ts +50 -1
- package/src/install-ci-action-pins.ts +209 -0
- package/src/install-ci-actions-pr-creation.ts +186 -0
- package/src/install-ci-capabilities/dorfl-item.ts +33 -0
- package/src/install-ci-core.ts +388 -233
- package/src/install-ci-github.ts +149 -1
- package/src/install-ci.ts +83 -37
- package/src/intake-marker.ts +6 -1
- package/src/intake-triage.ts +7 -3
- package/src/intake-trigger-template.ts +126 -327
- package/src/intake.ts +725 -544
- package/src/integration-core.ts +975 -523
- package/src/integrator.ts +28 -0
- package/src/isolation.ts +29 -10
- package/src/item-lock.ts +308 -92
- package/src/land-question-residue.ts +122 -0
- package/src/lifecycle-gather.ts +82 -10
- package/src/merge-answer.ts +82 -0
- package/src/merge-question-surfacer.ts +320 -112
- package/src/merge-question-tick.ts +182 -0
- package/src/mint-adr.ts +28 -2
- package/src/needs-attention.ts +350 -42
- package/src/phase-recorder.ts +350 -0
- package/src/phase.ts +177 -0
- package/src/pi-harness.ts +25 -5
- package/src/prepare.ts +4 -0
- package/src/prompt.ts +68 -1
- package/src/reap-branches.ts +107 -0
- package/src/ref-write.ts +324 -0
- package/src/repo-config.ts +8 -8
- package/src/repo-mirror.ts +107 -1
- package/src/review-verdict.ts +1 -1
- package/src/run.ts +47 -6
- package/src/scan.ts +37 -7
- package/src/sidecar.ts +36 -0
- package/src/single-job-warning.ts +135 -0
- package/src/spec-complete.ts +36 -14
- package/src/start.ts +55 -16
- package/src/status.ts +29 -3
- package/src/tasking.ts +593 -378
- package/src/triage-persist.ts +24 -2
- package/src/verify-workflow-template.ts +8 -4
- package/src/verify.ts +4 -0
- package/src/watch-session.ts +86 -28
- package/src/work-layout.ts +11 -3
- package/src/workspace.ts +210 -14
package/dist/intake.js
CHANGED
|
@@ -4,8 +4,10 @@ import { runAsync } from './git.js';
|
|
|
4
4
|
import { workFolderRel, workFolderPrefix } from './work-layout.js';
|
|
5
5
|
import { paramCase } from './brand.js';
|
|
6
6
|
import { ensureSafeSlug } from './slug-safety.js';
|
|
7
|
-
import { performIntegration, } from './integration-core.js';
|
|
7
|
+
import { performIntegration, proposePointer, } from './integration-core.js';
|
|
8
|
+
import { proposeRequestNotOpened, } from './integrator.js';
|
|
8
9
|
import { integrationFromFlags } from './complete.js';
|
|
10
|
+
import { assertFrontmatterFields, quoteYamlScalar, singleLineScalarProblem, } from './frontmatter.js';
|
|
9
11
|
import { TASK_PLACEMENT_SLOTS, landingToSide } from './tasking.js';
|
|
10
12
|
import { placementFolder, resolvePlacement, } from './placement.js';
|
|
11
13
|
import { identityEnv, assertTransportAllowed, } from './identity.js';
|
|
@@ -154,20 +156,15 @@ export async function performIntake(options) {
|
|
|
154
156
|
// silently pushing under an ambient credential. Resolve the arbiter URL softly
|
|
155
157
|
// (a non-zero/unknown URL is skipped — the guard is a no-op without an identity
|
|
156
158
|
// or a resolvable URL).
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
167
|
-
note(message);
|
|
168
|
-
return { exitCode: 1, outcome: 'usage-error', issueNumber, message };
|
|
169
|
-
}
|
|
170
|
-
}
|
|
159
|
+
const transportProblem = await intakeTransportProblem(options.identity, cwd, arbiter, env);
|
|
160
|
+
if (transportProblem !== undefined) {
|
|
161
|
+
note(transportProblem);
|
|
162
|
+
return {
|
|
163
|
+
exitCode: 1,
|
|
164
|
+
outcome: 'usage-error',
|
|
165
|
+
issueNumber,
|
|
166
|
+
message: transportProblem,
|
|
167
|
+
};
|
|
171
168
|
}
|
|
172
169
|
// 1. READ the issue + thread via the seam (the core never imports `gh`; only the
|
|
173
170
|
// adapter shells out). A read failure surfaces as a usage error — `intake`
|
|
@@ -350,14 +347,16 @@ function createLockReleaser(params) {
|
|
|
350
347
|
* Split out so the lock release is a clean `try`/`finally` in the caller (the lock
|
|
351
348
|
* MUST release on every terminal path — success or handled failure). The agent
|
|
352
349
|
* DRAFTS only; the runner owns every git/seam side-effect here.
|
|
350
|
+
*
|
|
351
|
+
* It composes the SAME two halves the CI phases run in two jobs (task
|
|
352
|
+
* `ci-split-intake`, `ci-phase-intake.ts`): {@link decideIntake} (the agent half:
|
|
353
|
+
* the decision agent and the lone-task review, no write) and
|
|
354
|
+
* {@link applyIntakeDecision} (the write half: the comment, the close, or the
|
|
355
|
+
* document render + integrate + completion comment).
|
|
353
356
|
*/
|
|
354
357
|
async function decideAndDispatch(options, cwd, issue, comments, ctx) {
|
|
355
358
|
const { arbiter, issueProvider, note } = ctx;
|
|
356
359
|
const issueNumber = issue.number;
|
|
357
|
-
// `env` here is the identity-scoped GIT/provider env (the runner's `gh`/git
|
|
358
|
-
// ops). The AGENT launches (decision agent, lone-task review) use the AMBIENT
|
|
359
|
-
// `options.env` — an agent must not act as the bot.
|
|
360
|
-
const env = ctx.gitEnv;
|
|
361
360
|
// TRIAGE (deterministic, under the lock, BEFORE the prompt): decide whether to run
|
|
362
361
|
// the decision at all, built ENTIRELY on intake's own MARKER on the thread (no
|
|
363
362
|
// sidecar/cursor/bot-identity). It SKIPS when intake has the last word
|
|
@@ -367,158 +366,459 @@ async function decideAndDispatch(options, cwd, issue, comments, ctx) {
|
|
|
367
366
|
// is excluded from the human-comment check by construction.
|
|
368
367
|
const triage = triageIntake(comments);
|
|
369
368
|
if (triage.action === 'skip') {
|
|
370
|
-
const message = triage.outcome
|
|
371
|
-
? `Intake of issue #${issueNumber} found nothing new: it has the last word ` +
|
|
372
|
-
`on the thread and has already seen every human comment up to it; doing ` +
|
|
373
|
-
`nothing (the decision prompt did not run).`
|
|
374
|
-
: `Intake of issue #${issueNumber} skipped: the issue was already ` +
|
|
375
|
-
`transformed (a terminal intake marker is on the thread); a later human ` +
|
|
376
|
-
`comment does not re-open it (the decision prompt did not run).`;
|
|
369
|
+
const message = intakeSkipMessage(issueNumber, triage.outcome);
|
|
377
370
|
note(message);
|
|
378
371
|
return { exitCode: 0, outcome: triage.outcome, issueNumber, message };
|
|
379
372
|
}
|
|
380
|
-
// DECIDE: prompt → VERDICT. The agent DRAFTS only (no
|
|
381
|
-
//
|
|
382
|
-
|
|
373
|
+
// DECIDE: prompt → VERDICT (+ the lone-task review). The agent DRAFTS only (no
|
|
374
|
+
// git, no seam ops).
|
|
375
|
+
const decided = await decideIntake(options, cwd, issue, comments, triage, note);
|
|
376
|
+
if (!decided.ok) {
|
|
377
|
+
return decided.result;
|
|
378
|
+
}
|
|
379
|
+
// DISPATCH on the decision. The runner owns every git/seam side-effect (the
|
|
380
|
+
// in-band boundary). PER-OUTCOME integration (spec `issue-intake` US #9): the
|
|
381
|
+
// resolved mode is keyed on the runtime artifact TYPE. The per-run `seen=`
|
|
382
|
+
// DELTA (the HUMAN comment ids intake READ this run, excluding its own
|
|
383
|
+
// marker-comments + already-seen ids) rides every comment intake posts.
|
|
384
|
+
return applyIntakeDecision(decided.decision, {
|
|
385
|
+
issueNumber,
|
|
386
|
+
cwd,
|
|
387
|
+
arbiter,
|
|
388
|
+
issueProvider,
|
|
389
|
+
integration: options.integration ?? { task: 'propose', spec: 'propose' },
|
|
390
|
+
mergeRetries: options.mergeRetries,
|
|
391
|
+
// The origin-trust STAMP, passed IN (not resolved here). Unset ⇒ unstamped
|
|
392
|
+
// (a local intake ⇒ human/trusted).
|
|
393
|
+
originTrust: options.originTrust,
|
|
394
|
+
noPR: options.noPR,
|
|
395
|
+
placement: intakePlacementInputs(options),
|
|
396
|
+
providerInstance: options.providerInstance,
|
|
397
|
+
seen: computeSeenDelta(comments),
|
|
398
|
+
// The identity-scoped GIT/provider env (the `gh` ops, push, PR).
|
|
399
|
+
env: ctx.gitEnv,
|
|
400
|
+
note,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
/** The message of a triage SKIP (the decision prompt did not run). */
|
|
404
|
+
export function intakeSkipMessage(issueNumber, outcome) {
|
|
405
|
+
return outcome === 'no-new-input'
|
|
406
|
+
? `Intake of issue #${issueNumber} found nothing new: it has the last word ` +
|
|
407
|
+
`on the thread and has already seen every human comment up to it; doing ` +
|
|
408
|
+
`nothing (the decision prompt did not run).`
|
|
409
|
+
: `Intake of issue #${issueNumber} skipped: the issue was already ` +
|
|
410
|
+
`transformed (a terminal intake marker is on the thread); a later human ` +
|
|
411
|
+
`comment does not re-open it (the decision prompt did not run).`;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* The push-time transport-coherence guard (identity): the problem message when
|
|
415
|
+
* a configured identity forbids the arbiter's transport, else `undefined`. The
|
|
416
|
+
* arbiter URL is resolved softly (a non-zero/unknown URL is skipped); a no-op
|
|
417
|
+
* without an identity.
|
|
418
|
+
*/
|
|
419
|
+
export async function intakeTransportProblem(identity, cwd, arbiter, env) {
|
|
420
|
+
if (identity === undefined) {
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
const urlRes = await runAsync('git', ['remote', 'get-url', arbiter], cwd, {
|
|
424
|
+
env,
|
|
425
|
+
});
|
|
426
|
+
if (urlRes.status !== 0) {
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
assertTransportAllowed(identity, urlRes.stdout.trim());
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
return err instanceof Error ? err.message : String(err);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* The AGENT half of intake (task `ci-split-intake`): run the decision agent on the
|
|
439
|
+
* issue + thread, refuse a verdict whose title cannot be one frontmatter line,
|
|
440
|
+
* derive the content-derived slug, and (for a task) run the bounded lone-task
|
|
441
|
+
* review. Performs NO write (no git, no seam op); returns the
|
|
442
|
+
* {@link IntakeDecision} the write half acts on, or the failed
|
|
443
|
+
* {@link IntakeResult} (`agent-failed`, or `usage-error` for a verdict with no
|
|
444
|
+
* usable slug). The laptop `intake` and the CI agent job both run it.
|
|
445
|
+
*/
|
|
446
|
+
export async function decideIntake(options, cwd, issue, comments, triage, note) {
|
|
447
|
+
const issueNumber = issue.number;
|
|
448
|
+
const failed = (outcome, message) => {
|
|
449
|
+
// Not noted: every caller returns this result, whose message the CLI
|
|
450
|
+
// prints as the run's result line (task `ci-phase-logs-each-line-once`).
|
|
451
|
+
return { ok: false, result: { exitCode: 1, outcome, issueNumber, message } };
|
|
452
|
+
};
|
|
453
|
+
// DECIDE: prompt → VERDICT. Tests inject a canned verdict (the dispatcher's
|
|
454
|
+
// testable seam); production wires the harness. The prompt's judgement is not
|
|
455
|
+
// unit-tested; only the dispatch is.
|
|
383
456
|
const prompt = buildIntakeDecisionSpec(issue, comments, triage);
|
|
384
457
|
let verdict;
|
|
385
458
|
try {
|
|
386
459
|
verdict = await runDecision(options, cwd, issue, comments, prompt);
|
|
460
|
+
// The VERDICT BOUNDARY for the drafted title (task
|
|
461
|
+
// `intake-frontmatter-title-injection-strips-origin-stamp`): a title that is
|
|
462
|
+
// not one line, holds a control character, or is blank is refused HERE, so
|
|
463
|
+
// it degrades onto `agent-failed` like any malformed verdict (never a silent
|
|
464
|
+
// emit). Runs for the injected `decide` seam and the parsed harness verdict.
|
|
465
|
+
assertIntakeVerdictTitle(verdict);
|
|
387
466
|
}
|
|
388
467
|
catch (err) {
|
|
389
468
|
const detail = err instanceof Error ? err.message : String(err);
|
|
390
|
-
|
|
391
|
-
note(message);
|
|
392
|
-
return { exitCode: 1, outcome: 'agent-failed', issueNumber, message };
|
|
469
|
+
return failed('agent-failed', `Intake decision failed for issue #${issueNumber}: ${detail}`);
|
|
393
470
|
}
|
|
394
|
-
// DISPATCH on the verdict — the FULL four-outcome decision table (spec
|
|
395
|
-
// `issue-intake`). The agent only DRAFTED the verdict; the runner owns every
|
|
396
|
-
// git/seam side-effect below (the in-band boundary): the write + integrate
|
|
397
|
-
// (task/spec) and the `postIssueComment` (ask/bounce).
|
|
398
|
-
//
|
|
399
|
-
// PER-OUTCOME integration (spec `issue-intake` US #9): the resolved mode is keyed on the runtime
|
|
400
|
-
// artifact TYPE — a `task` verdict integrates with the task mode, a `spec`
|
|
401
|
-
// verdict with the spec mode. Unset ⇒ propose for both. ask/bounce never
|
|
402
|
-
// integrate, so the modes are no-ops for them.
|
|
403
|
-
const modes = options.integration ?? { task: 'propose', spec: 'propose' };
|
|
404
|
-
// The per-run `seen=` DELTA (the HUMAN comment ids intake READ this run, excluding
|
|
405
|
-
// its own marker-comments + already-seen ids) the marker records on every comment
|
|
406
|
-
// intake posts — the chain-model primitive the TRIAGE unions into `seenSet`.
|
|
407
|
-
const seenDelta = computeSeenDelta(comments);
|
|
408
471
|
switch (verdict.outcome) {
|
|
409
|
-
case 'task':
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
472
|
+
case 'task': {
|
|
473
|
+
// A content-derived slug, NEVER a counter (spec `issue-intake` US #8).
|
|
474
|
+
const slug = resolveSlug(verdict);
|
|
475
|
+
if (slug === '') {
|
|
476
|
+
return failed('usage-error', `Intake produced a 'task' verdict for issue #${issueNumber} with no usable ` +
|
|
477
|
+
`slug/title to derive a content-derived slug from (never a counter).`);
|
|
478
|
+
}
|
|
479
|
+
// BOUNDED INTERNAL REVIEW (observation
|
|
480
|
+
// `intake-lone-task-skips-adversarial-review-the-prd-path-gets`, rulings
|
|
481
|
+
// A/B/C): AFTER the `task` verdict and BEFORE the write/integrate, run a
|
|
482
|
+
// bounded (3-round, HARD-CAPPED) adversarial self-review on the SINGLE
|
|
483
|
+
// drafted task. It mutates the candidate body IN MEMORY. A launch/parse
|
|
484
|
+
// failure degrades onto `agent-failed` (never a silent emit of the
|
|
485
|
+
// un-reviewed task).
|
|
486
|
+
let review;
|
|
487
|
+
try {
|
|
488
|
+
review = await runLoneTaskReview({
|
|
489
|
+
slug,
|
|
490
|
+
issueNumber,
|
|
491
|
+
draftTitle: verdict.taskTitle ?? slug,
|
|
492
|
+
draftBody: verdict.taskBody,
|
|
493
|
+
gate: resolveLoneTaskReviewGate(options),
|
|
494
|
+
cwd,
|
|
495
|
+
// The review AGENT launches AMBIENT (never the identity-scoped env).
|
|
496
|
+
env: options.env,
|
|
497
|
+
note,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
catch (err) {
|
|
501
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
502
|
+
return failed('agent-failed', `Intake lone-task review failed for issue #${issueNumber}: ${detail}`);
|
|
503
|
+
}
|
|
504
|
+
if (review.outcome === 'non-converge') {
|
|
505
|
+
// NON-CONVERGE (ruling C): FLIP the verdict TASK→ASK, reusing the EXISTING
|
|
506
|
+
// `asked` outcome + `kind=ask` marker. The ASK comment carries BOTH the
|
|
507
|
+
// proposed task DRAFT and the open question(s) in its BODY. NEVER write
|
|
508
|
+
// the task; the next intake run resumes via the triage gate.
|
|
509
|
+
note(`Intake's lone-task review did not converge for issue #${issueNumber} ` +
|
|
510
|
+
`(${review.passes} round(s)); flipping TASK→ASK with the draft + open ` +
|
|
511
|
+
`question(s) in the comment body.`);
|
|
512
|
+
return {
|
|
513
|
+
ok: true,
|
|
514
|
+
decision: {
|
|
515
|
+
kind: 'ask',
|
|
516
|
+
body: composeLoneTaskAskComment({
|
|
517
|
+
issueNumber,
|
|
518
|
+
slug,
|
|
519
|
+
draftTitle: review.title,
|
|
520
|
+
draftBody: review.body,
|
|
521
|
+
questions: review.questions,
|
|
522
|
+
}),
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
// CONVERGED: the (possibly edited) task is emitted; the refined body
|
|
527
|
+
// replaces the agent's first draft.
|
|
528
|
+
return {
|
|
529
|
+
ok: true,
|
|
530
|
+
decision: { kind: 'task', slug, title: review.title, body: review.body },
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
// The `spec` outcome is the parent-spec verdict. HARD CUTOVER: the legacy
|
|
534
|
+
// ''prd'' outcome case is GONE.
|
|
535
|
+
case 'spec': {
|
|
536
|
+
const slug = resolveSpecSlug(verdict);
|
|
537
|
+
if (slug === '') {
|
|
538
|
+
return failed('usage-error', `Intake produced a 'spec' verdict for issue #${issueNumber} with no usable ` +
|
|
539
|
+
`slug/title to derive a content-derived slug from (never a counter).`);
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
ok: true,
|
|
543
|
+
decision: {
|
|
544
|
+
kind: 'spec',
|
|
545
|
+
slug,
|
|
546
|
+
title: verdict.specTitle ?? slug,
|
|
547
|
+
body: verdict.specBody,
|
|
548
|
+
humanOnly: verdict.specHumanOnly,
|
|
549
|
+
needsAnswers: verdict.specNeedsAnswers,
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
case 'ask':
|
|
554
|
+
// The drafted clarifying question; a thin fallback keeps the comment
|
|
555
|
+
// non-empty if the agent left it blank.
|
|
556
|
+
return {
|
|
557
|
+
ok: true,
|
|
558
|
+
decision: {
|
|
559
|
+
kind: 'ask',
|
|
560
|
+
body: verdict.question && verdict.question.trim() !== ''
|
|
561
|
+
? verdict.question
|
|
562
|
+
: `Could you clarify issue #${issueNumber} so it can be acted on?`,
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
case 'bounce':
|
|
566
|
+
// The drafted bounce message; a thin fallback restates the "file separate
|
|
567
|
+
// issues" ask. A bounce is TERMINAL: the issue is CLOSED atomically.
|
|
568
|
+
return {
|
|
569
|
+
ok: true,
|
|
570
|
+
decision: {
|
|
571
|
+
kind: 'bounce',
|
|
572
|
+
body: verdict.bounceMessage && verdict.bounceMessage.trim() !== ''
|
|
573
|
+
? verdict.bounceMessage
|
|
574
|
+
: `This issue looks like multiple unrelated concerns — please file ` +
|
|
575
|
+
`separate issues so each can be intaken on its own.`,
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
default:
|
|
579
|
+
// Unreachable from the parsed harness verdict (`parseIntakeVerdict` refuses
|
|
580
|
+
// any other token); an injected decider could still name one (the dead
|
|
581
|
+
// ''prd'' token). It dispatches NOTHING.
|
|
582
|
+
return failed('agent-failed', `Intake decision failed for issue #${issueNumber}: the verdict outcome ` +
|
|
583
|
+
`${JSON.stringify(verdict.outcome)} is not one of ask|task|spec|bounce.`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/** The {@link IntakePlacementInputs} of a run's options. */
|
|
587
|
+
export function intakePlacementInputs(options) {
|
|
588
|
+
return {
|
|
589
|
+
tasksLandIn: options.tasksLandIn,
|
|
590
|
+
untrustedTasksLandIn: options.untrustedTasksLandIn,
|
|
591
|
+
explicitTasksLandIn: options.explicitTasksLandIn,
|
|
592
|
+
specsLandIn: options.specsLandIn,
|
|
593
|
+
untrustedSpecsLandIn: options.untrustedSpecsLandIn,
|
|
594
|
+
explicitSpecsLandIn: options.explicitSpecsLandIn,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* RUNNER-DETERMINISTIC PLACEMENT of an intake document (ADR
|
|
599
|
+
* `untrusted-origin-carries-via-stamp-not-forced-staging`): the folder the
|
|
600
|
+
* document is written into. The SAME shared resolver and precedence chain the
|
|
601
|
+
* tasker uses (`explicit > configured default > built-in staging`); only the
|
|
602
|
+
* lifecycle SLOTS differ between a task (`tasks/backlog` vs `tasks/ready`) and a
|
|
603
|
+
* spec (`specs/proposed` vs `specs/ready`). The `originTrust` stamp selects the
|
|
604
|
+
* untrusted-side configured default (`untrusted*LandIn`) over the trusted one.
|
|
605
|
+
* Safety for an untrusted document landing in `ready` flows through its carried
|
|
606
|
+
* stamp at BUILD / tasking time, not the folder.
|
|
607
|
+
*/
|
|
608
|
+
export function intakePlacementDir(kind, originTrust, placement) {
|
|
609
|
+
const untrusted = originTrust === 'untrusted';
|
|
610
|
+
if (kind === 'task') {
|
|
611
|
+
const configured = untrusted
|
|
612
|
+
? placement.untrustedTasksLandIn
|
|
613
|
+
: placement.tasksLandIn;
|
|
614
|
+
const decision = resolvePlacement({
|
|
615
|
+
explicit: landingToSide(placement.explicitTasksLandIn),
|
|
616
|
+
configuredDefault: landingToSide(configured),
|
|
617
|
+
});
|
|
618
|
+
return placementFolder(TASK_PLACEMENT_SLOTS, decision.choice);
|
|
619
|
+
}
|
|
620
|
+
const configured = untrusted
|
|
621
|
+
? placement.untrustedSpecsLandIn
|
|
622
|
+
: placement.specsLandIn;
|
|
623
|
+
const decision = resolvePlacement({
|
|
624
|
+
explicit: specLandingToSide(placement.explicitSpecsLandIn),
|
|
625
|
+
configuredDefault: specLandingToSide(configured),
|
|
626
|
+
});
|
|
627
|
+
return placementFolder(SPEC_PLACEMENT_SLOTS, decision.choice);
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* The WRITE half of intake (task `ci-split-intake`): act on a decision. An ask
|
|
631
|
+
* posts the question and leaves the issue open; a bounce closes the issue
|
|
632
|
+
* atomically with the bounce text; a task or spec is rendered (the title
|
|
633
|
+
* quoted, the frontmatter re-parsed), written onto a `work/intake-<type>-<slug>`
|
|
634
|
+
* branch and integrated, then the completion comment is posted. A render
|
|
635
|
+
* failure maps onto `agent-failed` (the input came from the agent) with nothing
|
|
636
|
+
* written. Runs no agent.
|
|
637
|
+
*/
|
|
638
|
+
export async function applyIntakeDecision(decision, ctx) {
|
|
639
|
+
const { issueNumber, cwd, issueProvider, seen, env, note } = ctx;
|
|
640
|
+
switch (decision.kind) {
|
|
481
641
|
case 'ask':
|
|
642
|
+
// STAMP the MARKER recording `kind=ask` (non-terminal: the TRIAGE owns
|
|
643
|
+
// that) + the `seen=` delta, so a re-run recognises this as intake's own
|
|
644
|
+
// turn and resumes only on genuine new human input.
|
|
482
645
|
return dispatchComment({
|
|
483
646
|
outcome: 'asked',
|
|
484
647
|
cwd,
|
|
485
648
|
issueNumber,
|
|
486
649
|
issueProvider,
|
|
487
|
-
|
|
488
|
-
// non-empty if the agent left it blank.
|
|
489
|
-
body: verdict.question && verdict.question.trim() !== ''
|
|
490
|
-
? verdict.question
|
|
491
|
-
: `Could you clarify issue #${issueNumber} so it can be acted on?`,
|
|
492
|
-
// STAMP the MARKER recording `kind=ask` (non-terminal — the TRIAGE owns
|
|
493
|
-
// that) + the `seen=` delta, so a re-run recognises this as intake's own
|
|
494
|
-
// turn and resumes only on genuine new human input.
|
|
650
|
+
body: decision.body,
|
|
495
651
|
markerKind: 'ask',
|
|
496
|
-
seen
|
|
652
|
+
seen,
|
|
497
653
|
env,
|
|
498
654
|
note,
|
|
499
655
|
});
|
|
500
656
|
case 'bounce':
|
|
657
|
+
// STAMP `kind=bounced` (TERMINAL: the TRIAGE then SKIPS
|
|
658
|
+
// `already-terminal` on a later human comment) + the `seen=` delta.
|
|
501
659
|
return dispatchComment({
|
|
502
660
|
outcome: 'bounced',
|
|
503
661
|
cwd,
|
|
504
662
|
issueNumber,
|
|
505
663
|
issueProvider,
|
|
506
|
-
|
|
507
|
-
// issues" ask. A bounce is TERMINAL: the issue is CLOSED atomically (this
|
|
508
|
-
// text as the closing comment + reason not planned).
|
|
509
|
-
body: verdict.bounceMessage && verdict.bounceMessage.trim() !== ''
|
|
510
|
-
? verdict.bounceMessage
|
|
511
|
-
: `This issue looks like multiple unrelated concerns — please file ` +
|
|
512
|
-
`separate issues so each can be intaken on its own.`,
|
|
513
|
-
// STAMP `kind=bounced` (TERMINAL — the TRIAGE then SKIPS `already-terminal`
|
|
514
|
-
// on a later human comment) + the `seen=` delta.
|
|
664
|
+
body: decision.body,
|
|
515
665
|
markerKind: 'bounced',
|
|
516
|
-
seen
|
|
666
|
+
seen,
|
|
517
667
|
env,
|
|
518
668
|
note,
|
|
519
669
|
});
|
|
670
|
+
case 'task':
|
|
671
|
+
case 'spec': {
|
|
672
|
+
// RENDER before any git: the renderer quotes the agent title and re-parses
|
|
673
|
+
// its own output, THROWING on anything that does not read back as
|
|
674
|
+
// written. That maps onto `agent-failed` with nothing written and no
|
|
675
|
+
// branch cut.
|
|
676
|
+
let content;
|
|
677
|
+
try {
|
|
678
|
+
content = renderIntakeDocument(decision, {
|
|
679
|
+
issueNumber,
|
|
680
|
+
originTrust: ctx.originTrust,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
catch (err) {
|
|
684
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
685
|
+
const message = `Intake could not render the ${decision.kind} for issue #${issueNumber}: ${detail}`;
|
|
686
|
+
note(message);
|
|
687
|
+
return { exitCode: 1, outcome: 'agent-failed', issueNumber, message };
|
|
688
|
+
}
|
|
689
|
+
return emitIntakeDocument(decision, content, ctx);
|
|
690
|
+
}
|
|
520
691
|
}
|
|
521
692
|
}
|
|
693
|
+
/**
|
|
694
|
+
* Render an intake document from a decision and the TRUSTED issue number and
|
|
695
|
+
* origin-trust stamp ({@link renderBacklogTask} / {@link renderSpec}: the title
|
|
696
|
+
* quoted, every runner-owned key re-parsed), then assert once more, through
|
|
697
|
+
* the shared frontmatter reader, that `origin`, `originTrust`, `slug` and
|
|
698
|
+
* `issue` read back as the trusted values ({@link assertIntakeDocumentStamp}).
|
|
699
|
+
* THROWS on any disagreement. The CI apply job calls this on the verdict it
|
|
700
|
+
* received, so the stamp never comes from the agent job.
|
|
701
|
+
*/
|
|
702
|
+
export function renderIntakeDocument(doc, trusted) {
|
|
703
|
+
const { issueNumber, originTrust } = trusted;
|
|
704
|
+
const rendered = doc.kind === 'task'
|
|
705
|
+
? renderBacklogTask({
|
|
706
|
+
slug: doc.slug,
|
|
707
|
+
title: doc.title,
|
|
708
|
+
body: doc.body,
|
|
709
|
+
issueNumber,
|
|
710
|
+
originTrust,
|
|
711
|
+
})
|
|
712
|
+
: renderSpec({
|
|
713
|
+
slug: doc.slug,
|
|
714
|
+
title: doc.title,
|
|
715
|
+
body: doc.body,
|
|
716
|
+
issueNumber,
|
|
717
|
+
humanOnly: doc.humanOnly,
|
|
718
|
+
needsAnswers: doc.needsAnswers,
|
|
719
|
+
originTrust,
|
|
720
|
+
});
|
|
721
|
+
assertIntakeDocumentStamp(rendered, {
|
|
722
|
+
slug: doc.slug,
|
|
723
|
+
issueNumber,
|
|
724
|
+
originTrust,
|
|
725
|
+
});
|
|
726
|
+
return rendered;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* The stamp check of a rendered intake document: its frontmatter must read back
|
|
730
|
+
* `slug`, `issue`, `origin` and `originTrust` EXACTLY as the trusted values
|
|
731
|
+
* (`origin: issue` + `originTrust` when a stamp is given, both absent
|
|
732
|
+
* otherwise). THROWS a `FrontmatterRenderError` otherwise.
|
|
733
|
+
*/
|
|
734
|
+
export function assertIntakeDocumentStamp(rendered, trusted) {
|
|
735
|
+
assertFrontmatterFields(rendered, {
|
|
736
|
+
slug: trusted.slug,
|
|
737
|
+
issue: String(trusted.issueNumber),
|
|
738
|
+
origin: trusted.originTrust === undefined ? undefined : 'issue',
|
|
739
|
+
originTrust: trusted.originTrust,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Emit a RENDERED intake document: place it (runner-deterministic, from the
|
|
744
|
+
* trusted stamp and the placement inputs), onboard a `work/intake-<type>-<slug>`
|
|
745
|
+
* branch cut from the freshly-fetched `<arbiter>/main`, stage exactly the one
|
|
746
|
+
* document path and integrate it through {@link performIntegration} (`propose`
|
|
747
|
+
* PR, or `merge` with the CAS loop), then post the completion comment. The
|
|
748
|
+
* `intake-` producer prefix keeps the branch distinct from a later `do
|
|
749
|
+
* task:<slug>` build branch for the same slug.
|
|
750
|
+
*/
|
|
751
|
+
export async function emitIntakeDocument(doc, content, ctx) {
|
|
752
|
+
const { issueNumber, cwd, arbiter, issueProvider, seen, env, note } = ctx;
|
|
753
|
+
const { kind, slug, title } = doc;
|
|
754
|
+
const relPath = `${intakePlacementDir(kind, ctx.originTrust, ctx.placement)}/${slug}.md`;
|
|
755
|
+
await switchToWorkBranch(cwd, arbiter, kind, slug, env);
|
|
756
|
+
const core = await performIntegration({
|
|
757
|
+
cwd,
|
|
758
|
+
arbiter,
|
|
759
|
+
slug,
|
|
760
|
+
// `source`/`recovering` are task-shaped and IGNORED when `lifecycle` is set.
|
|
761
|
+
source: 'in-progress',
|
|
762
|
+
recovering: false,
|
|
763
|
+
// An intake-emitted document has no `verify` floor of its own (it is a new
|
|
764
|
+
// backlog item / spec, not a build); skip the acceptance gate, exactly as
|
|
765
|
+
// the tasking transition does.
|
|
766
|
+
skipVerify: true,
|
|
767
|
+
// The EXPLICITLY-chosen per-type mode proceeds as-is (`merge` IS the
|
|
768
|
+
// auto-land mode, never downgraded).
|
|
769
|
+
mode: ctx.integration[kind],
|
|
770
|
+
// The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`).
|
|
771
|
+
mergeRetries: ctx.mergeRetries,
|
|
772
|
+
...(ctx.mergeJitterMs === undefined
|
|
773
|
+
? {}
|
|
774
|
+
: { mergeJitterMs: ctx.mergeJitterMs }),
|
|
775
|
+
noPR: ctx.noPR,
|
|
776
|
+
providerInstance: ctx.providerInstance,
|
|
777
|
+
type: 'feat',
|
|
778
|
+
// A short PR body so an intake PR is never blank (`gh --fill`): it names the
|
|
779
|
+
// source issue WITHOUT a closing keyword (the issue must outlive the merge).
|
|
780
|
+
body: composeIntakeProposeBody({ kind, issueNumber }),
|
|
781
|
+
lifecycle: {
|
|
782
|
+
// The document IS the title source. Pass the DRAFTED title EXPLICITLY:
|
|
783
|
+
// `stage()` writes the file AFTER the core reads the title, so a
|
|
784
|
+
// `titlePath` read would race the write. `titlePath` stays set (the
|
|
785
|
+
// lifecycle contract requires it) but is IGNORED while `title` is present.
|
|
786
|
+
titlePath: join(cwd, relPath),
|
|
787
|
+
title,
|
|
788
|
+
commitTag: 'intake',
|
|
789
|
+
// The document RESTS at its placed path (staged or pool), so point the
|
|
790
|
+
// reviewer at THAT file, never the task-build `work/tasks/done/` default.
|
|
791
|
+
proposeHeader: proposePointer(kind === 'task' ? 'Task' : 'Spec', relPath),
|
|
792
|
+
stage: () => stageIntakeContent({ cwd, relPath, content, env }),
|
|
793
|
+
},
|
|
794
|
+
env,
|
|
795
|
+
note,
|
|
796
|
+
});
|
|
797
|
+
return integrationToIntakeResult(core, {
|
|
798
|
+
issueNumber,
|
|
799
|
+
slug,
|
|
800
|
+
relPath,
|
|
801
|
+
kind,
|
|
802
|
+
cwd,
|
|
803
|
+
issueProvider,
|
|
804
|
+
seen,
|
|
805
|
+
env,
|
|
806
|
+
note,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* The propose PR prose of an intake emit (task
|
|
811
|
+
* `propose-pr-bodies-point-at-the-real-item`); the shared core puts the
|
|
812
|
+
* document pointer above it. It names the source issue as a plain `#<n>`
|
|
813
|
+
* reference and deliberately uses NO closing keyword (`closes`, `fixes`,
|
|
814
|
+
* `resolves`, ...): merging an intake PR only files the task or spec, and the
|
|
815
|
+
* issue stays open until that work is done (the close job owns closing it).
|
|
816
|
+
*/
|
|
817
|
+
export function composeIntakeProposeBody(params) {
|
|
818
|
+
const { kind, issueNumber } = params;
|
|
819
|
+
return (`Intake drafted this ${kind} from issue #${issueNumber}. Merging this PR ` +
|
|
820
|
+
`files the ${kind}; it leaves the issue open.`);
|
|
821
|
+
}
|
|
522
822
|
/**
|
|
523
823
|
* DISPATCH the `ask` / `bounce` outcomes — the SHARED comment band, which now
|
|
524
824
|
* BRANCHES on the outcome:
|
|
@@ -593,275 +893,6 @@ async function dispatchComment(params) {
|
|
|
593
893
|
message,
|
|
594
894
|
};
|
|
595
895
|
}
|
|
596
|
-
/**
|
|
597
|
-
* DISPATCH the `task` outcome: derive a content-derived slug, write
|
|
598
|
-
* `work/backlog/<slug>.md` (`covers: []`, NO `spec:`) carrying `issue: N` (the
|
|
599
|
-
* lone-task closure link, NOT `Fixes #N`), and integrate via {@link
|
|
600
|
-
* performIntegration}. The runner owns the git: it onboards a
|
|
601
|
-
* `work/<slug>` branch off fresh `<arbiter>/main`, then the lifecycle `stage`
|
|
602
|
-
* writes + stages the task and the band commits + rebases + integrates it. The
|
|
603
|
-
* agent did NO git/seam ops.
|
|
604
|
-
*/
|
|
605
|
-
async function dispatchTask(params) {
|
|
606
|
-
const { verdict, issueNumber, cwd, arbiter, integration, mergeRetries, originTrust, noPR, tasksLandIn, untrustedTasksLandIn, explicitTasksLandIn, providerInstance, issueProvider, reviewTask, seen, env, agentEnv, note, } = params;
|
|
607
|
-
// A content-derived slug — NEVER a counter (spec `issue-intake` US #8). Prefer the drafted
|
|
608
|
-
// `taskSlug`, else derive from the drafted title; sanitise either through
|
|
609
|
-
// `paramCase` so the filename + frontmatter slug are well-formed.
|
|
610
|
-
const slug = resolveSlug(verdict);
|
|
611
|
-
if (slug === '') {
|
|
612
|
-
const message = `Intake produced a 'task' verdict for issue #${issueNumber} with no usable ` +
|
|
613
|
-
`slug/title to derive a content-derived slug from (never a counter).`;
|
|
614
|
-
note(message);
|
|
615
|
-
return { exitCode: 1, outcome: 'usage-error', issueNumber, message };
|
|
616
|
-
}
|
|
617
|
-
// RUNNER-DETERMINISTIC PLACEMENT (task `intake-task-placement-symmetry`; the
|
|
618
|
-
// untrusted-forces-staging rung RETIRED by ADR
|
|
619
|
-
// `untrusted-origin-carries-via-stamp-not-forced-staging`). Resolve which
|
|
620
|
-
// folder the runner writes the intake-authored task into BEFORE handing it to
|
|
621
|
-
// the shared integrate band — the SAME precedence chain, the SAME shared
|
|
622
|
-
// resolver, and (via `TASK_PLACEMENT_SLOTS` + `landingToSide` reused from
|
|
623
|
-
// `tasking.ts`) the SAME task slots the tasker uses; only the intake dispatch
|
|
624
|
-
// differs from the spec twin (`dispatchSpec`) in its lifecycle SLOTS. THIS
|
|
625
|
-
// caller selects the trusted-vs-untrusted configured default by reading the
|
|
626
|
-
// `originTrust` stamp: an untrusted task selects `untrustedTasksLandIn`
|
|
627
|
-
// (default staging; `ready` when configured), a trusted/unset task selects
|
|
628
|
-
// `tasksLandIn`. The agent (the intake decider) never influences placement.
|
|
629
|
-
// Net vs the old hardcoded `tasks-ready`: an untrusted-author task now MERGES
|
|
630
|
-
// a document to `main` in `backlog` (by default) carrying the stamp instead of
|
|
631
|
-
// opening a document PR; safety for a `ready`-landed untrusted task flows
|
|
632
|
-
// through its carried stamp at BUILD time (forces a code PR), not the folder.
|
|
633
|
-
const configuredTasksLanding = originTrust === 'untrusted' ? untrustedTasksLandIn : tasksLandIn;
|
|
634
|
-
const placementDecision = resolvePlacement({
|
|
635
|
-
explicit: landingToSide(explicitTasksLandIn),
|
|
636
|
-
configuredDefault: landingToSide(configuredTasksLanding),
|
|
637
|
-
});
|
|
638
|
-
const placementDir = placementFolder(TASK_PLACEMENT_SLOTS, placementDecision.choice);
|
|
639
|
-
const relPath = `${placementDir}/${slug}.md`;
|
|
640
|
-
// BOUNDED INTERNAL REVIEW (observation
|
|
641
|
-
// `intake-lone-task-skips-adversarial-review-the-prd-path-gets`, rulings A/B/C):
|
|
642
|
-
// the `do spec:` path gets `runTaskReviewLoop`; the lone-TASK path got NOTHING.
|
|
643
|
-
// AFTER the `task` verdict and BEFORE the write/integrate, run a bounded (3-round,
|
|
644
|
-
// HARD-CAPPED) adversarial self-review on the SINGLE drafted task. It mutates the
|
|
645
|
-
// candidate body IN MEMORY (no `work/backlog/` write pre-convergence). A launch/
|
|
646
|
-
// parse failure THROWS — `decideAndDispatch`'s try/catch maps it onto `agent-failed`
|
|
647
|
-
// (never a silent emit of the un-reviewed task).
|
|
648
|
-
let review;
|
|
649
|
-
try {
|
|
650
|
-
review = await runLoneTaskReview({
|
|
651
|
-
slug,
|
|
652
|
-
issueNumber,
|
|
653
|
-
draftTitle: verdict.taskTitle ?? slug,
|
|
654
|
-
draftBody: verdict.taskBody,
|
|
655
|
-
gate: reviewTask,
|
|
656
|
-
cwd,
|
|
657
|
-
// The review AGENT launches AMBIENT (never the identity-scoped env).
|
|
658
|
-
env: agentEnv,
|
|
659
|
-
note,
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
catch (err) {
|
|
663
|
-
// A review-agent launch/parse FAILURE DEGRADES honestly onto the EXISTING
|
|
664
|
-
// `agent-failed` outcome (exit 1) — NEVER a silent emit of the un-reviewed
|
|
665
|
-
// task. The SAME try/catch discipline the decision step uses.
|
|
666
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
667
|
-
const message = `Intake lone-task review failed for issue #${issueNumber}: ${detail}`;
|
|
668
|
-
note(message);
|
|
669
|
-
return { exitCode: 1, outcome: 'agent-failed', issueNumber, message };
|
|
670
|
-
}
|
|
671
|
-
if (review.outcome === 'non-converge') {
|
|
672
|
-
// NON-CONVERGE (ruling C): FLIP the verdict TASK→ASK, reusing the EXISTING
|
|
673
|
-
// `asked` outcome + `kind=ask` marker. The ASK comment carries BOTH the proposed
|
|
674
|
-
// task DRAFT and the open question(s) in its BODY (NOT a new marker kind) — the
|
|
675
|
-
// human reacts to a concrete draft, strictly richer than a blank-question ask.
|
|
676
|
-
// NEVER write `work/backlog/<slug>.md`; NEVER silently emit the under-refined
|
|
677
|
-
// task. The next intake run resumes via the already-built triage gate.
|
|
678
|
-
note(`Intake's lone-task review did not converge for issue #${issueNumber} ` +
|
|
679
|
-
`(${review.passes} round(s)); flipping TASK→ASK with the draft + open ` +
|
|
680
|
-
`question(s) in the comment body.`);
|
|
681
|
-
return dispatchComment({
|
|
682
|
-
outcome: 'asked',
|
|
683
|
-
cwd,
|
|
684
|
-
issueNumber,
|
|
685
|
-
issueProvider,
|
|
686
|
-
body: composeLoneTaskAskComment({
|
|
687
|
-
issueNumber,
|
|
688
|
-
slug,
|
|
689
|
-
draftTitle: review.title,
|
|
690
|
-
draftBody: review.body,
|
|
691
|
-
questions: review.questions,
|
|
692
|
-
}),
|
|
693
|
-
markerKind: 'ask',
|
|
694
|
-
seen,
|
|
695
|
-
env,
|
|
696
|
-
note,
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
// CONVERGED: the (possibly edited) task is emitted via the EXISTING write/integrate
|
|
700
|
-
// path below + the existing `task created` completion comment. The refined body
|
|
701
|
-
// replaces the agent's first draft.
|
|
702
|
-
const reviewedBody = review.body;
|
|
703
|
-
// ONBOARD the task write onto a `work/intake-task-<slug>` branch cut from the
|
|
704
|
-
// freshly-fetched `<arbiter>/main` (the SAME runner-owns-git discipline the
|
|
705
|
-
// tasking path uses): the lifecycle `stage` writes the file ON THIS BRANCH and
|
|
706
|
-
// the shared integrate core (`--propose` PR / `--merge` main) lands it. The
|
|
707
|
-
// intake- producer prefix keeps it distinct from a later `do task:<slug>`
|
|
708
|
-
// build branch for the same slug. The agent ran no git.
|
|
709
|
-
await switchToWorkBranch(cwd, arbiter, 'task', slug, env);
|
|
710
|
-
const taskContent = renderBacklogTask({
|
|
711
|
-
slug,
|
|
712
|
-
title: review.title,
|
|
713
|
-
body: reviewedBody,
|
|
714
|
-
issueNumber,
|
|
715
|
-
originTrust,
|
|
716
|
-
});
|
|
717
|
-
const core = await performIntegration({
|
|
718
|
-
cwd,
|
|
719
|
-
arbiter,
|
|
720
|
-
slug,
|
|
721
|
-
// `source`/`recovering` are task-shaped and IGNORED when `lifecycle` is set.
|
|
722
|
-
source: 'in-progress',
|
|
723
|
-
recovering: false,
|
|
724
|
-
// An intake-emitted task has no `verify` floor of its own (it is a new
|
|
725
|
-
// backlog item, not a build); skip the acceptance gate, exactly as the
|
|
726
|
-
// tasking transition does.
|
|
727
|
-
skipVerify: true,
|
|
728
|
-
// Default `propose` (the per-outcome KNOBS are a later task). The
|
|
729
|
-
// EXPLICITLY-chosen mode proceeds as-is: a future `--merge-task` lands on main
|
|
730
|
-
// (`merge` IS the auto-land mode, never downgraded).
|
|
731
|
-
mode: integration,
|
|
732
|
-
// The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) — the
|
|
733
|
-
// git-alone floor of the cross-job land queue, resolved through the gate-
|
|
734
|
-
// family precedence chain in the CLI and threaded here so intake's lone-task
|
|
735
|
-
// emit lands under the same per-repo cap the build path uses. Unset ⇒ the
|
|
736
|
-
// engine default (byte-for-byte unchanged).
|
|
737
|
-
mergeRetries,
|
|
738
|
-
noPR,
|
|
739
|
-
providerInstance,
|
|
740
|
-
type: 'feat',
|
|
741
|
-
lifecycle: {
|
|
742
|
-
// The emitted task IS the title source. Pass the DRAFTED title EXPLICITLY
|
|
743
|
-
// (not a read-from-path): `stage()` WRITES `work/backlog/<slug>.md` AFTER the
|
|
744
|
-
// core reads the title, so a `titlePath` read would race the write and degrade
|
|
745
|
-
// the commit subject / PR title to the generic fallback. `titlePath` stays set
|
|
746
|
-
// (the lifecycle contract requires it) but is IGNORED while `title` is present.
|
|
747
|
-
titlePath: join(cwd, relPath),
|
|
748
|
-
title: review.title,
|
|
749
|
-
commitTag: 'intake',
|
|
750
|
-
stage: () => stageIntakeContent({ cwd, relPath, content: taskContent, env }),
|
|
751
|
-
},
|
|
752
|
-
env,
|
|
753
|
-
note,
|
|
754
|
-
});
|
|
755
|
-
return integrationToIntakeResult(core, {
|
|
756
|
-
issueNumber,
|
|
757
|
-
slug,
|
|
758
|
-
relPath,
|
|
759
|
-
cwd,
|
|
760
|
-
issueProvider,
|
|
761
|
-
seen,
|
|
762
|
-
env,
|
|
763
|
-
note,
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
/**
|
|
767
|
-
* DISPATCH the `spec` outcome (the SOLE parent-spec outcome after the HARD CUTOVER;
|
|
768
|
-
* the legacy ''prd'' outcome token is GONE): derive a content-derived slug, write the spec file
|
|
769
|
-
* (`work/specs/ready/<slug>.md`) carrying `issue: N` (the loop-closure linkage the
|
|
770
|
-
* close JOB reaches via `task.spec: → spec issue:`; on a fanned spec the number
|
|
771
|
-
* lives ONLY on the spec — a fanned task uses `spec:`, NOT its own `issue:`, which
|
|
772
|
-
* is the lone-task outcome's link) + the gate axes the prompt JUDGED, integrate it
|
|
773
|
-
* via {@link performIntegration}, then STOP. Tasking the emitted spec is the
|
|
774
|
-
* SEPARATE `do spec:` step (NOT done here). A coupled-but-SMALL pair lands here too
|
|
775
|
-
* (the spec vs BOUNCE line is SHARED VISION, not size — the over-bounce guard). The
|
|
776
|
-
* runner owns the git exactly as the task branch does; the agent did NO git/seam ops.
|
|
777
|
-
*/
|
|
778
|
-
async function dispatchSpec(params) {
|
|
779
|
-
const { verdict, issueNumber, cwd, arbiter, integration, mergeRetries, originTrust, noPR, specsLandIn, untrustedSpecsLandIn, explicitSpecsLandIn, providerInstance, issueProvider, seen, env, note, } = params;
|
|
780
|
-
// A content-derived slug — NEVER a counter (spec `issue-intake` US #8). Prefer the drafted
|
|
781
|
-
// `specSlug`, else derive from the drafted title.
|
|
782
|
-
const slug = resolveSpecSlug(verdict);
|
|
783
|
-
if (slug === '') {
|
|
784
|
-
const message = `Intake produced a 'spec' verdict for issue #${issueNumber} with no usable ` +
|
|
785
|
-
`slug/title to derive a content-derived slug from (never a counter).`;
|
|
786
|
-
note(message);
|
|
787
|
-
return { exitCode: 1, outcome: 'usage-error', issueNumber, message };
|
|
788
|
-
}
|
|
789
|
-
// RUNNER-DETERMINISTIC PLACEMENT (task
|
|
790
|
-
// `pre-prd-staging-pool-split-and-untrusted-prd-placement`; the
|
|
791
|
-
// untrusted-forces-staging rung RETIRED by ADR
|
|
792
|
-
// `untrusted-origin-carries-via-stamp-not-forced-staging`). Resolve which
|
|
793
|
-
// folder the runner writes the intake-authored spec into BEFORE handing it to
|
|
794
|
-
// the shared integrate band: the SAME precedence chain the tasker uses
|
|
795
|
-
// (`explicit > configured default > built-in (staging)`), the SAME shared
|
|
796
|
-
// resolver — only the lifecycle SLOTS differ. THIS caller selects the
|
|
797
|
-
// trusted-vs-untrusted configured default by reading the `originTrust` stamp:
|
|
798
|
-
// an untrusted spec selects `untrustedSpecsLandIn` (default staging; `ready`
|
|
799
|
-
// when configured), a trusted/unset spec selects `specsLandIn`. The agent
|
|
800
|
-
// (the intake decider) never influences placement; it returns the verdict and
|
|
801
|
-
// the runner computes the destination from unforgeable inputs. Safety for an
|
|
802
|
-
// untrusted spec landing in `ready` flows through its tasks' carried stamp,
|
|
803
|
-
// not the folder.
|
|
804
|
-
const configuredSpecsLanding = originTrust === 'untrusted' ? untrustedSpecsLandIn : specsLandIn;
|
|
805
|
-
const placementDecision = resolvePlacement({
|
|
806
|
-
explicit: specLandingToSide(explicitSpecsLandIn),
|
|
807
|
-
configuredDefault: specLandingToSide(configuredSpecsLanding),
|
|
808
|
-
});
|
|
809
|
-
const placementDir = placementFolder(SPEC_PLACEMENT_SLOTS, placementDecision.choice);
|
|
810
|
-
const relPath = `${placementDir}/${slug}.md`;
|
|
811
|
-
// ONBOARD onto a `work/intake-spec-<slug>` branch off fresh `<arbiter>/main` —
|
|
812
|
-
// the SAME runner-owns-git discipline the task branch uses; the intake-
|
|
813
|
-
// producer prefix keeps it distinct from a `do spec:<slug>` tasking branch.
|
|
814
|
-
await switchToWorkBranch(cwd, arbiter, 'spec', slug, env);
|
|
815
|
-
const specContent = renderSpec({
|
|
816
|
-
slug,
|
|
817
|
-
title: verdict.specTitle ?? slug,
|
|
818
|
-
body: verdict.specBody,
|
|
819
|
-
issueNumber,
|
|
820
|
-
humanOnly: verdict.specHumanOnly,
|
|
821
|
-
needsAnswers: verdict.specNeedsAnswers,
|
|
822
|
-
originTrust,
|
|
823
|
-
});
|
|
824
|
-
const core = await performIntegration({
|
|
825
|
-
cwd,
|
|
826
|
-
arbiter,
|
|
827
|
-
slug,
|
|
828
|
-
source: 'in-progress',
|
|
829
|
-
recovering: false,
|
|
830
|
-
// An intake-emitted spec has no `verify` floor of its own (it is a new spec,
|
|
831
|
-
// not a build), exactly as the task branch + the tasking transition skip it.
|
|
832
|
-
skipVerify: true,
|
|
833
|
-
mode: integration,
|
|
834
|
-
// The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) — same
|
|
835
|
-
// rationale as the lone-task emit above; threaded so intake's spec emit lands
|
|
836
|
-
// under the same per-repo cap. Unset ⇒ the engine default (byte-for-byte).
|
|
837
|
-
mergeRetries,
|
|
838
|
-
noPR,
|
|
839
|
-
providerInstance,
|
|
840
|
-
type: 'feat',
|
|
841
|
-
lifecycle: {
|
|
842
|
-
// The emitted spec IS the title source. Pass the DRAFTED title EXPLICITLY (same
|
|
843
|
-
// race as the task path: `stage()` writes the spec file AFTER the title
|
|
844
|
-
// read). `titlePath` stays set but is IGNORED while `title` is present.
|
|
845
|
-
titlePath: join(cwd, relPath),
|
|
846
|
-
title: verdict.specTitle ?? slug,
|
|
847
|
-
commitTag: 'intake',
|
|
848
|
-
stage: () => stageIntakeContent({ cwd, relPath, content: specContent, env }),
|
|
849
|
-
},
|
|
850
|
-
env,
|
|
851
|
-
note,
|
|
852
|
-
});
|
|
853
|
-
return integrationToIntakeResult(core, {
|
|
854
|
-
issueNumber,
|
|
855
|
-
slug,
|
|
856
|
-
relPath,
|
|
857
|
-
kind: 'spec',
|
|
858
|
-
cwd,
|
|
859
|
-
issueProvider,
|
|
860
|
-
seen,
|
|
861
|
-
env,
|
|
862
|
-
note,
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
896
|
/**
|
|
866
897
|
* Map the shared integrate band's {@link IntegrationCoreResult} onto the intake
|
|
867
898
|
* {@link IntakeResult}. On `completed` the artifact was written + integrated; a
|
|
@@ -875,9 +906,18 @@ async function integrationToIntakeResult(core, ctx) {
|
|
|
875
906
|
const kind = ctx.kind ?? 'task';
|
|
876
907
|
const artifact = kind === 'spec' ? 'spec' : 'task';
|
|
877
908
|
if (core.outcome === 'completed') {
|
|
909
|
+
// HONEST propose report (task `intake-reports-the-pr-it-actually-opened`):
|
|
910
|
+
// say "opened a PR" ONLY when the provider opened one. A degraded `gh pr
|
|
911
|
+
// create` (e.g. a repository that refuses PR creation by Actions) pushed the
|
|
912
|
+
// branch but opened nothing; the shared core already printed the provider's
|
|
913
|
+
// instruction, and the message names the branch the work is on.
|
|
914
|
+
const notOpened = proposeRequestNotOpened(core.integration);
|
|
878
915
|
const landed = core.integration?.mode === 'merge'
|
|
879
916
|
? 'landed it on the arbiter main'
|
|
880
|
-
:
|
|
917
|
+
: notOpened !== undefined
|
|
918
|
+
? `pushed it on branch ${core.integration?.pushedRef ?? core.branch} ` +
|
|
919
|
+
'but opened NO PR (main untouched; see the provider note above)'
|
|
920
|
+
: 'opened a PR carrying it (main untouched)';
|
|
881
921
|
// Both a lone task and a spec carry `issue: N` as their closure link (the task
|
|
882
922
|
// closes its own issue; a spec is reached via `task.spec: → spec issue:`). On the
|
|
883
923
|
// task/spec path `intake` never closes the issue (CI's close-job does, via the
|
|
@@ -943,10 +983,14 @@ async function integrationToIntakeResult(core, ctx) {
|
|
|
943
983
|
* - reports `task created` / `spec created` framed as CREATED — NEVER "issue
|
|
944
984
|
* resolved/closed" (intake never closes on the task/spec path).
|
|
945
985
|
* - LINKS the artifact by INTEGRATION MODE: the PR `url` in propose, the landed
|
|
946
|
-
* `commit` (the additive {@link IntegrateResult.commit}) in merge.
|
|
947
|
-
*
|
|
986
|
+
* `commit` (the additive {@link IntegrateResult.commit}) in merge. An opened PR
|
|
987
|
+
* whose `url` was unparseable or a failed merge-tip read (no `commit`) simply OMITS the
|
|
948
988
|
* link — the comment still confirms what was created (the artifact is safe on the
|
|
949
989
|
* branch/main regardless). No spec link beyond the slug.
|
|
990
|
+
* - says so when a propose land opened NO PR at all (the provider degraded, e.g. a
|
|
991
|
+
* repository that refuses PR creation by Actions, or a push-only provider): it
|
|
992
|
+
* names the pushed branch and quotes the provider's reason verbatim, instead of
|
|
993
|
+
* reading as if a PR carried the artifact.
|
|
950
994
|
* - carries the FULL intake MARKER via the SHARED {@link stampIntakeMarker} helper
|
|
951
995
|
* (`kind=created slug=<slug> seen=<id>,…`) so the triage's `already-terminal`
|
|
952
996
|
* branch consumes it — the comment cannot re-trigger intake.
|
|
@@ -954,13 +998,21 @@ async function integrationToIntakeResult(core, ctx) {
|
|
|
954
998
|
export function composeIntakeCompletionComment(params) {
|
|
955
999
|
const { kind, slug, integration, seen } = params;
|
|
956
1000
|
const artifact = kind === 'spec' ? 'spec' : 'task';
|
|
1001
|
+
// A propose land that opened NO PR (the provider degraded, or push-only) says
|
|
1002
|
+
// so, names the pushed branch and quotes the provider's reason, instead of
|
|
1003
|
+
// implying a PR (task `intake-reports-the-pr-it-actually-opened`).
|
|
1004
|
+
const notOpened = proposeRequestNotOpened(integration);
|
|
957
1005
|
const link = integration?.mode === 'merge'
|
|
958
1006
|
? integration.commit !== undefined
|
|
959
1007
|
? `\n\nIt landed on \`main\` in commit ${integration.commit}.`
|
|
960
1008
|
: ''
|
|
961
|
-
:
|
|
962
|
-
? `\n\nIt is
|
|
963
|
-
|
|
1009
|
+
: notOpened !== undefined
|
|
1010
|
+
? `\n\nIt is pushed on branch \`${integration.pushedRef}\`, but **no PR ` +
|
|
1011
|
+
`could be opened**, so nothing proposes it for review yet. The ` +
|
|
1012
|
+
`reason reported:\n\n${fenceVerbatim(notOpened)}`
|
|
1013
|
+
: integration?.url !== undefined
|
|
1014
|
+
? `\n\nIt is carried by the PR: ${integration.url}`
|
|
1015
|
+
: '';
|
|
964
1016
|
const body = `Created ${artifact} \`${slug}\` from this issue.${link}\n\n` +
|
|
965
1017
|
`This is an informational update — the issue stays open (it remains in play ` +
|
|
966
1018
|
`until the ${artifact} lands; intake does not change the issue's state).`;
|
|
@@ -968,6 +1020,17 @@ export function composeIntakeCompletionComment(params) {
|
|
|
968
1020
|
// `already-terminal` branch recognises this terminal `created` comment.
|
|
969
1021
|
return stampIntakeMarker(body, { kind: 'created', seen, slug });
|
|
970
1022
|
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Wrap `text` in a fenced code block that renders it VERBATIM: the fence is one
|
|
1025
|
+
* backtick longer than the longest backtick run inside `text` (minimum three), so
|
|
1026
|
+
* a provider instruction carrying inline code or a quoted PR body can never close
|
|
1027
|
+
* the fence early.
|
|
1028
|
+
*/
|
|
1029
|
+
function fenceVerbatim(text) {
|
|
1030
|
+
const longest = Math.max(0, ...Array.from(text.matchAll(/`+/g), (m) => m[0].length));
|
|
1031
|
+
const fence = '`'.repeat(Math.max(3, longest + 1));
|
|
1032
|
+
return `${fence}text\n${text}\n${fence}`;
|
|
1033
|
+
}
|
|
971
1034
|
/**
|
|
972
1035
|
* Post the INFORMATIONAL completion comment for a SUCCESSFUL `task` / `spec`
|
|
973
1036
|
* outcome (this task). It closes the loop the ASK/BOUNCE comments already close
|
|
@@ -1041,6 +1104,31 @@ function resolveSpecSlug(verdict) {
|
|
|
1041
1104
|
: (verdict.specTitle ?? '');
|
|
1042
1105
|
return ensureSafeSlug(paramCase(candidate));
|
|
1043
1106
|
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Refuse an intake verdict whose drafted title cannot be one frontmatter line
|
|
1109
|
+
* (task `intake-frontmatter-title-injection-strips-origin-stamp`). Only the
|
|
1110
|
+
* title the verdict's OUTCOME will render is checked (`taskTitle` for `task`,
|
|
1111
|
+
* `specTitle` for `spec`): it must be one line with no control character and
|
|
1112
|
+
* not blank. An ABSENT title stays legal (the dispatcher falls back to the
|
|
1113
|
+
* slug). THROWS; the caller maps the throw onto `agent-failed`.
|
|
1114
|
+
*/
|
|
1115
|
+
export function assertIntakeVerdictTitle(verdict) {
|
|
1116
|
+
const [field, title] = verdict.outcome === 'task'
|
|
1117
|
+
? ['taskTitle', verdict.taskTitle]
|
|
1118
|
+
: verdict.outcome === 'spec'
|
|
1119
|
+
? ['specTitle', verdict.specTitle]
|
|
1120
|
+
: [undefined, undefined];
|
|
1121
|
+
if (field === undefined || title === undefined) {
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const problem = singleLineScalarProblem(title);
|
|
1125
|
+
if (problem !== undefined) {
|
|
1126
|
+
throw new Error(`intake verdict '${field}' ${problem}.`);
|
|
1127
|
+
}
|
|
1128
|
+
if (title.trim() === '') {
|
|
1129
|
+
throw new Error(`intake verdict '${field}' is blank.`);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1044
1132
|
/**
|
|
1045
1133
|
* Render the backlog task file: the frontmatter (`title`/`slug`/`covers: []`, NO
|
|
1046
1134
|
* `spec:` — its own source of truth, spec `issue-intake` decision table) carrying the lone-task
|
|
@@ -1054,9 +1142,13 @@ function resolveSpecSlug(verdict) {
|
|
|
1054
1142
|
*/
|
|
1055
1143
|
export function renderBacklogTask(params) {
|
|
1056
1144
|
const { slug, title, body, issueNumber, originTrust } = params;
|
|
1145
|
+
// The TITLE is agent-drafted (issue text any user can write): it goes through
|
|
1146
|
+
// the shared quoter, which THROWS on a multi-line / control-character value
|
|
1147
|
+
// (the verdict boundary already refused one; this is the backstop), so it can
|
|
1148
|
+
// never close the fence early and demote the origin-trust stamp below it.
|
|
1057
1149
|
const lines = [
|
|
1058
1150
|
'---',
|
|
1059
|
-
`title: ${title}`,
|
|
1151
|
+
`title: ${quoteYamlScalar(title)}`,
|
|
1060
1152
|
`slug: ${slug}`,
|
|
1061
1153
|
`issue: ${issueNumber}`,
|
|
1062
1154
|
];
|
|
@@ -1081,7 +1173,19 @@ export function renderBacklogTask(params) {
|
|
|
1081
1173
|
acceptanceCriteria: '- [ ] the issue is resolved',
|
|
1082
1174
|
prompt: `Resolve issue #${issueNumber}: ${title}`,
|
|
1083
1175
|
}).trimEnd();
|
|
1084
|
-
|
|
1176
|
+
const rendered = `${frontmatter}\n\n${drafted}\n`;
|
|
1177
|
+
// Re-parse what we rendered: every key is runner-owned, so the frontmatter
|
|
1178
|
+
// must read back EXACTLY these keys and values, else fail loudly.
|
|
1179
|
+
assertFrontmatterFields(rendered, {
|
|
1180
|
+
title,
|
|
1181
|
+
slug,
|
|
1182
|
+
issue: String(issueNumber),
|
|
1183
|
+
origin: originTrust === undefined ? undefined : 'issue',
|
|
1184
|
+
originTrust,
|
|
1185
|
+
covers: '[]',
|
|
1186
|
+
blockedBy: '[]',
|
|
1187
|
+
}, { exact: true });
|
|
1188
|
+
return rendered;
|
|
1085
1189
|
}
|
|
1086
1190
|
/**
|
|
1087
1191
|
* Render the emitted spec file: the frontmatter (`title`/`slug` + the loop-closure
|
|
@@ -1097,9 +1201,11 @@ export function renderBacklogTask(params) {
|
|
|
1097
1201
|
*/
|
|
1098
1202
|
export function renderSpec(params) {
|
|
1099
1203
|
const { slug, title, body, issueNumber, humanOnly, needsAnswers, originTrust } = params;
|
|
1204
|
+
// The agent-drafted TITLE goes through the shared quoter (see
|
|
1205
|
+
// `renderBacklogTask`).
|
|
1100
1206
|
const lines = [
|
|
1101
1207
|
'---',
|
|
1102
|
-
`title: ${title}`,
|
|
1208
|
+
`title: ${quoteYamlScalar(title)}`,
|
|
1103
1209
|
`slug: ${slug}`,
|
|
1104
1210
|
`issue: ${issueNumber}`,
|
|
1105
1211
|
];
|
|
@@ -1133,7 +1239,18 @@ export function renderSpec(params) {
|
|
|
1133
1239
|
solution: '(to be detailed; this spec needs tasking via `do spec:`).',
|
|
1134
1240
|
userStories: `1. As a user, I want issue #${issueNumber} addressed.`,
|
|
1135
1241
|
}).trimEnd();
|
|
1136
|
-
|
|
1242
|
+
const rendered = `${frontmatter}\n\n${drafted}\n`;
|
|
1243
|
+
// Re-parse what we rendered (see `renderBacklogTask`).
|
|
1244
|
+
assertFrontmatterFields(rendered, {
|
|
1245
|
+
title,
|
|
1246
|
+
slug,
|
|
1247
|
+
issue: String(issueNumber),
|
|
1248
|
+
origin: originTrust === undefined ? undefined : 'issue',
|
|
1249
|
+
originTrust,
|
|
1250
|
+
humanOnly: humanOnly === true ? 'true' : undefined,
|
|
1251
|
+
needsAnswers: needsAnswers === true ? 'true' : undefined,
|
|
1252
|
+
}, { exact: true });
|
|
1253
|
+
return rendered;
|
|
1137
1254
|
}
|
|
1138
1255
|
/**
|
|
1139
1256
|
* STAGE the intake artifact content into the index on the `work/<slug>` branch (the
|