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/src/intake.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type {Phase} from './phase.js';
|
|
1
2
|
import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'node:fs';
|
|
2
3
|
import {dirname, join} from 'node:path';
|
|
3
4
|
import {runAsync, type RunResult} from './git.js';
|
|
@@ -6,12 +7,22 @@ import {paramCase} from './brand.js';
|
|
|
6
7
|
import {ensureSafeSlug} from './slug-safety.js';
|
|
7
8
|
import {
|
|
8
9
|
performIntegration,
|
|
10
|
+
proposePointer,
|
|
9
11
|
type IntegrationCoreResult,
|
|
10
12
|
} from './integration-core.js';
|
|
11
|
-
import
|
|
13
|
+
import {
|
|
14
|
+
proposeRequestNotOpened,
|
|
15
|
+
type IntegrateResult,
|
|
16
|
+
type ReviewProvider,
|
|
17
|
+
} from './integrator.js';
|
|
12
18
|
import {integrationFromFlags} from './complete.js';
|
|
13
19
|
import type {IntegrationMode, SpecsLandIn, TasksLandIn} from './config.js';
|
|
14
|
-
import
|
|
20
|
+
import {
|
|
21
|
+
assertFrontmatterFields,
|
|
22
|
+
quoteYamlScalar,
|
|
23
|
+
singleLineScalarProblem,
|
|
24
|
+
type OriginTrust,
|
|
25
|
+
} from './frontmatter.js';
|
|
15
26
|
import {TASK_PLACEMENT_SLOTS, landingToSide} from './tasking.js';
|
|
16
27
|
import {
|
|
17
28
|
placementFolder,
|
|
@@ -393,6 +404,13 @@ export interface PerformIntakeOptions {
|
|
|
393
404
|
env?: NodeJS.ProcessEnv;
|
|
394
405
|
/** Sink for human-readable progress notes. */
|
|
395
406
|
note?: (message: string) => void;
|
|
407
|
+
/**
|
|
408
|
+
* The CI phase this run executes as (`--phase lock|agent|apply`, CI-only;
|
|
409
|
+
* see `phase.ts`). `undefined` ⇒ today's single process. Threaded next to
|
|
410
|
+
* `env` and `note` so the per-path phase splits can branch on it; the phase
|
|
411
|
+
* guards read the process-wide active phase.
|
|
412
|
+
*/
|
|
413
|
+
phase?: Phase;
|
|
396
414
|
}
|
|
397
415
|
|
|
398
416
|
const DEFAULT_ARBITER = 'origin';
|
|
@@ -608,19 +626,20 @@ export async function performIntake(
|
|
|
608
626
|
// silently pushing under an ambient credential. Resolve the arbiter URL softly
|
|
609
627
|
// (a non-zero/unknown URL is skipped — the guard is a no-op without an identity
|
|
610
628
|
// or a resolvable URL).
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
629
|
+
const transportProblem = await intakeTransportProblem(
|
|
630
|
+
options.identity,
|
|
631
|
+
cwd,
|
|
632
|
+
arbiter,
|
|
633
|
+
env,
|
|
634
|
+
);
|
|
635
|
+
if (transportProblem !== undefined) {
|
|
636
|
+
note(transportProblem);
|
|
637
|
+
return {
|
|
638
|
+
exitCode: 1,
|
|
639
|
+
outcome: 'usage-error',
|
|
640
|
+
issueNumber,
|
|
641
|
+
message: transportProblem,
|
|
642
|
+
};
|
|
624
643
|
}
|
|
625
644
|
|
|
626
645
|
// 1. READ the issue + thread via the seam (the core never imports `gh`; only the
|
|
@@ -823,6 +842,12 @@ function createLockReleaser(params: {
|
|
|
823
842
|
* Split out so the lock release is a clean `try`/`finally` in the caller (the lock
|
|
824
843
|
* MUST release on every terminal path — success or handled failure). The agent
|
|
825
844
|
* DRAFTS only; the runner owns every git/seam side-effect here.
|
|
845
|
+
*
|
|
846
|
+
* It composes the SAME two halves the CI phases run in two jobs (task
|
|
847
|
+
* `ci-split-intake`, `ci-phase-intake.ts`): {@link decideIntake} (the agent half:
|
|
848
|
+
* the decision agent and the lone-task review, no write) and
|
|
849
|
+
* {@link applyIntakeDecision} (the write half: the comment, the close, or the
|
|
850
|
+
* document render + integrate + completion comment).
|
|
826
851
|
*/
|
|
827
852
|
async function decideAndDispatch(
|
|
828
853
|
options: PerformIntakeOptions,
|
|
@@ -839,10 +864,6 @@ async function decideAndDispatch(
|
|
|
839
864
|
): Promise<IntakeResult> {
|
|
840
865
|
const {arbiter, issueProvider, note} = ctx;
|
|
841
866
|
const issueNumber = issue.number;
|
|
842
|
-
// `env` here is the identity-scoped GIT/provider env (the runner's `gh`/git
|
|
843
|
-
// ops). The AGENT launches (decision agent, lone-task review) use the AMBIENT
|
|
844
|
-
// `options.env` — an agent must not act as the bot.
|
|
845
|
-
const env = ctx.gitEnv;
|
|
846
867
|
|
|
847
868
|
// TRIAGE (deterministic, under the lock, BEFORE the prompt): decide whether to run
|
|
848
869
|
// the decision at all, built ENTIRELY on intake's own MARKER on the thread (no
|
|
@@ -853,163 +874,617 @@ async function decideAndDispatch(
|
|
|
853
874
|
// is excluded from the human-comment check by construction.
|
|
854
875
|
const triage = triageIntake(comments);
|
|
855
876
|
if (triage.action === 'skip') {
|
|
856
|
-
const message =
|
|
857
|
-
triage.outcome === 'no-new-input'
|
|
858
|
-
? `Intake of issue #${issueNumber} found nothing new: it has the last word ` +
|
|
859
|
-
`on the thread and has already seen every human comment up to it; doing ` +
|
|
860
|
-
`nothing (the decision prompt did not run).`
|
|
861
|
-
: `Intake of issue #${issueNumber} skipped: the issue was already ` +
|
|
862
|
-
`transformed (a terminal intake marker is on the thread); a later human ` +
|
|
863
|
-
`comment does not re-open it (the decision prompt did not run).`;
|
|
877
|
+
const message = intakeSkipMessage(issueNumber, triage.outcome);
|
|
864
878
|
note(message);
|
|
865
879
|
return {exitCode: 0, outcome: triage.outcome, issueNumber, message};
|
|
866
880
|
}
|
|
867
881
|
|
|
868
|
-
// DECIDE: prompt → VERDICT. The agent DRAFTS only (no
|
|
869
|
-
//
|
|
870
|
-
|
|
882
|
+
// DECIDE: prompt → VERDICT (+ the lone-task review). The agent DRAFTS only (no
|
|
883
|
+
// git, no seam ops).
|
|
884
|
+
const decided = await decideIntake(
|
|
885
|
+
options,
|
|
886
|
+
cwd,
|
|
887
|
+
issue,
|
|
888
|
+
comments,
|
|
889
|
+
triage,
|
|
890
|
+
note,
|
|
891
|
+
);
|
|
892
|
+
if (!decided.ok) {
|
|
893
|
+
return decided.result;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// DISPATCH on the decision. The runner owns every git/seam side-effect (the
|
|
897
|
+
// in-band boundary). PER-OUTCOME integration (spec `issue-intake` US #9): the
|
|
898
|
+
// resolved mode is keyed on the runtime artifact TYPE. The per-run `seen=`
|
|
899
|
+
// DELTA (the HUMAN comment ids intake READ this run, excluding its own
|
|
900
|
+
// marker-comments + already-seen ids) rides every comment intake posts.
|
|
901
|
+
return applyIntakeDecision(decided.decision, {
|
|
902
|
+
issueNumber,
|
|
903
|
+
cwd,
|
|
904
|
+
arbiter,
|
|
905
|
+
issueProvider,
|
|
906
|
+
integration: options.integration ?? {task: 'propose', spec: 'propose'},
|
|
907
|
+
mergeRetries: options.mergeRetries,
|
|
908
|
+
// The origin-trust STAMP, passed IN (not resolved here). Unset ⇒ unstamped
|
|
909
|
+
// (a local intake ⇒ human/trusted).
|
|
910
|
+
originTrust: options.originTrust,
|
|
911
|
+
noPR: options.noPR,
|
|
912
|
+
placement: intakePlacementInputs(options),
|
|
913
|
+
providerInstance: options.providerInstance,
|
|
914
|
+
seen: computeSeenDelta(comments),
|
|
915
|
+
// The identity-scoped GIT/provider env (the `gh` ops, push, PR).
|
|
916
|
+
env: ctx.gitEnv,
|
|
917
|
+
note,
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/** The message of a triage SKIP (the decision prompt did not run). */
|
|
922
|
+
export function intakeSkipMessage(
|
|
923
|
+
issueNumber: number,
|
|
924
|
+
outcome: 'no-new-input' | 'already-terminal',
|
|
925
|
+
): string {
|
|
926
|
+
return outcome === 'no-new-input'
|
|
927
|
+
? `Intake of issue #${issueNumber} found nothing new: it has the last word ` +
|
|
928
|
+
`on the thread and has already seen every human comment up to it; doing ` +
|
|
929
|
+
`nothing (the decision prompt did not run).`
|
|
930
|
+
: `Intake of issue #${issueNumber} skipped: the issue was already ` +
|
|
931
|
+
`transformed (a terminal intake marker is on the thread); a later human ` +
|
|
932
|
+
`comment does not re-open it (the decision prompt did not run).`;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* The push-time transport-coherence guard (identity): the problem message when
|
|
937
|
+
* a configured identity forbids the arbiter's transport, else `undefined`. The
|
|
938
|
+
* arbiter URL is resolved softly (a non-zero/unknown URL is skipped); a no-op
|
|
939
|
+
* without an identity.
|
|
940
|
+
*/
|
|
941
|
+
export async function intakeTransportProblem(
|
|
942
|
+
identity: Identity | undefined,
|
|
943
|
+
cwd: string,
|
|
944
|
+
arbiter: string,
|
|
945
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
946
|
+
): Promise<string | undefined> {
|
|
947
|
+
if (identity === undefined) {
|
|
948
|
+
return undefined;
|
|
949
|
+
}
|
|
950
|
+
const urlRes = await runAsync('git', ['remote', 'get-url', arbiter], cwd, {
|
|
951
|
+
env,
|
|
952
|
+
});
|
|
953
|
+
if (urlRes.status !== 0) {
|
|
954
|
+
return undefined;
|
|
955
|
+
}
|
|
956
|
+
try {
|
|
957
|
+
assertTransportAllowed(identity, urlRes.stdout.trim());
|
|
958
|
+
return undefined;
|
|
959
|
+
} catch (err) {
|
|
960
|
+
return err instanceof Error ? err.message : String(err);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/** A decided task or spec document, before it is rendered (no write yet). */
|
|
965
|
+
export type IntakeDocumentDecision =
|
|
966
|
+
| {
|
|
967
|
+
kind: 'task';
|
|
968
|
+
/** The content-derived, `slug-safety.ts`-checked slug. */
|
|
969
|
+
slug: string;
|
|
970
|
+
title: string;
|
|
971
|
+
/** The (reviewed) drafted body; absent ⇒ the default scaffold. */
|
|
972
|
+
body: string | undefined;
|
|
973
|
+
}
|
|
974
|
+
| {
|
|
975
|
+
kind: 'spec';
|
|
976
|
+
slug: string;
|
|
977
|
+
title: string;
|
|
978
|
+
body: string | undefined;
|
|
979
|
+
/** The gate axes as the prompt judged them. */
|
|
980
|
+
humanOnly?: boolean;
|
|
981
|
+
needsAnswers?: boolean;
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* What the AGENT half of intake decided ({@link decideIntake}), before any
|
|
986
|
+
* write: the comment text of an ask (a lone-task review that did not converge
|
|
987
|
+
* is an ask carrying the draft) or a bounce, or a document to emit.
|
|
988
|
+
*/
|
|
989
|
+
export type IntakeDecision =
|
|
990
|
+
| {kind: 'ask'; body: string}
|
|
991
|
+
| {kind: 'bounce'; body: string}
|
|
992
|
+
| IntakeDocumentDecision;
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* The AGENT half of intake (task `ci-split-intake`): run the decision agent on the
|
|
996
|
+
* issue + thread, refuse a verdict whose title cannot be one frontmatter line,
|
|
997
|
+
* derive the content-derived slug, and (for a task) run the bounded lone-task
|
|
998
|
+
* review. Performs NO write (no git, no seam op); returns the
|
|
999
|
+
* {@link IntakeDecision} the write half acts on, or the failed
|
|
1000
|
+
* {@link IntakeResult} (`agent-failed`, or `usage-error` for a verdict with no
|
|
1001
|
+
* usable slug). The laptop `intake` and the CI agent job both run it.
|
|
1002
|
+
*/
|
|
1003
|
+
export async function decideIntake(
|
|
1004
|
+
options: PerformIntakeOptions,
|
|
1005
|
+
cwd: string,
|
|
1006
|
+
issue: Issue,
|
|
1007
|
+
comments: IssueComment[],
|
|
1008
|
+
triage: IntakeTriageDecision | undefined,
|
|
1009
|
+
note: (message: string) => void,
|
|
1010
|
+
): Promise<
|
|
1011
|
+
{ok: true; decision: IntakeDecision} | {ok: false; result: IntakeResult}
|
|
1012
|
+
> {
|
|
1013
|
+
const issueNumber = issue.number;
|
|
1014
|
+
const failed = (
|
|
1015
|
+
outcome: 'agent-failed' | 'usage-error',
|
|
1016
|
+
message: string,
|
|
1017
|
+
): {ok: false; result: IntakeResult} => {
|
|
1018
|
+
// Not noted: every caller returns this result, whose message the CLI
|
|
1019
|
+
// prints as the run's result line (task `ci-phase-logs-each-line-once`).
|
|
1020
|
+
return {ok: false, result: {exitCode: 1, outcome, issueNumber, message}};
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
// DECIDE: prompt → VERDICT. Tests inject a canned verdict (the dispatcher's
|
|
1024
|
+
// testable seam); production wires the harness. The prompt's judgement is not
|
|
1025
|
+
// unit-tested; only the dispatch is.
|
|
871
1026
|
const prompt = buildIntakeDecisionSpec(issue, comments, triage);
|
|
872
1027
|
let verdict: IntakeVerdict;
|
|
873
1028
|
try {
|
|
874
1029
|
verdict = await runDecision(options, cwd, issue, comments, prompt);
|
|
1030
|
+
// The VERDICT BOUNDARY for the drafted title (task
|
|
1031
|
+
// `intake-frontmatter-title-injection-strips-origin-stamp`): a title that is
|
|
1032
|
+
// not one line, holds a control character, or is blank is refused HERE, so
|
|
1033
|
+
// it degrades onto `agent-failed` like any malformed verdict (never a silent
|
|
1034
|
+
// emit). Runs for the injected `decide` seam and the parsed harness verdict.
|
|
1035
|
+
assertIntakeVerdictTitle(verdict);
|
|
875
1036
|
} catch (err) {
|
|
876
1037
|
const detail = err instanceof Error ? err.message : String(err);
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1038
|
+
return failed(
|
|
1039
|
+
'agent-failed',
|
|
1040
|
+
`Intake decision failed for issue #${issueNumber}: ${detail}`,
|
|
1041
|
+
);
|
|
880
1042
|
}
|
|
881
1043
|
|
|
882
|
-
// DISPATCH on the verdict — the FULL four-outcome decision table (spec
|
|
883
|
-
// `issue-intake`). The agent only DRAFTED the verdict; the runner owns every
|
|
884
|
-
// git/seam side-effect below (the in-band boundary): the write + integrate
|
|
885
|
-
// (task/spec) and the `postIssueComment` (ask/bounce).
|
|
886
|
-
//
|
|
887
|
-
// PER-OUTCOME integration (spec `issue-intake` US #9): the resolved mode is keyed on the runtime
|
|
888
|
-
// artifact TYPE — a `task` verdict integrates with the task mode, a `spec`
|
|
889
|
-
// verdict with the spec mode. Unset ⇒ propose for both. ask/bounce never
|
|
890
|
-
// integrate, so the modes are no-ops for them.
|
|
891
|
-
const modes = options.integration ?? {task: 'propose', spec: 'propose'};
|
|
892
|
-
// The per-run `seen=` DELTA (the HUMAN comment ids intake READ this run, excluding
|
|
893
|
-
// its own marker-comments + already-seen ids) the marker records on every comment
|
|
894
|
-
// intake posts — the chain-model primitive the TRIAGE unions into `seenSet`.
|
|
895
|
-
const seenDelta = computeSeenDelta(comments);
|
|
896
1044
|
switch (verdict.outcome) {
|
|
897
|
-
case 'task':
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1045
|
+
case 'task': {
|
|
1046
|
+
// A content-derived slug, NEVER a counter (spec `issue-intake` US #8).
|
|
1047
|
+
const slug = resolveSlug(verdict);
|
|
1048
|
+
if (slug === '') {
|
|
1049
|
+
return failed(
|
|
1050
|
+
'usage-error',
|
|
1051
|
+
`Intake produced a 'task' verdict for issue #${issueNumber} with no usable ` +
|
|
1052
|
+
`slug/title to derive a content-derived slug from (never a counter).`,
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
// BOUNDED INTERNAL REVIEW (observation
|
|
1056
|
+
// `intake-lone-task-skips-adversarial-review-the-prd-path-gets`, rulings
|
|
1057
|
+
// A/B/C): AFTER the `task` verdict and BEFORE the write/integrate, run a
|
|
1058
|
+
// bounded (3-round, HARD-CAPPED) adversarial self-review on the SINGLE
|
|
1059
|
+
// drafted task. It mutates the candidate body IN MEMORY. A launch/parse
|
|
1060
|
+
// failure degrades onto `agent-failed` (never a silent emit of the
|
|
1061
|
+
// un-reviewed task).
|
|
1062
|
+
let review: LoneTaskReviewResult;
|
|
1063
|
+
try {
|
|
1064
|
+
review = await runLoneTaskReview({
|
|
1065
|
+
slug,
|
|
1066
|
+
issueNumber,
|
|
1067
|
+
draftTitle: verdict.taskTitle ?? slug,
|
|
1068
|
+
draftBody: verdict.taskBody,
|
|
1069
|
+
gate: resolveLoneTaskReviewGate(options),
|
|
1070
|
+
cwd,
|
|
1071
|
+
// The review AGENT launches AMBIENT (never the identity-scoped env).
|
|
1072
|
+
env: options.env,
|
|
1073
|
+
note,
|
|
1074
|
+
});
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1077
|
+
return failed(
|
|
1078
|
+
'agent-failed',
|
|
1079
|
+
`Intake lone-task review failed for issue #${issueNumber}: ${detail}`,
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
if (review.outcome === 'non-converge') {
|
|
1083
|
+
// NON-CONVERGE (ruling C): FLIP the verdict TASK→ASK, reusing the EXISTING
|
|
1084
|
+
// `asked` outcome + `kind=ask` marker. The ASK comment carries BOTH the
|
|
1085
|
+
// proposed task DRAFT and the open question(s) in its BODY. NEVER write
|
|
1086
|
+
// the task; the next intake run resumes via the triage gate.
|
|
1087
|
+
note(
|
|
1088
|
+
`Intake's lone-task review did not converge for issue #${issueNumber} ` +
|
|
1089
|
+
`(${review.passes} round(s)); flipping TASK→ASK with the draft + open ` +
|
|
1090
|
+
`question(s) in the comment body.`,
|
|
1091
|
+
);
|
|
1092
|
+
return {
|
|
1093
|
+
ok: true,
|
|
1094
|
+
decision: {
|
|
1095
|
+
kind: 'ask',
|
|
1096
|
+
body: composeLoneTaskAskComment({
|
|
1097
|
+
issueNumber,
|
|
1098
|
+
slug,
|
|
1099
|
+
draftTitle: review.title,
|
|
1100
|
+
draftBody: review.body,
|
|
1101
|
+
questions: review.questions,
|
|
1102
|
+
}),
|
|
1103
|
+
},
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
// CONVERGED: the (possibly edited) task is emitted; the refined body
|
|
1107
|
+
// replaces the agent's first draft.
|
|
1108
|
+
return {
|
|
1109
|
+
ok: true,
|
|
1110
|
+
decision: {kind: 'task', slug, title: review.title, body: review.body},
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
// The `spec` outcome is the parent-spec verdict. HARD CUTOVER: the legacy
|
|
1114
|
+
// ''prd'' outcome case is GONE.
|
|
1115
|
+
case 'spec': {
|
|
1116
|
+
const slug = resolveSpecSlug(verdict);
|
|
1117
|
+
if (slug === '') {
|
|
1118
|
+
return failed(
|
|
1119
|
+
'usage-error',
|
|
1120
|
+
`Intake produced a 'spec' verdict for issue #${issueNumber} with no usable ` +
|
|
1121
|
+
`slug/title to derive a content-derived slug from (never a counter).`,
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
return {
|
|
1125
|
+
ok: true,
|
|
1126
|
+
decision: {
|
|
1127
|
+
kind: 'spec',
|
|
1128
|
+
slug,
|
|
1129
|
+
title: verdict.specTitle ?? slug,
|
|
1130
|
+
body: verdict.specBody,
|
|
1131
|
+
humanOnly: verdict.specHumanOnly,
|
|
1132
|
+
needsAnswers: verdict.specNeedsAnswers,
|
|
1133
|
+
},
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
case 'ask':
|
|
1137
|
+
// The drafted clarifying question; a thin fallback keeps the comment
|
|
1138
|
+
// non-empty if the agent left it blank.
|
|
1139
|
+
return {
|
|
1140
|
+
ok: true,
|
|
1141
|
+
decision: {
|
|
1142
|
+
kind: 'ask',
|
|
1143
|
+
body:
|
|
1144
|
+
verdict.question && verdict.question.trim() !== ''
|
|
1145
|
+
? verdict.question
|
|
1146
|
+
: `Could you clarify issue #${issueNumber} so it can be acted on?`,
|
|
1147
|
+
},
|
|
1148
|
+
};
|
|
1149
|
+
case 'bounce':
|
|
1150
|
+
// The drafted bounce message; a thin fallback restates the "file separate
|
|
1151
|
+
// issues" ask. A bounce is TERMINAL: the issue is CLOSED atomically.
|
|
1152
|
+
return {
|
|
1153
|
+
ok: true,
|
|
1154
|
+
decision: {
|
|
1155
|
+
kind: 'bounce',
|
|
1156
|
+
body:
|
|
1157
|
+
verdict.bounceMessage && verdict.bounceMessage.trim() !== ''
|
|
1158
|
+
? verdict.bounceMessage
|
|
1159
|
+
: `This issue looks like multiple unrelated concerns — please file ` +
|
|
1160
|
+
`separate issues so each can be intaken on its own.`,
|
|
1161
|
+
},
|
|
1162
|
+
};
|
|
1163
|
+
default:
|
|
1164
|
+
// Unreachable from the parsed harness verdict (`parseIntakeVerdict` refuses
|
|
1165
|
+
// any other token); an injected decider could still name one (the dead
|
|
1166
|
+
// ''prd'' token). It dispatches NOTHING.
|
|
1167
|
+
return failed(
|
|
1168
|
+
'agent-failed',
|
|
1169
|
+
`Intake decision failed for issue #${issueNumber}: the verdict outcome ` +
|
|
1170
|
+
`${JSON.stringify((verdict as {outcome: unknown}).outcome)} is not one of ask|task|spec|bounce.`,
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* The placement inputs of an intake document: the configured-default rungs
|
|
1177
|
+
* (trusted and untrusted side) and the operator's explicit overrides, per
|
|
1178
|
+
* lifecycle. The agent never influences placement.
|
|
1179
|
+
*/
|
|
1180
|
+
export interface IntakePlacementInputs {
|
|
1181
|
+
tasksLandIn?: TasksLandIn;
|
|
1182
|
+
untrustedTasksLandIn?: TasksLandIn;
|
|
1183
|
+
explicitTasksLandIn?: TasksLandIn;
|
|
1184
|
+
specsLandIn?: SpecsLandIn;
|
|
1185
|
+
untrustedSpecsLandIn?: SpecsLandIn;
|
|
1186
|
+
explicitSpecsLandIn?: SpecsLandIn;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/** The {@link IntakePlacementInputs} of a run's options. */
|
|
1190
|
+
export function intakePlacementInputs(
|
|
1191
|
+
options: PerformIntakeOptions,
|
|
1192
|
+
): IntakePlacementInputs {
|
|
1193
|
+
return {
|
|
1194
|
+
tasksLandIn: options.tasksLandIn,
|
|
1195
|
+
untrustedTasksLandIn: options.untrustedTasksLandIn,
|
|
1196
|
+
explicitTasksLandIn: options.explicitTasksLandIn,
|
|
1197
|
+
specsLandIn: options.specsLandIn,
|
|
1198
|
+
untrustedSpecsLandIn: options.untrustedSpecsLandIn,
|
|
1199
|
+
explicitSpecsLandIn: options.explicitSpecsLandIn,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* RUNNER-DETERMINISTIC PLACEMENT of an intake document (ADR
|
|
1205
|
+
* `untrusted-origin-carries-via-stamp-not-forced-staging`): the folder the
|
|
1206
|
+
* document is written into. The SAME shared resolver and precedence chain the
|
|
1207
|
+
* tasker uses (`explicit > configured default > built-in staging`); only the
|
|
1208
|
+
* lifecycle SLOTS differ between a task (`tasks/backlog` vs `tasks/ready`) and a
|
|
1209
|
+
* spec (`specs/proposed` vs `specs/ready`). The `originTrust` stamp selects the
|
|
1210
|
+
* untrusted-side configured default (`untrusted*LandIn`) over the trusted one.
|
|
1211
|
+
* Safety for an untrusted document landing in `ready` flows through its carried
|
|
1212
|
+
* stamp at BUILD / tasking time, not the folder.
|
|
1213
|
+
*/
|
|
1214
|
+
export function intakePlacementDir(
|
|
1215
|
+
kind: IntakeArtifactType,
|
|
1216
|
+
originTrust: OriginTrust | undefined,
|
|
1217
|
+
placement: IntakePlacementInputs,
|
|
1218
|
+
): string {
|
|
1219
|
+
const untrusted = originTrust === 'untrusted';
|
|
1220
|
+
if (kind === 'task') {
|
|
1221
|
+
const configured = untrusted
|
|
1222
|
+
? placement.untrustedTasksLandIn
|
|
1223
|
+
: placement.tasksLandIn;
|
|
1224
|
+
const decision = resolvePlacement({
|
|
1225
|
+
explicit: landingToSide(placement.explicitTasksLandIn),
|
|
1226
|
+
configuredDefault: landingToSide(configured),
|
|
1227
|
+
});
|
|
1228
|
+
return placementFolder(TASK_PLACEMENT_SLOTS, decision.choice);
|
|
1229
|
+
}
|
|
1230
|
+
const configured = untrusted
|
|
1231
|
+
? placement.untrustedSpecsLandIn
|
|
1232
|
+
: placement.specsLandIn;
|
|
1233
|
+
const decision = resolvePlacement({
|
|
1234
|
+
explicit: specLandingToSide(placement.explicitSpecsLandIn),
|
|
1235
|
+
configuredDefault: specLandingToSide(configured),
|
|
1236
|
+
});
|
|
1237
|
+
return placementFolder(SPEC_PLACEMENT_SLOTS, decision.choice);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/** What the WRITE half of intake ({@link applyIntakeDecision}) acts with. */
|
|
1241
|
+
export interface IntakeWriteContext {
|
|
1242
|
+
issueNumber: number;
|
|
1243
|
+
cwd: string;
|
|
1244
|
+
arbiter: string;
|
|
1245
|
+
/** The issue seam the comments / close / completion comment go through. */
|
|
1246
|
+
issueProvider: IssueProvider;
|
|
1247
|
+
/** The per-type document modes. */
|
|
1248
|
+
integration: IntakeIntegrationModes;
|
|
1249
|
+
/** The resolved cross-job CAS-retry cap (undefined ⇒ engine default). */
|
|
1250
|
+
mergeRetries?: number;
|
|
1251
|
+
/** The origin-trust stamp (unset ⇒ emit unstamped ⇒ human/trusted). */
|
|
1252
|
+
originTrust?: OriginTrust;
|
|
1253
|
+
noPR?: boolean;
|
|
1254
|
+
placement: IntakePlacementInputs;
|
|
1255
|
+
providerInstance?: ReviewProvider;
|
|
1256
|
+
/** The `seen=` ids the marker of every posted comment records. */
|
|
1257
|
+
seen: string[];
|
|
1258
|
+
/** The identity-scoped GIT/provider env (push, PR, comments). */
|
|
1259
|
+
env: NodeJS.ProcessEnv | undefined;
|
|
1260
|
+
note: (message: string) => void;
|
|
1261
|
+
/** The merge-mode CAS-loop jitter (tests pass 0). */
|
|
1262
|
+
mergeJitterMs?: number;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* The WRITE half of intake (task `ci-split-intake`): act on a decision. An ask
|
|
1267
|
+
* posts the question and leaves the issue open; a bounce closes the issue
|
|
1268
|
+
* atomically with the bounce text; a task or spec is rendered (the title
|
|
1269
|
+
* quoted, the frontmatter re-parsed), written onto a `work/intake-<type>-<slug>`
|
|
1270
|
+
* branch and integrated, then the completion comment is posted. A render
|
|
1271
|
+
* failure maps onto `agent-failed` (the input came from the agent) with nothing
|
|
1272
|
+
* written. Runs no agent.
|
|
1273
|
+
*/
|
|
1274
|
+
export async function applyIntakeDecision(
|
|
1275
|
+
decision: IntakeDecision,
|
|
1276
|
+
ctx: IntakeWriteContext,
|
|
1277
|
+
): Promise<IntakeResult> {
|
|
1278
|
+
const {issueNumber, cwd, issueProvider, seen, env, note} = ctx;
|
|
1279
|
+
switch (decision.kind) {
|
|
969
1280
|
case 'ask':
|
|
1281
|
+
// STAMP the MARKER recording `kind=ask` (non-terminal: the TRIAGE owns
|
|
1282
|
+
// that) + the `seen=` delta, so a re-run recognises this as intake's own
|
|
1283
|
+
// turn and resumes only on genuine new human input.
|
|
970
1284
|
return dispatchComment({
|
|
971
1285
|
outcome: 'asked',
|
|
972
1286
|
cwd,
|
|
973
1287
|
issueNumber,
|
|
974
1288
|
issueProvider,
|
|
975
|
-
|
|
976
|
-
// non-empty if the agent left it blank.
|
|
977
|
-
body:
|
|
978
|
-
verdict.question && verdict.question.trim() !== ''
|
|
979
|
-
? verdict.question
|
|
980
|
-
: `Could you clarify issue #${issueNumber} so it can be acted on?`,
|
|
981
|
-
// STAMP the MARKER recording `kind=ask` (non-terminal — the TRIAGE owns
|
|
982
|
-
// that) + the `seen=` delta, so a re-run recognises this as intake's own
|
|
983
|
-
// turn and resumes only on genuine new human input.
|
|
1289
|
+
body: decision.body,
|
|
984
1290
|
markerKind: 'ask',
|
|
985
|
-
seen
|
|
1291
|
+
seen,
|
|
986
1292
|
env,
|
|
987
1293
|
note,
|
|
988
1294
|
});
|
|
989
1295
|
case 'bounce':
|
|
1296
|
+
// STAMP `kind=bounced` (TERMINAL: the TRIAGE then SKIPS
|
|
1297
|
+
// `already-terminal` on a later human comment) + the `seen=` delta.
|
|
990
1298
|
return dispatchComment({
|
|
991
1299
|
outcome: 'bounced',
|
|
992
1300
|
cwd,
|
|
993
1301
|
issueNumber,
|
|
994
1302
|
issueProvider,
|
|
995
|
-
|
|
996
|
-
// issues" ask. A bounce is TERMINAL: the issue is CLOSED atomically (this
|
|
997
|
-
// text as the closing comment + reason not planned).
|
|
998
|
-
body:
|
|
999
|
-
verdict.bounceMessage && verdict.bounceMessage.trim() !== ''
|
|
1000
|
-
? verdict.bounceMessage
|
|
1001
|
-
: `This issue looks like multiple unrelated concerns — please file ` +
|
|
1002
|
-
`separate issues so each can be intaken on its own.`,
|
|
1003
|
-
// STAMP `kind=bounced` (TERMINAL — the TRIAGE then SKIPS `already-terminal`
|
|
1004
|
-
// on a later human comment) + the `seen=` delta.
|
|
1303
|
+
body: decision.body,
|
|
1005
1304
|
markerKind: 'bounced',
|
|
1006
|
-
seen
|
|
1305
|
+
seen,
|
|
1007
1306
|
env,
|
|
1008
1307
|
note,
|
|
1009
1308
|
});
|
|
1309
|
+
case 'task':
|
|
1310
|
+
case 'spec': {
|
|
1311
|
+
// RENDER before any git: the renderer quotes the agent title and re-parses
|
|
1312
|
+
// its own output, THROWING on anything that does not read back as
|
|
1313
|
+
// written. That maps onto `agent-failed` with nothing written and no
|
|
1314
|
+
// branch cut.
|
|
1315
|
+
let content: string;
|
|
1316
|
+
try {
|
|
1317
|
+
content = renderIntakeDocument(decision, {
|
|
1318
|
+
issueNumber,
|
|
1319
|
+
originTrust: ctx.originTrust,
|
|
1320
|
+
});
|
|
1321
|
+
} catch (err) {
|
|
1322
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1323
|
+
const message = `Intake could not render the ${decision.kind} for issue #${issueNumber}: ${detail}`;
|
|
1324
|
+
note(message);
|
|
1325
|
+
return {exitCode: 1, outcome: 'agent-failed', issueNumber, message};
|
|
1326
|
+
}
|
|
1327
|
+
return emitIntakeDocument(decision, content, ctx);
|
|
1328
|
+
}
|
|
1010
1329
|
}
|
|
1011
1330
|
}
|
|
1012
1331
|
|
|
1332
|
+
/**
|
|
1333
|
+
* Render an intake document from a decision and the TRUSTED issue number and
|
|
1334
|
+
* origin-trust stamp ({@link renderBacklogTask} / {@link renderSpec}: the title
|
|
1335
|
+
* quoted, every runner-owned key re-parsed), then assert once more, through
|
|
1336
|
+
* the shared frontmatter reader, that `origin`, `originTrust`, `slug` and
|
|
1337
|
+
* `issue` read back as the trusted values ({@link assertIntakeDocumentStamp}).
|
|
1338
|
+
* THROWS on any disagreement. The CI apply job calls this on the verdict it
|
|
1339
|
+
* received, so the stamp never comes from the agent job.
|
|
1340
|
+
*/
|
|
1341
|
+
export function renderIntakeDocument(
|
|
1342
|
+
doc: IntakeDocumentDecision,
|
|
1343
|
+
trusted: {issueNumber: number; originTrust: OriginTrust | undefined},
|
|
1344
|
+
): string {
|
|
1345
|
+
const {issueNumber, originTrust} = trusted;
|
|
1346
|
+
const rendered =
|
|
1347
|
+
doc.kind === 'task'
|
|
1348
|
+
? renderBacklogTask({
|
|
1349
|
+
slug: doc.slug,
|
|
1350
|
+
title: doc.title,
|
|
1351
|
+
body: doc.body,
|
|
1352
|
+
issueNumber,
|
|
1353
|
+
originTrust,
|
|
1354
|
+
})
|
|
1355
|
+
: renderSpec({
|
|
1356
|
+
slug: doc.slug,
|
|
1357
|
+
title: doc.title,
|
|
1358
|
+
body: doc.body,
|
|
1359
|
+
issueNumber,
|
|
1360
|
+
humanOnly: doc.humanOnly,
|
|
1361
|
+
needsAnswers: doc.needsAnswers,
|
|
1362
|
+
originTrust,
|
|
1363
|
+
});
|
|
1364
|
+
assertIntakeDocumentStamp(rendered, {
|
|
1365
|
+
slug: doc.slug,
|
|
1366
|
+
issueNumber,
|
|
1367
|
+
originTrust,
|
|
1368
|
+
});
|
|
1369
|
+
return rendered;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* The stamp check of a rendered intake document: its frontmatter must read back
|
|
1374
|
+
* `slug`, `issue`, `origin` and `originTrust` EXACTLY as the trusted values
|
|
1375
|
+
* (`origin: issue` + `originTrust` when a stamp is given, both absent
|
|
1376
|
+
* otherwise). THROWS a `FrontmatterRenderError` otherwise.
|
|
1377
|
+
*/
|
|
1378
|
+
export function assertIntakeDocumentStamp(
|
|
1379
|
+
rendered: string,
|
|
1380
|
+
trusted: {
|
|
1381
|
+
slug: string;
|
|
1382
|
+
issueNumber: number;
|
|
1383
|
+
originTrust: OriginTrust | undefined;
|
|
1384
|
+
},
|
|
1385
|
+
): void {
|
|
1386
|
+
assertFrontmatterFields(rendered, {
|
|
1387
|
+
slug: trusted.slug,
|
|
1388
|
+
issue: String(trusted.issueNumber),
|
|
1389
|
+
origin: trusted.originTrust === undefined ? undefined : 'issue',
|
|
1390
|
+
originTrust: trusted.originTrust,
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* Emit a RENDERED intake document: place it (runner-deterministic, from the
|
|
1396
|
+
* trusted stamp and the placement inputs), onboard a `work/intake-<type>-<slug>`
|
|
1397
|
+
* branch cut from the freshly-fetched `<arbiter>/main`, stage exactly the one
|
|
1398
|
+
* document path and integrate it through {@link performIntegration} (`propose`
|
|
1399
|
+
* PR, or `merge` with the CAS loop), then post the completion comment. The
|
|
1400
|
+
* `intake-` producer prefix keeps the branch distinct from a later `do
|
|
1401
|
+
* task:<slug>` build branch for the same slug.
|
|
1402
|
+
*/
|
|
1403
|
+
export async function emitIntakeDocument(
|
|
1404
|
+
doc: IntakeDocumentDecision,
|
|
1405
|
+
content: string,
|
|
1406
|
+
ctx: IntakeWriteContext,
|
|
1407
|
+
): Promise<IntakeResult> {
|
|
1408
|
+
const {issueNumber, cwd, arbiter, issueProvider, seen, env, note} = ctx;
|
|
1409
|
+
const {kind, slug, title} = doc;
|
|
1410
|
+
const relPath = `${intakePlacementDir(kind, ctx.originTrust, ctx.placement)}/${slug}.md`;
|
|
1411
|
+
|
|
1412
|
+
await switchToWorkBranch(cwd, arbiter, kind, slug, env);
|
|
1413
|
+
|
|
1414
|
+
const core = await performIntegration({
|
|
1415
|
+
cwd,
|
|
1416
|
+
arbiter,
|
|
1417
|
+
slug,
|
|
1418
|
+
// `source`/`recovering` are task-shaped and IGNORED when `lifecycle` is set.
|
|
1419
|
+
source: 'in-progress',
|
|
1420
|
+
recovering: false,
|
|
1421
|
+
// An intake-emitted document has no `verify` floor of its own (it is a new
|
|
1422
|
+
// backlog item / spec, not a build); skip the acceptance gate, exactly as
|
|
1423
|
+
// the tasking transition does.
|
|
1424
|
+
skipVerify: true,
|
|
1425
|
+
// The EXPLICITLY-chosen per-type mode proceeds as-is (`merge` IS the
|
|
1426
|
+
// auto-land mode, never downgraded).
|
|
1427
|
+
mode: ctx.integration[kind],
|
|
1428
|
+
// The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`).
|
|
1429
|
+
mergeRetries: ctx.mergeRetries,
|
|
1430
|
+
...(ctx.mergeJitterMs === undefined
|
|
1431
|
+
? {}
|
|
1432
|
+
: {mergeJitterMs: ctx.mergeJitterMs}),
|
|
1433
|
+
noPR: ctx.noPR,
|
|
1434
|
+
providerInstance: ctx.providerInstance,
|
|
1435
|
+
type: 'feat',
|
|
1436
|
+
// A short PR body so an intake PR is never blank (`gh --fill`): it names the
|
|
1437
|
+
// source issue WITHOUT a closing keyword (the issue must outlive the merge).
|
|
1438
|
+
body: composeIntakeProposeBody({kind, issueNumber}),
|
|
1439
|
+
lifecycle: {
|
|
1440
|
+
// The document IS the title source. Pass the DRAFTED title EXPLICITLY:
|
|
1441
|
+
// `stage()` writes the file AFTER the core reads the title, so a
|
|
1442
|
+
// `titlePath` read would race the write. `titlePath` stays set (the
|
|
1443
|
+
// lifecycle contract requires it) but is IGNORED while `title` is present.
|
|
1444
|
+
titlePath: join(cwd, relPath),
|
|
1445
|
+
title,
|
|
1446
|
+
commitTag: 'intake',
|
|
1447
|
+
// The document RESTS at its placed path (staged or pool), so point the
|
|
1448
|
+
// reviewer at THAT file, never the task-build `work/tasks/done/` default.
|
|
1449
|
+
proposeHeader: proposePointer(kind === 'task' ? 'Task' : 'Spec', relPath),
|
|
1450
|
+
stage: () => stageIntakeContent({cwd, relPath, content, env}),
|
|
1451
|
+
},
|
|
1452
|
+
env,
|
|
1453
|
+
note,
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
return integrationToIntakeResult(core, {
|
|
1457
|
+
issueNumber,
|
|
1458
|
+
slug,
|
|
1459
|
+
relPath,
|
|
1460
|
+
kind,
|
|
1461
|
+
cwd,
|
|
1462
|
+
issueProvider,
|
|
1463
|
+
seen,
|
|
1464
|
+
env,
|
|
1465
|
+
note,
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* The propose PR prose of an intake emit (task
|
|
1471
|
+
* `propose-pr-bodies-point-at-the-real-item`); the shared core puts the
|
|
1472
|
+
* document pointer above it. It names the source issue as a plain `#<n>`
|
|
1473
|
+
* reference and deliberately uses NO closing keyword (`closes`, `fixes`,
|
|
1474
|
+
* `resolves`, ...): merging an intake PR only files the task or spec, and the
|
|
1475
|
+
* issue stays open until that work is done (the close job owns closing it).
|
|
1476
|
+
*/
|
|
1477
|
+
export function composeIntakeProposeBody(params: {
|
|
1478
|
+
kind: IntakeArtifactType;
|
|
1479
|
+
issueNumber: number;
|
|
1480
|
+
}): string {
|
|
1481
|
+
const {kind, issueNumber} = params;
|
|
1482
|
+
return (
|
|
1483
|
+
`Intake drafted this ${kind} from issue #${issueNumber}. Merging this PR ` +
|
|
1484
|
+
`files the ${kind}; it leaves the issue open.`
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1013
1488
|
/**
|
|
1014
1489
|
* DISPATCH the `ask` / `bounce` outcomes — the SHARED comment band, which now
|
|
1015
1490
|
* BRANCHES on the outcome:
|
|
@@ -1111,400 +1586,6 @@ async function dispatchComment(params: {
|
|
|
1111
1586
|
};
|
|
1112
1587
|
}
|
|
1113
1588
|
|
|
1114
|
-
/**
|
|
1115
|
-
* DISPATCH the `task` outcome: derive a content-derived slug, write
|
|
1116
|
-
* `work/backlog/<slug>.md` (`covers: []`, NO `spec:`) carrying `issue: N` (the
|
|
1117
|
-
* lone-task closure link, NOT `Fixes #N`), and integrate via {@link
|
|
1118
|
-
* performIntegration}. The runner owns the git: it onboards a
|
|
1119
|
-
* `work/<slug>` branch off fresh `<arbiter>/main`, then the lifecycle `stage`
|
|
1120
|
-
* writes + stages the task and the band commits + rebases + integrates it. The
|
|
1121
|
-
* agent did NO git/seam ops.
|
|
1122
|
-
*/
|
|
1123
|
-
async function dispatchTask(params: {
|
|
1124
|
-
verdict: IntakeVerdict;
|
|
1125
|
-
issueNumber: number;
|
|
1126
|
-
cwd: string;
|
|
1127
|
-
arbiter: string;
|
|
1128
|
-
integration: IntegrationMode;
|
|
1129
|
-
/** The resolved cross-job CAS-retry cap (undefined ⇒ engine default). */
|
|
1130
|
-
mergeRetries: number | undefined;
|
|
1131
|
-
/** The origin-trust stamp passed IN (unset ⇒ emit unstamped ⇒ human/trusted). */
|
|
1132
|
-
originTrust: OriginTrust | undefined;
|
|
1133
|
-
noPR: boolean | undefined;
|
|
1134
|
-
/** The per-repo TRUSTED-side TASK-PLACEMENT default (configured-default rung when the task is trusted/unset). */
|
|
1135
|
-
tasksLandIn: TasksLandIn | undefined;
|
|
1136
|
-
/**
|
|
1137
|
-
* The per-repo UNTRUSTED-side TASK-PLACEMENT default, selected as the
|
|
1138
|
-
* configured-default rung when `originTrust` is `untrusted` (ADR
|
|
1139
|
-
* `untrusted-origin-carries-via-stamp-not-forced-staging`).
|
|
1140
|
-
*/
|
|
1141
|
-
untrustedTasksLandIn: TasksLandIn | undefined;
|
|
1142
|
-
/** The OPERATOR's EXPLICIT task-placement override (the TOP rung). */
|
|
1143
|
-
explicitTasksLandIn: TasksLandIn | undefined;
|
|
1144
|
-
providerInstance: ReviewProvider | undefined;
|
|
1145
|
-
/** The issue seam the completion comment is posted back through (runner-owned). */
|
|
1146
|
-
issueProvider: IssueProvider;
|
|
1147
|
-
/** The bounded lone-task review seam (tests inject a canned verdict; prod: harness). */
|
|
1148
|
-
reviewTask: LoneTaskReviewGate;
|
|
1149
|
-
/** The per-run `seen=` delta of HUMAN comment ids the completion marker records. */
|
|
1150
|
-
seen: string[];
|
|
1151
|
-
/** The identity-scoped GIT/provider env (push, PR, completion comment). */
|
|
1152
|
-
env: NodeJS.ProcessEnv | undefined;
|
|
1153
|
-
/** The AMBIENT env for the lone-task review AGENT launch (never the identity). */
|
|
1154
|
-
agentEnv: NodeJS.ProcessEnv | undefined;
|
|
1155
|
-
note: (message: string) => void;
|
|
1156
|
-
}): Promise<IntakeResult> {
|
|
1157
|
-
const {
|
|
1158
|
-
verdict,
|
|
1159
|
-
issueNumber,
|
|
1160
|
-
cwd,
|
|
1161
|
-
arbiter,
|
|
1162
|
-
integration,
|
|
1163
|
-
mergeRetries,
|
|
1164
|
-
originTrust,
|
|
1165
|
-
noPR,
|
|
1166
|
-
tasksLandIn,
|
|
1167
|
-
untrustedTasksLandIn,
|
|
1168
|
-
explicitTasksLandIn,
|
|
1169
|
-
providerInstance,
|
|
1170
|
-
issueProvider,
|
|
1171
|
-
reviewTask,
|
|
1172
|
-
seen,
|
|
1173
|
-
env,
|
|
1174
|
-
agentEnv,
|
|
1175
|
-
note,
|
|
1176
|
-
} = params;
|
|
1177
|
-
|
|
1178
|
-
// A content-derived slug — NEVER a counter (spec `issue-intake` US #8). Prefer the drafted
|
|
1179
|
-
// `taskSlug`, else derive from the drafted title; sanitise either through
|
|
1180
|
-
// `paramCase` so the filename + frontmatter slug are well-formed.
|
|
1181
|
-
const slug = resolveSlug(verdict);
|
|
1182
|
-
if (slug === '') {
|
|
1183
|
-
const message =
|
|
1184
|
-
`Intake produced a 'task' verdict for issue #${issueNumber} with no usable ` +
|
|
1185
|
-
`slug/title to derive a content-derived slug from (never a counter).`;
|
|
1186
|
-
note(message);
|
|
1187
|
-
return {exitCode: 1, outcome: 'usage-error', issueNumber, message};
|
|
1188
|
-
}
|
|
1189
|
-
// RUNNER-DETERMINISTIC PLACEMENT (task `intake-task-placement-symmetry`; the
|
|
1190
|
-
// untrusted-forces-staging rung RETIRED by ADR
|
|
1191
|
-
// `untrusted-origin-carries-via-stamp-not-forced-staging`). Resolve which
|
|
1192
|
-
// folder the runner writes the intake-authored task into BEFORE handing it to
|
|
1193
|
-
// the shared integrate band — the SAME precedence chain, the SAME shared
|
|
1194
|
-
// resolver, and (via `TASK_PLACEMENT_SLOTS` + `landingToSide` reused from
|
|
1195
|
-
// `tasking.ts`) the SAME task slots the tasker uses; only the intake dispatch
|
|
1196
|
-
// differs from the spec twin (`dispatchSpec`) in its lifecycle SLOTS. THIS
|
|
1197
|
-
// caller selects the trusted-vs-untrusted configured default by reading the
|
|
1198
|
-
// `originTrust` stamp: an untrusted task selects `untrustedTasksLandIn`
|
|
1199
|
-
// (default staging; `ready` when configured), a trusted/unset task selects
|
|
1200
|
-
// `tasksLandIn`. The agent (the intake decider) never influences placement.
|
|
1201
|
-
// Net vs the old hardcoded `tasks-ready`: an untrusted-author task now MERGES
|
|
1202
|
-
// a document to `main` in `backlog` (by default) carrying the stamp instead of
|
|
1203
|
-
// opening a document PR; safety for a `ready`-landed untrusted task flows
|
|
1204
|
-
// through its carried stamp at BUILD time (forces a code PR), not the folder.
|
|
1205
|
-
const configuredTasksLanding =
|
|
1206
|
-
originTrust === 'untrusted' ? untrustedTasksLandIn : tasksLandIn;
|
|
1207
|
-
const placementDecision = resolvePlacement({
|
|
1208
|
-
explicit: landingToSide(explicitTasksLandIn),
|
|
1209
|
-
configuredDefault: landingToSide(configuredTasksLanding),
|
|
1210
|
-
});
|
|
1211
|
-
const placementDir = placementFolder(
|
|
1212
|
-
TASK_PLACEMENT_SLOTS,
|
|
1213
|
-
placementDecision.choice,
|
|
1214
|
-
);
|
|
1215
|
-
const relPath = `${placementDir}/${slug}.md`;
|
|
1216
|
-
|
|
1217
|
-
// BOUNDED INTERNAL REVIEW (observation
|
|
1218
|
-
// `intake-lone-task-skips-adversarial-review-the-prd-path-gets`, rulings A/B/C):
|
|
1219
|
-
// the `do spec:` path gets `runTaskReviewLoop`; the lone-TASK path got NOTHING.
|
|
1220
|
-
// AFTER the `task` verdict and BEFORE the write/integrate, run a bounded (3-round,
|
|
1221
|
-
// HARD-CAPPED) adversarial self-review on the SINGLE drafted task. It mutates the
|
|
1222
|
-
// candidate body IN MEMORY (no `work/backlog/` write pre-convergence). A launch/
|
|
1223
|
-
// parse failure THROWS — `decideAndDispatch`'s try/catch maps it onto `agent-failed`
|
|
1224
|
-
// (never a silent emit of the un-reviewed task).
|
|
1225
|
-
let review: LoneTaskReviewResult;
|
|
1226
|
-
try {
|
|
1227
|
-
review = await runLoneTaskReview({
|
|
1228
|
-
slug,
|
|
1229
|
-
issueNumber,
|
|
1230
|
-
draftTitle: verdict.taskTitle ?? slug,
|
|
1231
|
-
draftBody: verdict.taskBody,
|
|
1232
|
-
gate: reviewTask,
|
|
1233
|
-
cwd,
|
|
1234
|
-
// The review AGENT launches AMBIENT (never the identity-scoped env).
|
|
1235
|
-
env: agentEnv,
|
|
1236
|
-
note,
|
|
1237
|
-
});
|
|
1238
|
-
} catch (err) {
|
|
1239
|
-
// A review-agent launch/parse FAILURE DEGRADES honestly onto the EXISTING
|
|
1240
|
-
// `agent-failed` outcome (exit 1) — NEVER a silent emit of the un-reviewed
|
|
1241
|
-
// task. The SAME try/catch discipline the decision step uses.
|
|
1242
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1243
|
-
const message = `Intake lone-task review failed for issue #${issueNumber}: ${detail}`;
|
|
1244
|
-
note(message);
|
|
1245
|
-
return {exitCode: 1, outcome: 'agent-failed', issueNumber, message};
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
if (review.outcome === 'non-converge') {
|
|
1249
|
-
// NON-CONVERGE (ruling C): FLIP the verdict TASK→ASK, reusing the EXISTING
|
|
1250
|
-
// `asked` outcome + `kind=ask` marker. The ASK comment carries BOTH the proposed
|
|
1251
|
-
// task DRAFT and the open question(s) in its BODY (NOT a new marker kind) — the
|
|
1252
|
-
// human reacts to a concrete draft, strictly richer than a blank-question ask.
|
|
1253
|
-
// NEVER write `work/backlog/<slug>.md`; NEVER silently emit the under-refined
|
|
1254
|
-
// task. The next intake run resumes via the already-built triage gate.
|
|
1255
|
-
note(
|
|
1256
|
-
`Intake's lone-task review did not converge for issue #${issueNumber} ` +
|
|
1257
|
-
`(${review.passes} round(s)); flipping TASK→ASK with the draft + open ` +
|
|
1258
|
-
`question(s) in the comment body.`,
|
|
1259
|
-
);
|
|
1260
|
-
return dispatchComment({
|
|
1261
|
-
outcome: 'asked',
|
|
1262
|
-
cwd,
|
|
1263
|
-
issueNumber,
|
|
1264
|
-
issueProvider,
|
|
1265
|
-
body: composeLoneTaskAskComment({
|
|
1266
|
-
issueNumber,
|
|
1267
|
-
slug,
|
|
1268
|
-
draftTitle: review.title,
|
|
1269
|
-
draftBody: review.body,
|
|
1270
|
-
questions: review.questions,
|
|
1271
|
-
}),
|
|
1272
|
-
markerKind: 'ask',
|
|
1273
|
-
seen,
|
|
1274
|
-
env,
|
|
1275
|
-
note,
|
|
1276
|
-
});
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
// CONVERGED: the (possibly edited) task is emitted via the EXISTING write/integrate
|
|
1280
|
-
// path below + the existing `task created` completion comment. The refined body
|
|
1281
|
-
// replaces the agent's first draft.
|
|
1282
|
-
const reviewedBody = review.body;
|
|
1283
|
-
|
|
1284
|
-
// ONBOARD the task write onto a `work/intake-task-<slug>` branch cut from the
|
|
1285
|
-
// freshly-fetched `<arbiter>/main` (the SAME runner-owns-git discipline the
|
|
1286
|
-
// tasking path uses): the lifecycle `stage` writes the file ON THIS BRANCH and
|
|
1287
|
-
// the shared integrate core (`--propose` PR / `--merge` main) lands it. The
|
|
1288
|
-
// intake- producer prefix keeps it distinct from a later `do task:<slug>`
|
|
1289
|
-
// build branch for the same slug. The agent ran no git.
|
|
1290
|
-
await switchToWorkBranch(cwd, arbiter, 'task', slug, env);
|
|
1291
|
-
|
|
1292
|
-
const taskContent = renderBacklogTask({
|
|
1293
|
-
slug,
|
|
1294
|
-
title: review.title,
|
|
1295
|
-
body: reviewedBody,
|
|
1296
|
-
issueNumber,
|
|
1297
|
-
originTrust,
|
|
1298
|
-
});
|
|
1299
|
-
|
|
1300
|
-
const core = await performIntegration({
|
|
1301
|
-
cwd,
|
|
1302
|
-
arbiter,
|
|
1303
|
-
slug,
|
|
1304
|
-
// `source`/`recovering` are task-shaped and IGNORED when `lifecycle` is set.
|
|
1305
|
-
source: 'in-progress',
|
|
1306
|
-
recovering: false,
|
|
1307
|
-
// An intake-emitted task has no `verify` floor of its own (it is a new
|
|
1308
|
-
// backlog item, not a build); skip the acceptance gate, exactly as the
|
|
1309
|
-
// tasking transition does.
|
|
1310
|
-
skipVerify: true,
|
|
1311
|
-
// Default `propose` (the per-outcome KNOBS are a later task). The
|
|
1312
|
-
// EXPLICITLY-chosen mode proceeds as-is: a future `--merge-task` lands on main
|
|
1313
|
-
// (`merge` IS the auto-land mode, never downgraded).
|
|
1314
|
-
mode: integration,
|
|
1315
|
-
// The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) — the
|
|
1316
|
-
// git-alone floor of the cross-job land queue, resolved through the gate-
|
|
1317
|
-
// family precedence chain in the CLI and threaded here so intake's lone-task
|
|
1318
|
-
// emit lands under the same per-repo cap the build path uses. Unset ⇒ the
|
|
1319
|
-
// engine default (byte-for-byte unchanged).
|
|
1320
|
-
mergeRetries,
|
|
1321
|
-
noPR,
|
|
1322
|
-
providerInstance,
|
|
1323
|
-
type: 'feat',
|
|
1324
|
-
lifecycle: {
|
|
1325
|
-
// The emitted task IS the title source. Pass the DRAFTED title EXPLICITLY
|
|
1326
|
-
// (not a read-from-path): `stage()` WRITES `work/backlog/<slug>.md` AFTER the
|
|
1327
|
-
// core reads the title, so a `titlePath` read would race the write and degrade
|
|
1328
|
-
// the commit subject / PR title to the generic fallback. `titlePath` stays set
|
|
1329
|
-
// (the lifecycle contract requires it) but is IGNORED while `title` is present.
|
|
1330
|
-
titlePath: join(cwd, relPath),
|
|
1331
|
-
title: review.title,
|
|
1332
|
-
commitTag: 'intake',
|
|
1333
|
-
stage: () =>
|
|
1334
|
-
stageIntakeContent({cwd, relPath, content: taskContent, env}),
|
|
1335
|
-
},
|
|
1336
|
-
env,
|
|
1337
|
-
note,
|
|
1338
|
-
});
|
|
1339
|
-
|
|
1340
|
-
return integrationToIntakeResult(core, {
|
|
1341
|
-
issueNumber,
|
|
1342
|
-
slug,
|
|
1343
|
-
relPath,
|
|
1344
|
-
cwd,
|
|
1345
|
-
issueProvider,
|
|
1346
|
-
seen,
|
|
1347
|
-
env,
|
|
1348
|
-
note,
|
|
1349
|
-
});
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
/**
|
|
1353
|
-
* DISPATCH the `spec` outcome (the SOLE parent-spec outcome after the HARD CUTOVER;
|
|
1354
|
-
* the legacy ''prd'' outcome token is GONE): derive a content-derived slug, write the spec file
|
|
1355
|
-
* (`work/specs/ready/<slug>.md`) carrying `issue: N` (the loop-closure linkage the
|
|
1356
|
-
* close JOB reaches via `task.spec: → spec issue:`; on a fanned spec the number
|
|
1357
|
-
* lives ONLY on the spec — a fanned task uses `spec:`, NOT its own `issue:`, which
|
|
1358
|
-
* is the lone-task outcome's link) + the gate axes the prompt JUDGED, integrate it
|
|
1359
|
-
* via {@link performIntegration}, then STOP. Tasking the emitted spec is the
|
|
1360
|
-
* SEPARATE `do spec:` step (NOT done here). A coupled-but-SMALL pair lands here too
|
|
1361
|
-
* (the spec vs BOUNCE line is SHARED VISION, not size — the over-bounce guard). The
|
|
1362
|
-
* runner owns the git exactly as the task branch does; the agent did NO git/seam ops.
|
|
1363
|
-
*/
|
|
1364
|
-
async function dispatchSpec(params: {
|
|
1365
|
-
verdict: IntakeVerdict;
|
|
1366
|
-
issueNumber: number;
|
|
1367
|
-
cwd: string;
|
|
1368
|
-
arbiter: string;
|
|
1369
|
-
integration: IntegrationMode;
|
|
1370
|
-
/** The resolved cross-job CAS-retry cap (undefined ⇒ engine default). */
|
|
1371
|
-
mergeRetries: number | undefined;
|
|
1372
|
-
/** The origin-trust stamp passed IN (unset ⇒ emit unstamped ⇒ human/trusted). */
|
|
1373
|
-
originTrust: OriginTrust | undefined;
|
|
1374
|
-
noPR: boolean | undefined;
|
|
1375
|
-
/** The per-repo TRUSTED-side SPEC-PLACEMENT default (configured-default rung when the spec is trusted/unset). */
|
|
1376
|
-
specsLandIn: SpecsLandIn | undefined;
|
|
1377
|
-
/**
|
|
1378
|
-
* The per-repo UNTRUSTED-side SPEC-PLACEMENT default, selected as the
|
|
1379
|
-
* configured-default rung when `originTrust` is `untrusted` (ADR
|
|
1380
|
-
* `untrusted-origin-carries-via-stamp-not-forced-staging`).
|
|
1381
|
-
*/
|
|
1382
|
-
untrustedSpecsLandIn: SpecsLandIn | undefined;
|
|
1383
|
-
/** The OPERATOR's EXPLICIT spec-placement override (the TOP rung). */
|
|
1384
|
-
explicitSpecsLandIn: SpecsLandIn | undefined;
|
|
1385
|
-
providerInstance: ReviewProvider | undefined;
|
|
1386
|
-
/** The issue seam the completion comment is posted back through (runner-owned). */
|
|
1387
|
-
issueProvider: IssueProvider;
|
|
1388
|
-
/** The per-run `seen=` delta of HUMAN comment ids the completion marker records. */
|
|
1389
|
-
seen: string[];
|
|
1390
|
-
env: NodeJS.ProcessEnv | undefined;
|
|
1391
|
-
note: (message: string) => void;
|
|
1392
|
-
}): Promise<IntakeResult> {
|
|
1393
|
-
const {
|
|
1394
|
-
verdict,
|
|
1395
|
-
issueNumber,
|
|
1396
|
-
cwd,
|
|
1397
|
-
arbiter,
|
|
1398
|
-
integration,
|
|
1399
|
-
mergeRetries,
|
|
1400
|
-
originTrust,
|
|
1401
|
-
noPR,
|
|
1402
|
-
specsLandIn,
|
|
1403
|
-
untrustedSpecsLandIn,
|
|
1404
|
-
explicitSpecsLandIn,
|
|
1405
|
-
providerInstance,
|
|
1406
|
-
issueProvider,
|
|
1407
|
-
seen,
|
|
1408
|
-
env,
|
|
1409
|
-
note,
|
|
1410
|
-
} = params;
|
|
1411
|
-
|
|
1412
|
-
// A content-derived slug — NEVER a counter (spec `issue-intake` US #8). Prefer the drafted
|
|
1413
|
-
// `specSlug`, else derive from the drafted title.
|
|
1414
|
-
const slug = resolveSpecSlug(verdict);
|
|
1415
|
-
if (slug === '') {
|
|
1416
|
-
const message =
|
|
1417
|
-
`Intake produced a 'spec' verdict for issue #${issueNumber} with no usable ` +
|
|
1418
|
-
`slug/title to derive a content-derived slug from (never a counter).`;
|
|
1419
|
-
note(message);
|
|
1420
|
-
return {exitCode: 1, outcome: 'usage-error', issueNumber, message};
|
|
1421
|
-
}
|
|
1422
|
-
// RUNNER-DETERMINISTIC PLACEMENT (task
|
|
1423
|
-
// `pre-prd-staging-pool-split-and-untrusted-prd-placement`; the
|
|
1424
|
-
// untrusted-forces-staging rung RETIRED by ADR
|
|
1425
|
-
// `untrusted-origin-carries-via-stamp-not-forced-staging`). Resolve which
|
|
1426
|
-
// folder the runner writes the intake-authored spec into BEFORE handing it to
|
|
1427
|
-
// the shared integrate band: the SAME precedence chain the tasker uses
|
|
1428
|
-
// (`explicit > configured default > built-in (staging)`), the SAME shared
|
|
1429
|
-
// resolver — only the lifecycle SLOTS differ. THIS caller selects the
|
|
1430
|
-
// trusted-vs-untrusted configured default by reading the `originTrust` stamp:
|
|
1431
|
-
// an untrusted spec selects `untrustedSpecsLandIn` (default staging; `ready`
|
|
1432
|
-
// when configured), a trusted/unset spec selects `specsLandIn`. The agent
|
|
1433
|
-
// (the intake decider) never influences placement; it returns the verdict and
|
|
1434
|
-
// the runner computes the destination from unforgeable inputs. Safety for an
|
|
1435
|
-
// untrusted spec landing in `ready` flows through its tasks' carried stamp,
|
|
1436
|
-
// not the folder.
|
|
1437
|
-
const configuredSpecsLanding =
|
|
1438
|
-
originTrust === 'untrusted' ? untrustedSpecsLandIn : specsLandIn;
|
|
1439
|
-
const placementDecision = resolvePlacement({
|
|
1440
|
-
explicit: specLandingToSide(explicitSpecsLandIn),
|
|
1441
|
-
configuredDefault: specLandingToSide(configuredSpecsLanding),
|
|
1442
|
-
});
|
|
1443
|
-
const placementDir = placementFolder(
|
|
1444
|
-
SPEC_PLACEMENT_SLOTS,
|
|
1445
|
-
placementDecision.choice,
|
|
1446
|
-
);
|
|
1447
|
-
const relPath = `${placementDir}/${slug}.md`;
|
|
1448
|
-
|
|
1449
|
-
// ONBOARD onto a `work/intake-spec-<slug>` branch off fresh `<arbiter>/main` —
|
|
1450
|
-
// the SAME runner-owns-git discipline the task branch uses; the intake-
|
|
1451
|
-
// producer prefix keeps it distinct from a `do spec:<slug>` tasking branch.
|
|
1452
|
-
await switchToWorkBranch(cwd, arbiter, 'spec', slug, env);
|
|
1453
|
-
|
|
1454
|
-
const specContent = renderSpec({
|
|
1455
|
-
slug,
|
|
1456
|
-
title: verdict.specTitle ?? slug,
|
|
1457
|
-
body: verdict.specBody,
|
|
1458
|
-
issueNumber,
|
|
1459
|
-
humanOnly: verdict.specHumanOnly,
|
|
1460
|
-
needsAnswers: verdict.specNeedsAnswers,
|
|
1461
|
-
originTrust,
|
|
1462
|
-
});
|
|
1463
|
-
|
|
1464
|
-
const core = await performIntegration({
|
|
1465
|
-
cwd,
|
|
1466
|
-
arbiter,
|
|
1467
|
-
slug,
|
|
1468
|
-
source: 'in-progress',
|
|
1469
|
-
recovering: false,
|
|
1470
|
-
// An intake-emitted spec has no `verify` floor of its own (it is a new spec,
|
|
1471
|
-
// not a build), exactly as the task branch + the tasking transition skip it.
|
|
1472
|
-
skipVerify: true,
|
|
1473
|
-
mode: integration,
|
|
1474
|
-
// The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) — same
|
|
1475
|
-
// rationale as the lone-task emit above; threaded so intake's spec emit lands
|
|
1476
|
-
// under the same per-repo cap. Unset ⇒ the engine default (byte-for-byte).
|
|
1477
|
-
mergeRetries,
|
|
1478
|
-
noPR,
|
|
1479
|
-
providerInstance,
|
|
1480
|
-
type: 'feat',
|
|
1481
|
-
lifecycle: {
|
|
1482
|
-
// The emitted spec IS the title source. Pass the DRAFTED title EXPLICITLY (same
|
|
1483
|
-
// race as the task path: `stage()` writes the spec file AFTER the title
|
|
1484
|
-
// read). `titlePath` stays set but is IGNORED while `title` is present.
|
|
1485
|
-
titlePath: join(cwd, relPath),
|
|
1486
|
-
title: verdict.specTitle ?? slug,
|
|
1487
|
-
commitTag: 'intake',
|
|
1488
|
-
stage: () =>
|
|
1489
|
-
stageIntakeContent({cwd, relPath, content: specContent, env}),
|
|
1490
|
-
},
|
|
1491
|
-
env,
|
|
1492
|
-
note,
|
|
1493
|
-
});
|
|
1494
|
-
|
|
1495
|
-
return integrationToIntakeResult(core, {
|
|
1496
|
-
issueNumber,
|
|
1497
|
-
slug,
|
|
1498
|
-
relPath,
|
|
1499
|
-
kind: 'spec',
|
|
1500
|
-
cwd,
|
|
1501
|
-
issueProvider,
|
|
1502
|
-
seen,
|
|
1503
|
-
env,
|
|
1504
|
-
note,
|
|
1505
|
-
});
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
1589
|
/**
|
|
1509
1590
|
* Map the shared integrate band's {@link IntegrationCoreResult} onto the intake
|
|
1510
1591
|
* {@link IntakeResult}. On `completed` the artifact was written + integrated; a
|
|
@@ -1534,10 +1615,19 @@ async function integrationToIntakeResult(
|
|
|
1534
1615
|
const kind = ctx.kind ?? 'task';
|
|
1535
1616
|
const artifact = kind === 'spec' ? 'spec' : 'task';
|
|
1536
1617
|
if (core.outcome === 'completed') {
|
|
1618
|
+
// HONEST propose report (task `intake-reports-the-pr-it-actually-opened`):
|
|
1619
|
+
// say "opened a PR" ONLY when the provider opened one. A degraded `gh pr
|
|
1620
|
+
// create` (e.g. a repository that refuses PR creation by Actions) pushed the
|
|
1621
|
+
// branch but opened nothing; the shared core already printed the provider's
|
|
1622
|
+
// instruction, and the message names the branch the work is on.
|
|
1623
|
+
const notOpened = proposeRequestNotOpened(core.integration);
|
|
1537
1624
|
const landed =
|
|
1538
1625
|
core.integration?.mode === 'merge'
|
|
1539
1626
|
? 'landed it on the arbiter main'
|
|
1540
|
-
:
|
|
1627
|
+
: notOpened !== undefined
|
|
1628
|
+
? `pushed it on branch ${core.integration?.pushedRef ?? core.branch} ` +
|
|
1629
|
+
'but opened NO PR (main untouched; see the provider note above)'
|
|
1630
|
+
: 'opened a PR carrying it (main untouched)';
|
|
1541
1631
|
// Both a lone task and a spec carry `issue: N` as their closure link (the task
|
|
1542
1632
|
// closes its own issue; a spec is reached via `task.spec: → spec issue:`). On the
|
|
1543
1633
|
// task/spec path `intake` never closes the issue (CI's close-job does, via the
|
|
@@ -1607,10 +1697,14 @@ async function integrationToIntakeResult(
|
|
|
1607
1697
|
* - reports `task created` / `spec created` framed as CREATED — NEVER "issue
|
|
1608
1698
|
* resolved/closed" (intake never closes on the task/spec path).
|
|
1609
1699
|
* - LINKS the artifact by INTEGRATION MODE: the PR `url` in propose, the landed
|
|
1610
|
-
* `commit` (the additive {@link IntegrateResult.commit}) in merge.
|
|
1611
|
-
*
|
|
1700
|
+
* `commit` (the additive {@link IntegrateResult.commit}) in merge. An opened PR
|
|
1701
|
+
* whose `url` was unparseable or a failed merge-tip read (no `commit`) simply OMITS the
|
|
1612
1702
|
* link — the comment still confirms what was created (the artifact is safe on the
|
|
1613
1703
|
* branch/main regardless). No spec link beyond the slug.
|
|
1704
|
+
* - says so when a propose land opened NO PR at all (the provider degraded, e.g. a
|
|
1705
|
+
* repository that refuses PR creation by Actions, or a push-only provider): it
|
|
1706
|
+
* names the pushed branch and quotes the provider's reason verbatim, instead of
|
|
1707
|
+
* reading as if a PR carried the artifact.
|
|
1614
1708
|
* - carries the FULL intake MARKER via the SHARED {@link stampIntakeMarker} helper
|
|
1615
1709
|
* (`kind=created slug=<slug> seen=<id>,…`) so the triage's `already-terminal`
|
|
1616
1710
|
* branch consumes it — the comment cannot re-trigger intake.
|
|
@@ -1623,14 +1717,22 @@ export function composeIntakeCompletionComment(params: {
|
|
|
1623
1717
|
}): string {
|
|
1624
1718
|
const {kind, slug, integration, seen} = params;
|
|
1625
1719
|
const artifact = kind === 'spec' ? 'spec' : 'task';
|
|
1720
|
+
// A propose land that opened NO PR (the provider degraded, or push-only) says
|
|
1721
|
+
// so, names the pushed branch and quotes the provider's reason, instead of
|
|
1722
|
+
// implying a PR (task `intake-reports-the-pr-it-actually-opened`).
|
|
1723
|
+
const notOpened = proposeRequestNotOpened(integration);
|
|
1626
1724
|
const link =
|
|
1627
1725
|
integration?.mode === 'merge'
|
|
1628
1726
|
? integration.commit !== undefined
|
|
1629
1727
|
? `\n\nIt landed on \`main\` in commit ${integration.commit}.`
|
|
1630
1728
|
: ''
|
|
1631
|
-
:
|
|
1632
|
-
? `\n\nIt is
|
|
1633
|
-
|
|
1729
|
+
: notOpened !== undefined
|
|
1730
|
+
? `\n\nIt is pushed on branch \`${integration!.pushedRef}\`, but **no PR ` +
|
|
1731
|
+
`could be opened**, so nothing proposes it for review yet. The ` +
|
|
1732
|
+
`reason reported:\n\n${fenceVerbatim(notOpened)}`
|
|
1733
|
+
: integration?.url !== undefined
|
|
1734
|
+
? `\n\nIt is carried by the PR: ${integration.url}`
|
|
1735
|
+
: '';
|
|
1634
1736
|
const body =
|
|
1635
1737
|
`Created ${artifact} \`${slug}\` from this issue.${link}\n\n` +
|
|
1636
1738
|
`This is an informational update — the issue stays open (it remains in play ` +
|
|
@@ -1640,6 +1742,21 @@ export function composeIntakeCompletionComment(params: {
|
|
|
1640
1742
|
return stampIntakeMarker(body, {kind: 'created', seen, slug});
|
|
1641
1743
|
}
|
|
1642
1744
|
|
|
1745
|
+
/**
|
|
1746
|
+
* Wrap `text` in a fenced code block that renders it VERBATIM: the fence is one
|
|
1747
|
+
* backtick longer than the longest backtick run inside `text` (minimum three), so
|
|
1748
|
+
* a provider instruction carrying inline code or a quoted PR body can never close
|
|
1749
|
+
* the fence early.
|
|
1750
|
+
*/
|
|
1751
|
+
function fenceVerbatim(text: string): string {
|
|
1752
|
+
const longest = Math.max(
|
|
1753
|
+
0,
|
|
1754
|
+
...Array.from(text.matchAll(/`+/g), (m) => m[0].length),
|
|
1755
|
+
);
|
|
1756
|
+
const fence = '`'.repeat(Math.max(3, longest + 1));
|
|
1757
|
+
return `${fence}text\n${text}\n${fence}`;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1643
1760
|
/**
|
|
1644
1761
|
* Post the INFORMATIONAL completion comment for a SUCCESSFUL `task` / `spec`
|
|
1645
1762
|
* outcome (this task). It closes the loop the ASK/BOUNCE comments already close
|
|
@@ -1743,6 +1860,33 @@ function resolveSpecSlug(verdict: IntakeVerdict): string {
|
|
|
1743
1860
|
return ensureSafeSlug(paramCase(candidate));
|
|
1744
1861
|
}
|
|
1745
1862
|
|
|
1863
|
+
/**
|
|
1864
|
+
* Refuse an intake verdict whose drafted title cannot be one frontmatter line
|
|
1865
|
+
* (task `intake-frontmatter-title-injection-strips-origin-stamp`). Only the
|
|
1866
|
+
* title the verdict's OUTCOME will render is checked (`taskTitle` for `task`,
|
|
1867
|
+
* `specTitle` for `spec`): it must be one line with no control character and
|
|
1868
|
+
* not blank. An ABSENT title stays legal (the dispatcher falls back to the
|
|
1869
|
+
* slug). THROWS; the caller maps the throw onto `agent-failed`.
|
|
1870
|
+
*/
|
|
1871
|
+
export function assertIntakeVerdictTitle(verdict: IntakeVerdict): void {
|
|
1872
|
+
const [field, title] =
|
|
1873
|
+
verdict.outcome === 'task'
|
|
1874
|
+
? ['taskTitle', verdict.taskTitle]
|
|
1875
|
+
: verdict.outcome === 'spec'
|
|
1876
|
+
? ['specTitle', verdict.specTitle]
|
|
1877
|
+
: [undefined, undefined];
|
|
1878
|
+
if (field === undefined || title === undefined) {
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
const problem = singleLineScalarProblem(title);
|
|
1882
|
+
if (problem !== undefined) {
|
|
1883
|
+
throw new Error(`intake verdict '${field}' ${problem}.`);
|
|
1884
|
+
}
|
|
1885
|
+
if (title.trim() === '') {
|
|
1886
|
+
throw new Error(`intake verdict '${field}' is blank.`);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1746
1890
|
/**
|
|
1747
1891
|
* Render the backlog task file: the frontmatter (`title`/`slug`/`covers: []`, NO
|
|
1748
1892
|
* `spec:` — its own source of truth, spec `issue-intake` decision table) carrying the lone-task
|
|
@@ -1768,9 +1912,13 @@ export function renderBacklogTask(params: {
|
|
|
1768
1912
|
originTrust?: OriginTrust;
|
|
1769
1913
|
}): string {
|
|
1770
1914
|
const {slug, title, body, issueNumber, originTrust} = params;
|
|
1915
|
+
// The TITLE is agent-drafted (issue text any user can write): it goes through
|
|
1916
|
+
// the shared quoter, which THROWS on a multi-line / control-character value
|
|
1917
|
+
// (the verdict boundary already refused one; this is the backstop), so it can
|
|
1918
|
+
// never close the fence early and demote the origin-trust stamp below it.
|
|
1771
1919
|
const lines = [
|
|
1772
1920
|
'---',
|
|
1773
|
-
`title: ${title}`,
|
|
1921
|
+
`title: ${quoteYamlScalar(title)}`,
|
|
1774
1922
|
`slug: ${slug}`,
|
|
1775
1923
|
`issue: ${issueNumber}`,
|
|
1776
1924
|
];
|
|
@@ -1796,7 +1944,23 @@ export function renderBacklogTask(params: {
|
|
|
1796
1944
|
acceptanceCriteria: '- [ ] the issue is resolved',
|
|
1797
1945
|
prompt: `Resolve issue #${issueNumber}: ${title}`,
|
|
1798
1946
|
}).trimEnd();
|
|
1799
|
-
|
|
1947
|
+
const rendered = `${frontmatter}\n\n${drafted}\n`;
|
|
1948
|
+
// Re-parse what we rendered: every key is runner-owned, so the frontmatter
|
|
1949
|
+
// must read back EXACTLY these keys and values, else fail loudly.
|
|
1950
|
+
assertFrontmatterFields(
|
|
1951
|
+
rendered,
|
|
1952
|
+
{
|
|
1953
|
+
title,
|
|
1954
|
+
slug,
|
|
1955
|
+
issue: String(issueNumber),
|
|
1956
|
+
origin: originTrust === undefined ? undefined : 'issue',
|
|
1957
|
+
originTrust,
|
|
1958
|
+
covers: '[]',
|
|
1959
|
+
blockedBy: '[]',
|
|
1960
|
+
},
|
|
1961
|
+
{exact: true},
|
|
1962
|
+
);
|
|
1963
|
+
return rendered;
|
|
1800
1964
|
}
|
|
1801
1965
|
|
|
1802
1966
|
/**
|
|
@@ -1828,9 +1992,11 @@ export function renderSpec(params: {
|
|
|
1828
1992
|
}): string {
|
|
1829
1993
|
const {slug, title, body, issueNumber, humanOnly, needsAnswers, originTrust} =
|
|
1830
1994
|
params;
|
|
1995
|
+
// The agent-drafted TITLE goes through the shared quoter (see
|
|
1996
|
+
// `renderBacklogTask`).
|
|
1831
1997
|
const lines = [
|
|
1832
1998
|
'---',
|
|
1833
|
-
`title: ${title}`,
|
|
1999
|
+
`title: ${quoteYamlScalar(title)}`,
|
|
1834
2000
|
`slug: ${slug}`,
|
|
1835
2001
|
`issue: ${issueNumber}`,
|
|
1836
2002
|
];
|
|
@@ -1865,7 +2031,22 @@ export function renderSpec(params: {
|
|
|
1865
2031
|
solution: '(to be detailed; this spec needs tasking via `do spec:`).',
|
|
1866
2032
|
userStories: `1. As a user, I want issue #${issueNumber} addressed.`,
|
|
1867
2033
|
}).trimEnd();
|
|
1868
|
-
|
|
2034
|
+
const rendered = `${frontmatter}\n\n${drafted}\n`;
|
|
2035
|
+
// Re-parse what we rendered (see `renderBacklogTask`).
|
|
2036
|
+
assertFrontmatterFields(
|
|
2037
|
+
rendered,
|
|
2038
|
+
{
|
|
2039
|
+
title,
|
|
2040
|
+
slug,
|
|
2041
|
+
issue: String(issueNumber),
|
|
2042
|
+
origin: originTrust === undefined ? undefined : 'issue',
|
|
2043
|
+
originTrust,
|
|
2044
|
+
humanOnly: humanOnly === true ? 'true' : undefined,
|
|
2045
|
+
needsAnswers: needsAnswers === true ? 'true' : undefined,
|
|
2046
|
+
},
|
|
2047
|
+
{exact: true},
|
|
2048
|
+
);
|
|
2049
|
+
return rendered;
|
|
1869
2050
|
}
|
|
1870
2051
|
|
|
1871
2052
|
/**
|