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/tasking.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
writeFileSync,
|
|
8
8
|
} from 'node:fs';
|
|
9
9
|
import {basename, dirname, join} from 'node:path';
|
|
10
|
-
import {parseFrontmatter} from './frontmatter.js';
|
|
10
|
+
import {parseFrontmatter, unquoteYamlScalar} from './frontmatter.js';
|
|
11
11
|
import {runAsync, type RunResult} from './git.js';
|
|
12
12
|
import {
|
|
13
13
|
workFolderRel,
|
|
@@ -18,10 +18,11 @@ import {
|
|
|
18
18
|
} from './work-layout.js';
|
|
19
19
|
import {
|
|
20
20
|
performIntegration,
|
|
21
|
+
proposePointer,
|
|
21
22
|
type IntegrationCoreResult,
|
|
22
23
|
} from './integration-core.js';
|
|
23
24
|
import type {IntegrationMode} from './config.js';
|
|
24
|
-
import type
|
|
25
|
+
import {proposeRequestNotOpened, type ReviewProvider} from './integrator.js';
|
|
25
26
|
import {
|
|
26
27
|
resolveTaskingEligibility,
|
|
27
28
|
type TaskingEligibilityResult,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
type ReleaseTaskingLockResult,
|
|
36
37
|
} from './tasking-lock.js';
|
|
37
38
|
import {releaseItemLock} from './item-lock.js';
|
|
39
|
+
import {refWrite} from './ref-write.js';
|
|
38
40
|
import {NullHarness, type Harness} from './harness.js';
|
|
39
41
|
import {launchWithOptionalWatch} from './agent-launch.js';
|
|
40
42
|
import {placementFolder, resolvePlacement} from './placement.js';
|
|
@@ -511,32 +513,20 @@ export async function performTask(
|
|
|
511
513
|
await switchToWorkBranch(cwd, arbiter, slug, env);
|
|
512
514
|
}
|
|
513
515
|
|
|
514
|
-
// 3.
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
let agent: {ok: boolean; detail?: string};
|
|
529
|
-
try {
|
|
530
|
-
agent = await runTaskAgent(options, cwd, prompt, slug);
|
|
531
|
-
} catch (err) {
|
|
532
|
-
agent = {
|
|
533
|
-
ok: false,
|
|
534
|
-
detail: err instanceof Error ? err.message : String(err),
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
if (!agent.ok) {
|
|
538
|
-
const detail = agent.detail ?? `the agent failed to task '${slug}'.`;
|
|
539
|
-
let message = `Agent failed tasking '${slug}' (${detail}).`;
|
|
516
|
+
// 3. + 3.5 THE AGENT HALF (task `ci-split-tasking`): the tasker agent and the
|
|
517
|
+
// tasker review→edit→converge loop. Shared, unchanged, with the CI agent
|
|
518
|
+
// phase (`ci-phase-tasking.ts`), which runs this SAME half in the job that
|
|
519
|
+
// holds no write token and hands the result over instead of integrating.
|
|
520
|
+
const half = await runTaskingAgentHalf(options, {
|
|
521
|
+
cwd,
|
|
522
|
+
slug,
|
|
523
|
+
specFm,
|
|
524
|
+
doer,
|
|
525
|
+
note,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
if (half.kind === 'agent-failed') {
|
|
529
|
+
let message = `Agent failed tasking '${slug}' (${half.detail}).`;
|
|
540
530
|
// RELEASE THE LOCK THE FAILED RUN TOOK (observation
|
|
541
531
|
// `crashed-do-spec-strands-a-tasking-lock-no-verb-releases`).
|
|
542
532
|
//
|
|
@@ -592,13 +582,218 @@ export async function performTask(
|
|
|
592
582
|
return {exitCode: 1, outcome: 'agent-failed', slug, message};
|
|
593
583
|
}
|
|
594
584
|
|
|
585
|
+
if (half.kind === 'review-leg-failed') {
|
|
586
|
+
// REVIEW-LEG FAILURE handling (observation
|
|
587
|
+
// `tasker-review-edits-payload-caps-the-verdict-response`): PERSIST the
|
|
588
|
+
// candidate tasks, and BOUNCE through the SAME surface the
|
|
589
|
+
// decomposition-unclear path uses (release the lock + a question sidecar).
|
|
590
|
+
// NEVER a silent approve — a parse failure is always a needs-attention route.
|
|
591
|
+
const candidatePaths = await persistTaskingCandidates(
|
|
592
|
+
cwd,
|
|
593
|
+
slug,
|
|
594
|
+
half.before,
|
|
595
|
+
arbiter,
|
|
596
|
+
env,
|
|
597
|
+
note,
|
|
598
|
+
);
|
|
599
|
+
const reason = reviewFailureReason(slug, half.error, candidatePaths);
|
|
600
|
+
const message = reviewFailureMessage(slug, half.error);
|
|
601
|
+
if (useLock) {
|
|
602
|
+
return await surfaceTaskingBlock({
|
|
603
|
+
slug,
|
|
604
|
+
cwd,
|
|
605
|
+
arbiter,
|
|
606
|
+
reason,
|
|
607
|
+
message,
|
|
608
|
+
lockedBlob,
|
|
609
|
+
release: lock.release,
|
|
610
|
+
mode: resolvedMode,
|
|
611
|
+
provider: options.providerInstance,
|
|
612
|
+
env,
|
|
613
|
+
note,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
note(reason);
|
|
617
|
+
// Human, no-lock path: a clean park-for-human is a success terminal (exit 0).
|
|
618
|
+
return {exitCode: 0, outcome: 'needs-attention', slug, message};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (half.kind === 'decomposition-unclear') {
|
|
622
|
+
// DECOMPOSITION UNCLEAR: emit NO guessed tasks — route the held spec to
|
|
623
|
+
// needs-attention with the questions as the reason. The lock release surfaces
|
|
624
|
+
// the spec (a question sidecar + `needsAnswers: true` on main) and releases the
|
|
625
|
+
// `spec:<slug>` lock; the spec body stays in `work/specs/ready/`.
|
|
626
|
+
const reason = decompositionUnclearReason(slug, half.questions);
|
|
627
|
+
if (useLock) {
|
|
628
|
+
return await surfaceTaskingBlock({
|
|
629
|
+
slug,
|
|
630
|
+
cwd,
|
|
631
|
+
arbiter,
|
|
632
|
+
reason,
|
|
633
|
+
message: half.message,
|
|
634
|
+
lockedBlob,
|
|
635
|
+
release: lock.release,
|
|
636
|
+
mode: resolvedMode,
|
|
637
|
+
provider: options.providerInstance,
|
|
638
|
+
env,
|
|
639
|
+
note,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
note(half.message);
|
|
643
|
+
// Human, no-lock path: nothing to release/surface via the seam; a clean
|
|
644
|
+
// park-for-human is still a success terminal (exit 0).
|
|
645
|
+
return {
|
|
646
|
+
exitCode: 0,
|
|
647
|
+
outcome: 'needs-attention',
|
|
648
|
+
slug,
|
|
649
|
+
message: half.message,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// 4. The RUNNER commits the COMPLETING transition: drop the produced backlog
|
|
654
|
+
// tasks IN + move the spec specs/ready/ -> specs/tasked/ (residence = tasked-ness) — now
|
|
655
|
+
// through the SHARED integrate core (`--propose` PR / `--merge` main), NOT a
|
|
656
|
+
// direct commit to `main`. The agent never does git. (The backlog snapshot is
|
|
657
|
+
// taken AFTER any loop edits, so the runner integrates the IMPROVED tasks,
|
|
658
|
+
// not the pre-loop candidates.)
|
|
659
|
+
const stagedEmitted = newOrChangedStagedTasks(cwd, half.before);
|
|
660
|
+
const emitTasks = collectEmittedTasks(cwd, stagedEmitted);
|
|
661
|
+
const placementDir = taskPlacementDir(options, specFm);
|
|
662
|
+
const loopTag = half.loopTag;
|
|
663
|
+
|
|
664
|
+
if (useLock) {
|
|
665
|
+
return await integrateTaskingCandidates({
|
|
666
|
+
slug,
|
|
667
|
+
cwd,
|
|
668
|
+
arbiter,
|
|
669
|
+
stagedEmitted,
|
|
670
|
+
emitTasks,
|
|
671
|
+
poolBefore: half.poolBefore,
|
|
672
|
+
placementDir,
|
|
673
|
+
loopTag,
|
|
674
|
+
lockedBlob,
|
|
675
|
+
release: lock.release,
|
|
676
|
+
mode: resolvedMode,
|
|
677
|
+
review: options.review,
|
|
678
|
+
reviewGate: options.reviewGate,
|
|
679
|
+
acceptanceReviewModel: options.acceptanceReviewModel,
|
|
680
|
+
mergeRetries: options.mergeRetries,
|
|
681
|
+
noPR: options.noPR,
|
|
682
|
+
providerInstance: options.providerInstance,
|
|
683
|
+
env,
|
|
684
|
+
agentEnv,
|
|
685
|
+
note,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// HUMAN, no-lock path: the human commits on `main` directly (the runner does
|
|
690
|
+
// not own the human's git). We report the produced tasks; moving the spec into
|
|
691
|
+
// `work/specs/tasked/` (residence = tasked-ness) and committing is the human's to
|
|
692
|
+
// do, as with the human `complete`.
|
|
693
|
+
const emitted = stagedEmitted.map(
|
|
694
|
+
(rel) => `${placementDir}/${basename(rel)}`,
|
|
695
|
+
);
|
|
696
|
+
const message =
|
|
697
|
+
`Tasked '${slug}' -> ${emitted.length} backlog task` +
|
|
698
|
+
`${emitted.length === 1 ? '' : 's'} (human path, no lock). Inspect + commit ` +
|
|
699
|
+
`the produced files (and move the spec into work/specs/tasked/) yourself.`;
|
|
700
|
+
note(message);
|
|
701
|
+
return {
|
|
702
|
+
exitCode: 0,
|
|
703
|
+
outcome: 'tasked',
|
|
704
|
+
slug,
|
|
705
|
+
emitted,
|
|
706
|
+
loop: loopTag,
|
|
707
|
+
message,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* How {@link runTaskingAgentHalf} ended (task `ci-split-tasking`): the data the
|
|
713
|
+
* write half acts on. The laptop `performTask` acts on it in-process; the CI
|
|
714
|
+
* agent phase (`ci-phase-tasking.ts`) hands it over to the apply phase.
|
|
715
|
+
*/
|
|
716
|
+
export type TaskingAgentHalf =
|
|
717
|
+
/** The tasker agent itself errored (nothing was produced to trust). */
|
|
718
|
+
| {kind: 'agent-failed'; detail: string}
|
|
719
|
+
/**
|
|
720
|
+
* The improver loop's review leg returned no parseable verdict; the candidate
|
|
721
|
+
* tasks (new-or-changed vs `before`) are on disk for recovery.
|
|
722
|
+
*/
|
|
723
|
+
| {
|
|
724
|
+
kind: 'review-leg-failed';
|
|
725
|
+
error: ReviewParseError;
|
|
726
|
+
before: Map<string, string>;
|
|
727
|
+
}
|
|
728
|
+
/** The improver loop found the decomposition unclear: no tasks land. */
|
|
729
|
+
| {kind: 'decomposition-unclear'; questions: string[]; message: string}
|
|
730
|
+
/** The candidate tasks are on disk (improved, uncertain ones marked). */
|
|
731
|
+
| {
|
|
732
|
+
kind: 'candidates';
|
|
733
|
+
/** The staged-tasks snapshot taken BEFORE the agent ran (the fence). */
|
|
734
|
+
before: Map<string, string>;
|
|
735
|
+
/** The pool snapshot taken BEFORE the agent ran (the scrub fence). */
|
|
736
|
+
poolBefore: Map<string, string>;
|
|
737
|
+
loopTag: 'converged' | 'uncertain-tasks' | undefined;
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* The AGENT half of the tasking path (steps 3 and 3.5): run the tasker agent in
|
|
742
|
+
* `cwd` (on the work branch), then, when a loop seam is wired and the doer is
|
|
743
|
+
* the agent, the tasker review→edit→converge loop, and report what is on disk.
|
|
744
|
+
* It performs NO write to the arbiter and no git transition: only the agents'
|
|
745
|
+
* file edits, the loop's local edits and markers, and the scratch cleanup.
|
|
746
|
+
*/
|
|
747
|
+
export async function runTaskingAgentHalf(
|
|
748
|
+
options: PerformTaskOptions,
|
|
749
|
+
params: {
|
|
750
|
+
cwd: string;
|
|
751
|
+
slug: string;
|
|
752
|
+
specFm: {spec?: string};
|
|
753
|
+
doer: 'agent' | 'human';
|
|
754
|
+
note: (message: string) => void;
|
|
755
|
+
},
|
|
756
|
+
): Promise<TaskingAgentHalf> {
|
|
757
|
+
const {cwd, slug, specFm, doer, note} = params;
|
|
758
|
+
const agentEnv = options.agentEnv ?? options.env;
|
|
759
|
+
|
|
760
|
+
// 3. INVOKE THE AGENT with the to-task spec. It WRITES
|
|
761
|
+
// `work/tasks/backlog/*.md` task files (the STAGED area — NOT `work/tasks/ready/`,
|
|
762
|
+
// which is the agent-eligible pool the runner owns the promotion into; task
|
|
763
|
+
// `pre-backlog-staging-folder-and-promote-step-a`); it does NO git. We
|
|
764
|
+
// snapshot the staged-tasks folder before/after so the runner (not the
|
|
765
|
+
// agent) captures + commits exactly what was produced.
|
|
766
|
+
const before = snapshotStagedTasks(cwd);
|
|
767
|
+
// Also snapshot the POOL `work/tasks/ready/` BEFORE the agent runs: the runner's
|
|
768
|
+
// final commit must scrub any agent writes there (an attempt to self-place into
|
|
769
|
+
// the pool, spec US #4) before `git add -A` would sweep them in.
|
|
770
|
+
const poolBefore = snapshotPool(cwd);
|
|
771
|
+
// Read the parent-spec self-pointer off `specFm.spec` (populated from the
|
|
772
|
+
// `spec:` key).
|
|
773
|
+
const prompt = buildTaskingSpec(slug, specFm.spec);
|
|
774
|
+
let agent: {ok: boolean; detail?: string};
|
|
775
|
+
try {
|
|
776
|
+
agent = await runTaskAgent(options, cwd, prompt, slug);
|
|
777
|
+
} catch (err) {
|
|
778
|
+
agent = {
|
|
779
|
+
ok: false,
|
|
780
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
if (!agent.ok) {
|
|
784
|
+
return {
|
|
785
|
+
kind: 'agent-failed',
|
|
786
|
+
detail: agent.detail ?? `the agent failed to task '${slug}'.`,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
595
790
|
// 3.5 THE TASKER REVIEW→EDIT→CONVERGE LOOP (`slicer-review-edit-loop`, Shape 2 /
|
|
596
791
|
// insertion point A): when a loop seam is wired, run the `review` SKILL as a
|
|
597
792
|
// review→edit→re-review loop that IMPROVES the candidate tasks in place, then
|
|
598
793
|
// determines the disposition (the three outcomes). This plugs in AFTER the
|
|
599
794
|
// candidate tasks are produced and BEFORE they are finalised. The agent makes
|
|
600
795
|
// the review/edit JUDGEMENTS; the loop applies edits to the candidate files
|
|
601
|
-
// and routes the verdict; the runner
|
|
796
|
+
// and routes the verdict; the runner owns the git transition. Only the
|
|
602
797
|
// AGENT path runs the loop — the human tasking path is unaffected.
|
|
603
798
|
let loopDisposition: RunTaskReviewLoopResult | undefined;
|
|
604
799
|
if (options.reviewLoop && doer === 'agent') {
|
|
@@ -608,10 +803,8 @@ export async function performTask(
|
|
|
608
803
|
// parseable verdict (a generic parse failure, or the NAMED cap-truncation
|
|
609
804
|
// class). The pre-fix behaviour THREW here, crashing the run, LEAVING THE
|
|
610
805
|
// LOCK HELD, writing NO question sidecar, and DISCARDING the tasker's
|
|
611
|
-
// candidate tasks. Now: catch it,
|
|
612
|
-
//
|
|
613
|
-
// lock + a question sidecar). NEVER a silent approve — a parse failure is
|
|
614
|
-
// always a needs-attention route. A non-review throw (genuine wiring error)
|
|
806
|
+
// candidate tasks. Now: catch it and report it, so the write half PERSISTS
|
|
807
|
+
// the candidate tasks and BOUNCES. A non-review throw (genuine wiring error)
|
|
615
808
|
// is re-thrown.
|
|
616
809
|
try {
|
|
617
810
|
loopDisposition = await runTaskReviewLoop({
|
|
@@ -634,7 +827,6 @@ export async function performTask(
|
|
|
634
827
|
if (!(err instanceof ReviewParseError)) {
|
|
635
828
|
throw err;
|
|
636
829
|
}
|
|
637
|
-
const reviewErr = err as ReviewParseError;
|
|
638
830
|
// Reap any review-edits SCRATCH the failed pass left on disk so it is never
|
|
639
831
|
// swept into a later run's integrate commit.
|
|
640
832
|
try {
|
|
@@ -645,66 +837,12 @@ export async function performTask(
|
|
|
645
837
|
} catch {
|
|
646
838
|
// Best-effort.
|
|
647
839
|
}
|
|
648
|
-
|
|
649
|
-
cwd,
|
|
650
|
-
slug,
|
|
651
|
-
before,
|
|
652
|
-
arbiter,
|
|
653
|
-
env,
|
|
654
|
-
note,
|
|
655
|
-
);
|
|
656
|
-
const reason = reviewFailureReason(slug, reviewErr, candidatePaths);
|
|
657
|
-
const message = reviewFailureMessage(slug, reviewErr);
|
|
658
|
-
if (useLock) {
|
|
659
|
-
return await surfaceTaskingBlock({
|
|
660
|
-
slug,
|
|
661
|
-
cwd,
|
|
662
|
-
arbiter,
|
|
663
|
-
reason,
|
|
664
|
-
message,
|
|
665
|
-
lockedBlob,
|
|
666
|
-
release: lock.release,
|
|
667
|
-
mode: resolvedMode,
|
|
668
|
-
provider: options.providerInstance,
|
|
669
|
-
env,
|
|
670
|
-
note,
|
|
671
|
-
});
|
|
672
|
-
}
|
|
673
|
-
note(reason);
|
|
674
|
-
// Human, no-lock path: a clean park-for-human is a success terminal (exit 0).
|
|
675
|
-
return {exitCode: 0, outcome: 'needs-attention', slug, message};
|
|
840
|
+
return {kind: 'review-leg-failed', error: err, before};
|
|
676
841
|
}
|
|
677
|
-
// DECOMPOSITION UNCLEAR: emit NO guessed tasks — route the held spec to
|
|
678
|
-
// needs-attention with the questions as the reason. The lock release amends the
|
|
679
|
-
// `spec:<slug>` unified lock `active → stuck` (the tasking needs-attention surface
|
|
680
|
-
// is the stuck lock now — NO folder write; the spec body stays in `work/specs/ready/`).
|
|
681
842
|
if (loopDisposition.outcome === 'decomposition-unclear') {
|
|
682
|
-
const reason = decompositionUnclearReason(
|
|
683
|
-
slug,
|
|
684
|
-
loopDisposition.specQuestions,
|
|
685
|
-
);
|
|
686
|
-
if (useLock) {
|
|
687
|
-
return await surfaceTaskingBlock({
|
|
688
|
-
slug,
|
|
689
|
-
cwd,
|
|
690
|
-
arbiter,
|
|
691
|
-
reason,
|
|
692
|
-
message: loopDisposition.message,
|
|
693
|
-
lockedBlob,
|
|
694
|
-
release: lock.release,
|
|
695
|
-
mode: resolvedMode,
|
|
696
|
-
provider: options.providerInstance,
|
|
697
|
-
env,
|
|
698
|
-
note,
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
note(loopDisposition.message);
|
|
702
|
-
// Human, no-lock path: nothing to release/surface via the seam; a clean
|
|
703
|
-
// park-for-human is still a success terminal (exit 0).
|
|
704
843
|
return {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
slug,
|
|
844
|
+
kind: 'decomposition-unclear',
|
|
845
|
+
questions: loopDisposition.specQuestions,
|
|
708
846
|
message: loopDisposition.message,
|
|
709
847
|
};
|
|
710
848
|
}
|
|
@@ -717,31 +855,42 @@ export async function performTask(
|
|
|
717
855
|
}
|
|
718
856
|
}
|
|
719
857
|
}
|
|
858
|
+
return {
|
|
859
|
+
kind: 'candidates',
|
|
860
|
+
before,
|
|
861
|
+
poolBefore,
|
|
862
|
+
loopTag:
|
|
863
|
+
loopDisposition?.outcome === 'converged'
|
|
864
|
+
? 'converged'
|
|
865
|
+
: loopDisposition?.outcome === 'uncertain-tasks'
|
|
866
|
+
? 'uncertain-tasks'
|
|
867
|
+
: undefined,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
720
870
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
// forced to a code PR), not the folder.
|
|
871
|
+
/**
|
|
872
|
+
* The RUNNER-DETERMINISTIC PLACEMENT folder for a spec's emitted tasks (task
|
|
873
|
+
* `runner-deterministic-slice-placement-policy-and-precedence`; the
|
|
874
|
+
* untrusted-forces-staging rung was RETIRED by ADR
|
|
875
|
+
* `untrusted-origin-carries-via-stamp-not-forced-staging`): precedence
|
|
876
|
+
* `explicit > configured default > built-in (staging)`. This caller selects the
|
|
877
|
+
* trusted-vs-untrusted configured default by reading the spec's propagated
|
|
878
|
+
* `originTrust:` stamp: an untrusted-origin spec selects `untrustedTasksLandIn`
|
|
879
|
+
* (default staging; `ready` when configured), a trusted/unset spec selects
|
|
880
|
+
* `tasksLandIn`. One knob (`untrustedTasksLandIn`) governs an untrusted task's
|
|
881
|
+
* destination whether born from an issue directly (intake) or from an untrusted
|
|
882
|
+
* spec here. The agent NEVER influences this; it always writes to
|
|
883
|
+
* `work/tasks/backlog/`, and the runner redirects at `stage()` time. Safety for
|
|
884
|
+
* an untrusted task landing in `ready` is the carried stamp (its BUILD is forced
|
|
885
|
+
* to a code PR), not the folder.
|
|
886
|
+
*/
|
|
887
|
+
export function taskPlacementDir(
|
|
888
|
+
options: Pick<
|
|
889
|
+
PerformTaskOptions,
|
|
890
|
+
'tasksLandIn' | 'untrustedTasksLandIn' | 'explicitTasksLandIn'
|
|
891
|
+
>,
|
|
892
|
+
specFm: {originTrust?: string},
|
|
893
|
+
): string {
|
|
745
894
|
const configuredTasksLanding =
|
|
746
895
|
specFm.originTrust === 'untrusted'
|
|
747
896
|
? options.untrustedTasksLandIn
|
|
@@ -750,265 +899,289 @@ export async function performTask(
|
|
|
750
899
|
explicit: landingToSide(options.explicitTasksLandIn),
|
|
751
900
|
configuredDefault: landingToSide(configuredTasksLanding),
|
|
752
901
|
});
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
902
|
+
return placementFolder(TASK_PLACEMENT_SLOTS, placementDecision.choice);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** The inputs of {@link integrateTaskingCandidates}. */
|
|
906
|
+
export interface IntegrateTaskingInput {
|
|
907
|
+
slug: string;
|
|
908
|
+
cwd: string;
|
|
909
|
+
arbiter: string;
|
|
910
|
+
/** The candidate tasks' repo-relative paths under `work/tasks/backlog/`. */
|
|
911
|
+
stagedEmitted: string[];
|
|
912
|
+
/** Their content, keyed by those paths. */
|
|
913
|
+
emitTasks: Record<string, string>;
|
|
914
|
+
/** The pool snapshot the stage scrubs agent writes against. */
|
|
915
|
+
poolBefore: Map<string, string>;
|
|
916
|
+
/** The runner-resolved destination folder ({@link taskPlacementDir}). */
|
|
917
|
+
placementDir: string;
|
|
918
|
+
loopTag: 'converged' | 'uncertain-tasks' | undefined;
|
|
919
|
+
/** The spec blob the lock took (the read-stability backstop). */
|
|
920
|
+
lockedBlob: string | undefined;
|
|
921
|
+
release: TaskingLockSeam['release'];
|
|
922
|
+
mode: IntegrationMode;
|
|
923
|
+
/** Run the task-set acceptance gate inside the core (the laptop path). */
|
|
924
|
+
review?: boolean;
|
|
925
|
+
reviewGate?: ReviewGate;
|
|
926
|
+
acceptanceReviewModel?: string;
|
|
927
|
+
/**
|
|
928
|
+
* The approved task-set review prose when the review already ran elsewhere
|
|
929
|
+
* (the CI agent phase); posted as the PR comment. `review` is then off.
|
|
930
|
+
*/
|
|
931
|
+
approvedReviewProse?: string;
|
|
932
|
+
mergeRetries?: number;
|
|
933
|
+
mergeJitterMs?: number;
|
|
934
|
+
noPR?: boolean;
|
|
935
|
+
providerInstance?: ReviewProvider;
|
|
936
|
+
env: NodeJS.ProcessEnv | undefined;
|
|
937
|
+
agentEnv: NodeJS.ProcessEnv | undefined;
|
|
938
|
+
note: (message: string) => void;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* The WRITE half of a tasking run that produced candidate tasks (step 4): the
|
|
943
|
+
* read-stability stale check, the integrate through the shared core (the tasks
|
|
944
|
+
* + the `specs/ready → specs/tasked` move, `--propose` PR / `--merge` main), the
|
|
945
|
+
* needs-attention routing of a not-landed transition, and the mode-dependent lock
|
|
946
|
+
* release. Shared by the laptop `performTask` (review ON inside the core) and the
|
|
947
|
+
* CI apply phase (`ci-phase-tasking.ts`, review OFF: it ran in the agent phase).
|
|
948
|
+
*/
|
|
949
|
+
export async function integrateTaskingCandidates(
|
|
950
|
+
params: IntegrateTaskingInput,
|
|
951
|
+
): Promise<TaskResult> {
|
|
952
|
+
const {
|
|
953
|
+
slug,
|
|
954
|
+
cwd,
|
|
955
|
+
arbiter,
|
|
956
|
+
stagedEmitted,
|
|
957
|
+
emitTasks,
|
|
958
|
+
poolBefore,
|
|
959
|
+
placementDir,
|
|
960
|
+
loopTag,
|
|
961
|
+
lockedBlob,
|
|
962
|
+
mode: resolvedMode,
|
|
963
|
+
env,
|
|
964
|
+
agentEnv,
|
|
965
|
+
note,
|
|
966
|
+
} = params;
|
|
757
967
|
// REWRITE the emitted list to the RUNNER-RESOLVED destination so callers see
|
|
758
968
|
// where the runner actually placed the files (not where the agent wrote them).
|
|
759
969
|
const emitted = stagedEmitted.map(
|
|
760
970
|
(rel) => `${placementDir}/${basename(rel)}`,
|
|
761
971
|
);
|
|
762
|
-
const loopTag: 'converged' | 'uncertain-tasks' | undefined =
|
|
763
|
-
loopDisposition?.outcome === 'converged'
|
|
764
|
-
? 'converged'
|
|
765
|
-
: loopDisposition?.outcome === 'uncertain-tasks'
|
|
766
|
-
? 'uncertain-tasks'
|
|
767
|
-
: undefined;
|
|
768
972
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
973
|
+
// READ-STABILITY BACKSTOP (the lock's content-identity check, now owned at the
|
|
974
|
+
// integrate seam): the OUTPUT no longer rides the lock release, so the band
|
|
975
|
+
// below would otherwise rebase a concurrent edit of the held spec body CLEANLY
|
|
976
|
+
// into spec/ (a rename+edit merge) while the tasks were cut from the OLD body —
|
|
977
|
+
// the exact silent stale-task drift the lock forbids
|
|
978
|
+
// (`work/notes/observations/tasking-lock-does-not-stabilise-spec-content.md`). So we
|
|
979
|
+
// compare the CURRENTLY held `work/specs/ready/<slug>.md` blob on the arbiter against
|
|
980
|
+
// the snapshot the lock TOOK (`lockedBlob`); ANY change ⇒ STALE ⇒ fail loud,
|
|
981
|
+
// touch NOTHING (the lock stays held; a human re-tasks or routes to
|
|
982
|
+
// needs-attention). It is the SAME content-identity check `releaseTaskingLock`
|
|
983
|
+
// runs — relocated here because this transition, not the release, owns the
|
|
984
|
+
// completing commit now.
|
|
985
|
+
const stale = await heldSpecIsStale(cwd, arbiter, slug, lockedBlob, env);
|
|
986
|
+
if (stale) {
|
|
987
|
+
const specRel = workItemRel('specs-ready', `${slug}.md`);
|
|
988
|
+
const message =
|
|
989
|
+
`RELEASE CONFLICT for '${slug}': the spec was edited (${specRel} ` +
|
|
990
|
+
`changed on ${arbiter}/main) while the tasking lock was held. The tasking is ` +
|
|
991
|
+
`STALE — re-task from the edited spec or route it to needs-attention. ` +
|
|
992
|
+
`The arbiter was NOT modified (lock still held).`;
|
|
993
|
+
note(message);
|
|
994
|
+
return {exitCode: 4, outcome: 'stale', slug, message};
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Route the OUTPUT through the SHARED integrate back-half (task
|
|
998
|
+
// `slice-output-through-integration`): the produced backlog tasks + the spec
|
|
999
|
+
// lifecycle move (`work/specs/ready/ -> work/specs/tasked/`, residence = tasked-ness) integrate
|
|
1000
|
+
// via `performIntegration` honoring `--propose` (push the work branch + open a
|
|
1001
|
+
// PR, NO `main` touch) / `--merge` (land on `main`). Because the integrate-time
|
|
1002
|
+
// args resolve ONCE in the shared core, every `do task:` arg applies here by
|
|
1003
|
+
// construction. The agent did NO git; the runner (the band) owns the ONE commit.
|
|
1004
|
+
const core = await performIntegration({
|
|
1005
|
+
cwd,
|
|
1006
|
+
arbiter,
|
|
1007
|
+
slug,
|
|
1008
|
+
// `source`/`recovering` are task-shaped and IGNORED when `lifecycle` is set
|
|
1009
|
+
// (a tasking transition never recovers a surfaced needs-attention move).
|
|
1010
|
+
source: 'in-progress',
|
|
1011
|
+
recovering: false,
|
|
1012
|
+
// Skip the build acceptance gate (Gate 1 / verify): a tasking transition has
|
|
1013
|
+
// no `verify` floor (the tasker review loop above is its quality gate).
|
|
1014
|
+
skipVerify: true,
|
|
1015
|
+
// THE TASK-SET ACCEPTANCE GATE (task `slice-acceptance-gate`): the
|
|
1016
|
+
// task-path mirror of the build Gate-2, riding THIS shared core's
|
|
1017
|
+
// review-before-integrate block. When `review` resolves on, the wired
|
|
1018
|
+
// `reviewGate` (production: `harnessTaskReviewGate` with the task-SET
|
|
1019
|
+
// prompt) runs a FRESH-CONTEXT review of the produced task SET before it
|
|
1020
|
+
// integrates: `approve` lands it, `block` routes the set to needs-attention
|
|
1021
|
+
// via the SAME machinery the build block uses (mapped to the tasking
|
|
1022
|
+
// `needs-attention` outcome below). It is ONE-SHOT: we pin
|
|
1023
|
+
// `reviewMaxRounds: 1` so the gate is a SINGLE reviewer invocation → verdict
|
|
1024
|
+
// (terminal pass/fail). The task path NEVER exposes/consults
|
|
1025
|
+
// `--review-max-rounds` — a gate is terminal, the rounds bound is an orphan
|
|
1026
|
+
// that belongs to a future revise↔review loop (see
|
|
1027
|
+
// `work/notes/observations/reviewmaxrounds-on-wrong-concept.md`). This is
|
|
1028
|
+
// independently controllable from the tasker improver loop (`reviewLoop` /
|
|
1029
|
+
// the `--tasker-loop*` family); toggling one does not affect the other.
|
|
1030
|
+
// The CI apply phase passes it OFF (it ran in the agent phase) and hands
|
|
1031
|
+
// its approved prose in as `approvedReviewProse`.
|
|
1032
|
+
review: params.review,
|
|
1033
|
+
reviewGate: params.reviewGate,
|
|
1034
|
+
reviewModel: params.acceptanceReviewModel,
|
|
1035
|
+
reviewMaxRounds: 1,
|
|
1036
|
+
approvedReviewProse: params.approvedReviewProse,
|
|
1037
|
+
// The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) —
|
|
1038
|
+
// threaded so a wide-matrix CI's raised cap reaches the tasking-
|
|
1039
|
+
// transition's land tail too (task
|
|
1040
|
+
// `thread-merge-retries-cross-task-and-ratify-default`). Unset ⇒ falls
|
|
1041
|
+
// through to the engine default (byte-for-byte unchanged).
|
|
1042
|
+
mergeRetries: params.mergeRetries,
|
|
1043
|
+
...(params.mergeJitterMs === undefined
|
|
1044
|
+
? {}
|
|
1045
|
+
: {mergeJitterMs: params.mergeJitterMs}),
|
|
1046
|
+
// The EXPLICITLY-chosen integrate mode proceeds AS-IS on an APPROVE — a
|
|
1047
|
+
// `--merge` tasking run lands on main, `--propose` opens a PR. The tasking
|
|
1048
|
+
// path's merge-vs-propose decision is the `integration` mode the user typed;
|
|
1049
|
+
// `merge` IS the auto-land mode, so a resolved `merge` is never downgraded.
|
|
1050
|
+
// The task gate family is `--review`/`--no-review`/`--review-model` only
|
|
1051
|
+
// (spec US #6).
|
|
1052
|
+
mode: resolvedMode,
|
|
1053
|
+
noPR: params.noPR,
|
|
1054
|
+
providerInstance: params.providerInstance,
|
|
1055
|
+
// PROPOSE-MODE PR BODY (task `slicing-pr-body-summary-threading`): mirror the
|
|
1056
|
+
// BUILD path's body threading (`do.ts` — `body: agent.output`). The tasker
|
|
1057
|
+
// agent produces no `LaunchResult.output` we can carry, but the slice-SET
|
|
1058
|
+
// itself is a summary-worthy artifact: the emitted task slugs+titles, their
|
|
1059
|
+
// coverage of the spec's user stories, the dependency graph (keystone +
|
|
1060
|
+
// blockedBy edges), and any `needsAnswers` open questions the tasker review
|
|
1061
|
+
// loop flagged. Compose it here and thread it into the shared core as `body`
|
|
1062
|
+
// so the propose-mode PR carries the summary instead of degrading to
|
|
1063
|
+
// `gh pr create --fill`. Empty-set (no emitted tasks) ⇒ undefined ⇒ the
|
|
1064
|
+
// provider's `--fill` fallback (no regression on that edge).
|
|
1065
|
+
body: composeTaskingProposeBody(slug, emitTasks),
|
|
1066
|
+
type: 'tasking',
|
|
1067
|
+
lifecycle: {
|
|
1068
|
+
// Read the PR title / commit summary from the held spec (before it moves).
|
|
1069
|
+
titlePath: workItemPath(cwd, 'specs-ready', slug),
|
|
1070
|
+
commitTag: 'tasked',
|
|
1071
|
+
// The spec RESTS in `work/specs/tasked/` once this lands (never
|
|
1072
|
+
// `work/tasks/done/`, the task-build default), so point the reviewer there.
|
|
1073
|
+
proposeHeader: proposePointer(
|
|
1074
|
+
'Spec',
|
|
1075
|
+
workItemRel('specs-tasked', `${slug}.md`),
|
|
1076
|
+
),
|
|
1077
|
+
stage: () =>
|
|
1078
|
+
stageTaskingLifecycle({
|
|
1079
|
+
cwd,
|
|
1080
|
+
slug,
|
|
1081
|
+
emitTasks,
|
|
1082
|
+
poolBefore,
|
|
1083
|
+
placementDir,
|
|
1084
|
+
note,
|
|
1085
|
+
env,
|
|
1086
|
+
}),
|
|
1087
|
+
},
|
|
1088
|
+
env,
|
|
1089
|
+
// The task-SET acceptance review AGENT launches AMBIENT, never the
|
|
1090
|
+
// identity-scoped `env` (an agent must not act as the bot).
|
|
1091
|
+
agentEnv,
|
|
1092
|
+
note,
|
|
1093
|
+
});
|
|
793
1094
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
// via `performIntegration` honoring `--propose` (push the work branch + open a
|
|
798
|
-
// PR, NO `main` touch) / `--merge` (land on `main`). Because the integrate-time
|
|
799
|
-
// args resolve ONCE in the shared core, every `do task:` arg applies here by
|
|
800
|
-
// construction. The agent did NO git; the runner (the band) owns the ONE commit.
|
|
801
|
-
const core = await performIntegration({
|
|
1095
|
+
const surfaceBlock = (reason: string, message: string) =>
|
|
1096
|
+
surfaceTaskingBlock({
|
|
1097
|
+
slug,
|
|
802
1098
|
cwd,
|
|
803
1099
|
arbiter,
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
recovering: false,
|
|
809
|
-
// Skip the build acceptance gate (Gate 1 / verify): a tasking transition has
|
|
810
|
-
// no `verify` floor (the tasker review loop above is its quality gate).
|
|
811
|
-
skipVerify: true,
|
|
812
|
-
// THE TASK-SET ACCEPTANCE GATE (task `slice-acceptance-gate`): the
|
|
813
|
-
// task-path mirror of the build Gate-2, riding THIS shared core's
|
|
814
|
-
// review-before-integrate block. When `review` resolves on, the wired
|
|
815
|
-
// `reviewGate` (production: `harnessTaskReviewGate` with the task-SET
|
|
816
|
-
// prompt) runs a FRESH-CONTEXT review of the produced task SET before it
|
|
817
|
-
// integrates: `approve` lands it, `block` routes the set to needs-attention
|
|
818
|
-
// via the SAME machinery the build block uses (mapped to the tasking
|
|
819
|
-
// `needs-attention` outcome below). It is ONE-SHOT: we pin
|
|
820
|
-
// `reviewMaxRounds: 1` so the gate is a SINGLE reviewer invocation → verdict
|
|
821
|
-
// (terminal pass/fail). The task path NEVER exposes/consults
|
|
822
|
-
// `--review-max-rounds` — a gate is terminal, the rounds bound is an orphan
|
|
823
|
-
// that belongs to a future revise↔review loop (see
|
|
824
|
-
// `work/notes/observations/reviewmaxrounds-on-wrong-concept.md`). This is
|
|
825
|
-
// independently controllable from the tasker improver loop (`reviewLoop` /
|
|
826
|
-
// the `--tasker-loop*` family); toggling one does not affect the other.
|
|
827
|
-
review: options.review,
|
|
828
|
-
reviewGate: options.reviewGate,
|
|
829
|
-
reviewModel: options.acceptanceReviewModel,
|
|
830
|
-
reviewMaxRounds: 1,
|
|
831
|
-
// The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) —
|
|
832
|
-
// threaded so a wide-matrix CI's raised cap reaches the tasking-
|
|
833
|
-
// transition's land tail too (task
|
|
834
|
-
// `thread-merge-retries-cross-task-and-ratify-default`). Unset ⇒ falls
|
|
835
|
-
// through to the engine default (byte-for-byte unchanged).
|
|
836
|
-
mergeRetries: options.mergeRetries,
|
|
837
|
-
// The EXPLICITLY-chosen integrate mode proceeds AS-IS on an APPROVE — a
|
|
838
|
-
// `--merge` tasking run lands on main, `--propose` opens a PR. The tasking
|
|
839
|
-
// path's merge-vs-propose decision is the `integration` mode the user typed;
|
|
840
|
-
// `merge` IS the auto-land mode, so a resolved `merge` is never downgraded.
|
|
841
|
-
// The task gate family is `--review`/`--no-review`/`--review-model` only
|
|
842
|
-
// (spec US #6).
|
|
1100
|
+
reason,
|
|
1101
|
+
message,
|
|
1102
|
+
lockedBlob,
|
|
1103
|
+
release: params.release,
|
|
843
1104
|
mode: resolvedMode,
|
|
844
|
-
|
|
845
|
-
providerInstance: options.providerInstance,
|
|
846
|
-
// PROPOSE-MODE PR BODY (task `slicing-pr-body-summary-threading`): mirror the
|
|
847
|
-
// BUILD path's body threading (`do.ts` — `body: agent.output`). The tasker
|
|
848
|
-
// agent produces no `LaunchResult.output` we can carry, but the slice-SET
|
|
849
|
-
// itself is a summary-worthy artifact: the emitted task slugs+titles, their
|
|
850
|
-
// coverage of the spec's user stories, the dependency graph (keystone +
|
|
851
|
-
// blockedBy edges), and any `needsAnswers` open questions the tasker review
|
|
852
|
-
// loop flagged. Compose it here and thread it into the shared core as `body`
|
|
853
|
-
// so the propose-mode PR carries the summary instead of degrading to
|
|
854
|
-
// `gh pr create --fill`. Empty-set (no emitted tasks) ⇒ undefined ⇒ the
|
|
855
|
-
// provider's `--fill` fallback (no regression on that edge).
|
|
856
|
-
body: composeTaskingProposeBody(slug, emitTasks),
|
|
857
|
-
type: 'tasking',
|
|
858
|
-
lifecycle: {
|
|
859
|
-
// Read the PR title / commit summary from the held spec (before it moves).
|
|
860
|
-
titlePath: workItemPath(cwd, 'specs-ready', slug),
|
|
861
|
-
commitTag: 'tasked',
|
|
862
|
-
stage: () =>
|
|
863
|
-
stageTaskingLifecycle({
|
|
864
|
-
cwd,
|
|
865
|
-
slug,
|
|
866
|
-
emitTasks,
|
|
867
|
-
poolBefore,
|
|
868
|
-
placementDir,
|
|
869
|
-
note,
|
|
870
|
-
env,
|
|
871
|
-
}),
|
|
872
|
-
},
|
|
1105
|
+
provider: params.providerInstance,
|
|
873
1106
|
env,
|
|
874
|
-
// The task-SET acceptance review AGENT launches AMBIENT, never the
|
|
875
|
-
// identity-scoped `env` (an agent must not act as the bot).
|
|
876
|
-
agentEnv,
|
|
877
1107
|
note,
|
|
878
1108
|
});
|
|
879
1109
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
-
`parked it for your attention (no tasks landed; re-run).`,
|
|
953
|
-
lockedBlob,
|
|
954
|
-
release: lock.release,
|
|
955
|
-
mode: resolvedMode,
|
|
956
|
-
provider: options.providerInstance,
|
|
957
|
-
env,
|
|
958
|
-
note,
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
if (core.outcome === 'completed') {
|
|
962
|
-
// LOCK LIFETIME IS MODE-DEPENDENT (fix
|
|
963
|
-
// `propose-tasking-releases-lock-so-spec-is-retasked-and-pr-force-pushed-every-tick`;
|
|
964
|
-
// the "hold-across-the-PR" capstone the older interim half deferred).
|
|
965
|
-
//
|
|
966
|
-
// `merge`: the durable `specs/ready → specs/tasked` `main` move HAS landed, so
|
|
967
|
-
// residence-in-`specs/tasked/` on `main` now carries tasked-ness and the lock
|
|
968
|
-
// has done its job — RELEASE it (delete the ref). Best-effort + idempotent
|
|
969
|
-
// (`not-held` is fine).
|
|
970
|
-
//
|
|
971
|
-
// `propose`: the move lives ONLY on the pushed work branch / open PR — `main`
|
|
972
|
-
// still holds the spec in `work/specs/ready/`, so residence does NOT yet signal
|
|
973
|
-
// tasked-ness. If we released here, the next in-place scan would see the spec
|
|
974
|
-
// eligible again (ready + not-tasked + lock free) and re-task it EVERY tick,
|
|
975
|
-
// force-recreating the branch (`git switch -C`) and force-pushing the SAME PR —
|
|
976
|
-
// regenerating its review forever until a human merges/closes it. So we KEEP
|
|
977
|
-
// the `spec:<slug>` lock HELD across the open PR: the held lock IS the
|
|
978
|
-
// "in-flight tasking" marker, and `scoreSpecs`/`taskableSpecs` subtract held
|
|
979
|
-
// specs from the taskable pool (symmetric to the task-pool held-slug
|
|
980
|
-
// subtraction). The lock is reaped when the PR resolves: a MERGE lands the
|
|
981
|
-
// durable move (which now carries tasked-ness on its own) and the reap/gc path
|
|
982
|
-
// deletes the merged branch + its lock; a human CLOSING the PR unmerged is a
|
|
983
|
-
// recovery the human owns via `release-lock` (the same model as any other
|
|
984
|
-
// abandoned in-flight item — no liveness heartbeat/auto-steal).
|
|
985
|
-
//
|
|
986
|
-
// MIGRATE step: the ref is keyed under the `spec:<slug>` identity (the
|
|
987
|
-
// `spec-<slug>` lock entry the acquire took).
|
|
988
|
-
if (useLock && resolvedMode === 'merge') {
|
|
989
|
-
await releaseItemLock({item: `spec:${slug}`, cwd, arbiter, env});
|
|
990
|
-
}
|
|
1110
|
+
// THE TASK-SET ACCEPTANCE GATE BLOCKED (task `slice-acceptance-gate`): the
|
|
1111
|
+
// fresh-context review of the produced SET returned `block`, so the core ran
|
|
1112
|
+
// the review BEFORE the stage/integrate and did NOT integrate the tasks
|
|
1113
|
+
// (correct). The CORRECT task-path destination is the SAME needs-attention
|
|
1114
|
+
// route the lock release owns for the decomposition-unclear verdict: it surfaces
|
|
1115
|
+
// the spec and releases the `spec:<slug>` lock with the block reason. So on a
|
|
1116
|
+
// block we route the held spec to needs-attention THROUGH the lock release —
|
|
1117
|
+
// the set never lands.
|
|
1118
|
+
if (core.outcome === 'review-blocked') {
|
|
1119
|
+
return await surfaceBlock(
|
|
1120
|
+
taskGateBlockedReason(slug, core.reviewBlockReason),
|
|
1121
|
+
`The task acceptance gate disapproved the set produced for '${slug}'; ` +
|
|
1122
|
+
`parked it for your attention (no tasks landed). ` +
|
|
1123
|
+
`Resolve the findings, then re-task.`,
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
if (core.outcome === 'sidecar-violation') {
|
|
1127
|
+
// A co-located `<slug>/` asset sidecar sits beside the FLOWING spec item
|
|
1128
|
+
// (WORK-CONTRACT.md rule 8): the tasking transition would `git mv`
|
|
1129
|
+
// `specs/ready → specs/tasked` and STRAND the sidecar. The core HARD-BLOCKED
|
|
1130
|
+
// before the stage/integrate; route the held spec to needs-attention through
|
|
1131
|
+
// the SAME `spec:<slug>` lock-release seam the block path uses (no tasks land),
|
|
1132
|
+
// carrying the actionable relocate-to-`docs/spikes/<slug>/` reason.
|
|
1133
|
+
return await surfaceBlock(
|
|
1134
|
+
`The spec '${slug}' carries a co-located asset sidecar: ` +
|
|
1135
|
+
`${core.reviewBlockReason ?? core.reason ?? ''}`,
|
|
1136
|
+
`The spec '${slug}' carries a co-located asset sidecar (WORK-CONTRACT ` +
|
|
1137
|
+
`rule 8); parked it for your attention (no tasks landed; relocate it ` +
|
|
1138
|
+
`to docs/spikes/${slug}/ and reference by path).`,
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
if (core.outcome === 'review-unparseable') {
|
|
1142
|
+
// The task-set acceptance gate RAN but its verdict was UNPARSEABLE (malformed
|
|
1143
|
+
// JSON). Route the held spec to needs-attention through the SAME lock-release
|
|
1144
|
+
// seam the block path uses. It is NOT a block (the gate output was
|
|
1145
|
+
// unreadable) — record it as the transient-infra-class re-run signal so the
|
|
1146
|
+
// stuck reason reads correctly; nothing landed.
|
|
1147
|
+
return await surfaceBlock(
|
|
1148
|
+
taskGateUnparseableReason(slug, core.reason),
|
|
1149
|
+
`The task acceptance gate produced an unparseable verdict for '${slug}'; ` +
|
|
1150
|
+
`parked it for your attention (no tasks landed; re-run).`,
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
if (core.outcome === 'completed') {
|
|
1154
|
+
// LOCK LIFETIME IS MODE-DEPENDENT (fix
|
|
1155
|
+
// `propose-tasking-releases-lock-so-spec-is-retasked-and-pr-force-pushed-every-tick`;
|
|
1156
|
+
// the "hold-across-the-PR" capstone the older interim half deferred).
|
|
1157
|
+
//
|
|
1158
|
+
// `merge`: the durable `specs/ready → specs/tasked` `main` move HAS landed, so
|
|
1159
|
+
// residence-in-`specs/tasked/` on `main` now carries tasked-ness and the lock
|
|
1160
|
+
// has done its job — RELEASE it (delete the ref). Best-effort + idempotent
|
|
1161
|
+
// (`not-held` is fine).
|
|
1162
|
+
//
|
|
1163
|
+
// `propose`: the move lives ONLY on the pushed work branch / open PR — `main`
|
|
1164
|
+
// still holds the spec in `work/specs/ready/`, so residence does NOT yet signal
|
|
1165
|
+
// tasked-ness. If we released here, the next in-place scan would see the spec
|
|
1166
|
+
// eligible again (ready + not-tasked + lock free) and re-task it EVERY tick,
|
|
1167
|
+
// force-recreating the branch (`git switch -C`) and force-pushing the SAME PR —
|
|
1168
|
+
// regenerating its review forever until a human merges/closes it. So we KEEP
|
|
1169
|
+
// the `spec:<slug>` lock HELD across the open PR: the held lock IS the
|
|
1170
|
+
// "in-flight tasking" marker, and `scoreSpecs`/`taskableSpecs` subtract held
|
|
1171
|
+
// specs from the taskable pool (symmetric to the task-pool held-slug
|
|
1172
|
+
// subtraction). The lock is reaped when the PR resolves: a MERGE lands the
|
|
1173
|
+
// durable move (which now carries tasked-ness on its own) and the reap/gc path
|
|
1174
|
+
// deletes the merged branch + its lock; a human CLOSING the PR unmerged is a
|
|
1175
|
+
// recovery the human owns via `release-lock` (the same model as any other
|
|
1176
|
+
// abandoned in-flight item — no liveness heartbeat/auto-steal).
|
|
1177
|
+
//
|
|
1178
|
+
// MIGRATE step: the ref is keyed under the `spec:<slug>` identity (the
|
|
1179
|
+
// `spec-<slug>` lock entry the acquire took).
|
|
1180
|
+
if (resolvedMode === 'merge') {
|
|
1181
|
+
await releaseItemLock({item: `spec:${slug}`, cwd, arbiter, env});
|
|
991
1182
|
}
|
|
992
|
-
return integrationToTaskResult(core, {slug, emitted, loop: loopTag});
|
|
993
1183
|
}
|
|
994
|
-
|
|
995
|
-
// HUMAN, no-lock path: the human commits on `main` directly (the runner does
|
|
996
|
-
// not own the human's git). We report the produced tasks; moving the spec into
|
|
997
|
-
// `work/specs/tasked/` (residence = tasked-ness) and committing is the human's to
|
|
998
|
-
// do, as with the human `complete`.
|
|
999
|
-
const message =
|
|
1000
|
-
`Tasked '${slug}' -> ${emitted.length} backlog task` +
|
|
1001
|
-
`${emitted.length === 1 ? '' : 's'} (human path, no lock). Inspect + commit ` +
|
|
1002
|
-
`the produced files (and move the spec into work/specs/tasked/) yourself.`;
|
|
1003
|
-
note(message);
|
|
1004
|
-
return {
|
|
1005
|
-
exitCode: 0,
|
|
1006
|
-
outcome: 'tasked',
|
|
1007
|
-
slug,
|
|
1008
|
-
emitted,
|
|
1009
|
-
loop: loopTag,
|
|
1010
|
-
message,
|
|
1011
|
-
};
|
|
1184
|
+
return integrationToTaskResult(core, {slug, emitted, loop: loopTag});
|
|
1012
1185
|
}
|
|
1013
1186
|
|
|
1014
1187
|
/**
|
|
@@ -1059,10 +1232,17 @@ function integrationToTaskResult(
|
|
|
1059
1232
|
): TaskResult {
|
|
1060
1233
|
const {slug, emitted, loop} = ctx;
|
|
1061
1234
|
if (core.outcome === 'completed') {
|
|
1235
|
+
// Say "opened a PR" ONLY when the provider opened one (task
|
|
1236
|
+
// `intake-reports-the-pr-it-actually-opened`); a degraded `gh pr create`
|
|
1237
|
+
// pushed the branch but opened nothing, and the shared core printed why.
|
|
1238
|
+
const notOpened = proposeRequestNotOpened(core.integration);
|
|
1062
1239
|
const landed =
|
|
1063
1240
|
core.integration?.mode === 'merge'
|
|
1064
1241
|
? 'landed them on the arbiter main'
|
|
1065
|
-
:
|
|
1242
|
+
: notOpened !== undefined
|
|
1243
|
+
? `pushed them on branch ${core.integration?.pushedRef ?? core.branch} ` +
|
|
1244
|
+
'but opened NO PR (main untouched; see the provider note above)'
|
|
1245
|
+
: 'opened a PR carrying them (main untouched)';
|
|
1066
1246
|
const message =
|
|
1067
1247
|
`Tasked '${slug}' -> ${emitted.length} backlog task` +
|
|
1068
1248
|
`${emitted.length === 1 ? '' : 's'}; the runner integrated the transition ` +
|
|
@@ -1100,11 +1280,18 @@ function integrationToTaskResult(
|
|
|
1100
1280
|
* pre-existing local `work/<slug>` (a re-run) is force-recreated off fresh main.
|
|
1101
1281
|
* The agent runs in-place on this branch (branch ≠ worktree).
|
|
1102
1282
|
*/
|
|
1103
|
-
async function switchToWorkBranch(
|
|
1283
|
+
export async function switchToWorkBranch(
|
|
1104
1284
|
cwd: string,
|
|
1105
1285
|
arbiter: string,
|
|
1106
1286
|
slug: string,
|
|
1107
1287
|
env: NodeJS.ProcessEnv | undefined,
|
|
1288
|
+
/**
|
|
1289
|
+
* The commit to cut the branch from; default the freshly fetched
|
|
1290
|
+
* `<arbiter>/main`. The CI phases pass the lock job's `baseSha` (task
|
|
1291
|
+
* `ci-split-tasking`), so the agent and apply jobs work on the tree the lock
|
|
1292
|
+
* job classified.
|
|
1293
|
+
*/
|
|
1294
|
+
base?: string,
|
|
1108
1295
|
): Promise<void> {
|
|
1109
1296
|
// The tasking path is the parent-spec namespace (`do spec:<slug>`): the branch
|
|
1110
1297
|
// is `work/spec-<slug>`, distinct from a same-slug task-build's `work/task-<slug>`.
|
|
@@ -1114,7 +1301,7 @@ async function switchToWorkBranch(
|
|
|
1114
1301
|
const branch = workBranchRef('spec', slug);
|
|
1115
1302
|
await gitHard(['fetch', '--quiet', arbiter], cwd, env);
|
|
1116
1303
|
await gitHard(
|
|
1117
|
-
['switch', '--quiet', '-C', branch, `${arbiter}/main`],
|
|
1304
|
+
['switch', '--quiet', '-C', branch, base ?? `${arbiter}/main`],
|
|
1118
1305
|
cwd,
|
|
1119
1306
|
env,
|
|
1120
1307
|
);
|
|
@@ -1331,7 +1518,7 @@ async function gitHard(
|
|
|
1331
1518
|
* generic line when absent. DISTINCT from the improver loop's
|
|
1332
1519
|
* {@link decompositionUnclearReason} (which carries the loop's open questions).
|
|
1333
1520
|
*/
|
|
1334
|
-
function taskGateBlockedReason(
|
|
1521
|
+
export function taskGateBlockedReason(
|
|
1335
1522
|
slug: string,
|
|
1336
1523
|
findingsReason: string | undefined,
|
|
1337
1524
|
): string {
|
|
@@ -1342,6 +1529,21 @@ function taskGateBlockedReason(
|
|
|
1342
1529
|
return findingsReason ? `${head}\n\n${findingsReason}` : head;
|
|
1343
1530
|
}
|
|
1344
1531
|
|
|
1532
|
+
/**
|
|
1533
|
+
* The needs-attention REASON for a task-SET ACCEPTANCE GATE whose verdict was
|
|
1534
|
+
* UNPARSEABLE (the gate ran; its output could not be read): NOT a block, the
|
|
1535
|
+
* transient-infra re-run signal. `detail` is the gate's parse-failure reason.
|
|
1536
|
+
*/
|
|
1537
|
+
export function taskGateUnparseableReason(
|
|
1538
|
+
slug: string,
|
|
1539
|
+
detail: string | undefined,
|
|
1540
|
+
): string {
|
|
1541
|
+
return (
|
|
1542
|
+
`The task acceptance gate for '${slug}' produced an UNPARSEABLE verdict ` +
|
|
1543
|
+
`(re-run — transient): ${detail ?? ''}`
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1345
1547
|
/**
|
|
1346
1548
|
* Shared terminal for a tasking transition that DID NOT land — a disapproving
|
|
1347
1549
|
* task-set review, a co-located sidecar violation, an unparseable verdict, or a
|
|
@@ -1363,7 +1565,7 @@ function taskGateBlockedReason(
|
|
|
1363
1565
|
* is GREEN — a park-for-human is not a failure). Only a surface that FAILED to
|
|
1364
1566
|
* publish maps to a non-zero {@link releaseFailureToResult}.
|
|
1365
1567
|
*/
|
|
1366
|
-
async function surfaceTaskingBlock(params: {
|
|
1568
|
+
export async function surfaceTaskingBlock(params: {
|
|
1367
1569
|
slug: string;
|
|
1368
1570
|
cwd: string;
|
|
1369
1571
|
arbiter: string;
|
|
@@ -1427,7 +1629,10 @@ async function surfaceTaskingBlock(params: {
|
|
|
1427
1629
|
* spec is parked for the human with these open questions + a question sidecar on
|
|
1428
1630
|
* main, no guessed tasks). Prose only — recorded as the parked question's reason.
|
|
1429
1631
|
*/
|
|
1430
|
-
function decompositionUnclearReason(
|
|
1632
|
+
export function decompositionUnclearReason(
|
|
1633
|
+
slug: string,
|
|
1634
|
+
questions: string[],
|
|
1635
|
+
): string {
|
|
1431
1636
|
const head =
|
|
1432
1637
|
`The tasker review→edit loop could not converge on a sound decomposition of ` +
|
|
1433
1638
|
`'${slug}' (--tasker-loop-max exhausted with unresolved blockers). The spec is parked ` +
|
|
@@ -1450,7 +1655,7 @@ function decompositionUnclearReason(slug: string, questions: string[]): string {
|
|
|
1450
1655
|
* The candidate tasks the tasker produced are PERSISTED on the work branch (see
|
|
1451
1656
|
* {@link persistTaskingCandidates}) so a human can recover them.
|
|
1452
1657
|
*/
|
|
1453
|
-
function reviewFailureReason(
|
|
1658
|
+
export function reviewFailureReason(
|
|
1454
1659
|
slug: string,
|
|
1455
1660
|
err: ReviewParseError,
|
|
1456
1661
|
candidatePaths: string[],
|
|
@@ -1476,7 +1681,10 @@ function reviewFailureReason(
|
|
|
1476
1681
|
}
|
|
1477
1682
|
|
|
1478
1683
|
/** The human-readable terminal MESSAGE for a review-leg failure (the result's `message`). */
|
|
1479
|
-
function reviewFailureMessage(
|
|
1684
|
+
export function reviewFailureMessage(
|
|
1685
|
+
slug: string,
|
|
1686
|
+
err: ReviewParseError,
|
|
1687
|
+
): string {
|
|
1480
1688
|
if (err instanceof ReviewOutputCappedError) {
|
|
1481
1689
|
return (
|
|
1482
1690
|
`Tasking '${slug}' parked: the review leg hit the model output cap ` +
|
|
@@ -1502,7 +1710,7 @@ function reviewFailureMessage(slug: string, err: ReviewParseError): string {
|
|
|
1502
1710
|
* candidate paths persisted (empty when there were none). Never throws — a git
|
|
1503
1711
|
* failure is noted and the bounce proceeds (the lock is still released).
|
|
1504
1712
|
*/
|
|
1505
|
-
async function persistTaskingCandidates(
|
|
1713
|
+
export async function persistTaskingCandidates(
|
|
1506
1714
|
cwd: string,
|
|
1507
1715
|
slug: string,
|
|
1508
1716
|
before: Map<string, string>,
|
|
@@ -1545,7 +1753,12 @@ async function persistTaskingCandidates(
|
|
|
1545
1753
|
// teardown on an isolated run (the branch ref survives locally too, but a push
|
|
1546
1754
|
// makes them remote-recoverable). A push failure is noted, never fatal.
|
|
1547
1755
|
try {
|
|
1548
|
-
const pushed = await
|
|
1756
|
+
const pushed = await refWrite.pushTaskingCandidatesBranch({
|
|
1757
|
+
arbiter,
|
|
1758
|
+
branch,
|
|
1759
|
+
cwd,
|
|
1760
|
+
env,
|
|
1761
|
+
});
|
|
1549
1762
|
if (pushed.status !== 0) {
|
|
1550
1763
|
note(
|
|
1551
1764
|
`Could not push the candidate-tasks work branch '${branch}' to ${arbiter} ` +
|
|
@@ -1619,7 +1832,7 @@ function appendQuestionsBlock(content: string, questions: string[]): string {
|
|
|
1619
1832
|
* cross-spec `taskedAfter` ordering, resolved against `work/specs/tasked/` residence of
|
|
1620
1833
|
* the specs present in the checkout.
|
|
1621
1834
|
*/
|
|
1622
|
-
function resolveAgentGate(
|
|
1835
|
+
export function resolveAgentGate(
|
|
1623
1836
|
cwd: string,
|
|
1624
1837
|
slug: string,
|
|
1625
1838
|
specFm: {humanOnly?: boolean; needsAnswers?: boolean; taskedAfter: string[]},
|
|
@@ -1637,7 +1850,7 @@ function resolveAgentGate(
|
|
|
1637
1850
|
}
|
|
1638
1851
|
|
|
1639
1852
|
/** Build an HONEST gate-refusal message naming WHY the agent skipped the spec. */
|
|
1640
|
-
function gateRefusalReason(
|
|
1853
|
+
export function gateRefusalReason(
|
|
1641
1854
|
slug: string,
|
|
1642
1855
|
specFm: {humanOnly?: boolean; needsAnswers?: boolean},
|
|
1643
1856
|
eligibility: TaskingEligibilityResult,
|
|
@@ -1796,7 +2009,7 @@ async function runTaskAgent(
|
|
|
1796
2009
|
}
|
|
1797
2010
|
|
|
1798
2011
|
/** A snapshot of {@link STAGED_TASKS_DIR}: filename → file content. */
|
|
1799
|
-
function snapshotStagedTasks(cwd: string): Map<string, string> {
|
|
2012
|
+
export function snapshotStagedTasks(cwd: string): Map<string, string> {
|
|
1800
2013
|
const dir = join(cwd, STAGED_TASKS_DIR);
|
|
1801
2014
|
const snap = new Map<string, string>();
|
|
1802
2015
|
for (const file of listMarkdown(dir)) {
|
|
@@ -1812,7 +2025,7 @@ function snapshotStagedTasks(cwd: string): Map<string, string> {
|
|
|
1812
2025
|
* re-committed.) The agent's staging folder is `work/tasks/backlog/`; writes to
|
|
1813
2026
|
* the pool `work/tasks/ready/` are scrubbed at stage time, never picked up here.
|
|
1814
2027
|
*/
|
|
1815
|
-
function newOrChangedStagedTasks(
|
|
2028
|
+
export function newOrChangedStagedTasks(
|
|
1816
2029
|
cwd: string,
|
|
1817
2030
|
before: Map<string, string>,
|
|
1818
2031
|
): string[] {
|
|
@@ -1828,7 +2041,7 @@ function newOrChangedStagedTasks(
|
|
|
1828
2041
|
}
|
|
1829
2042
|
|
|
1830
2043
|
/** Snapshot the POOL `work/tasks/ready/` (for the agent-write fence at stage time). */
|
|
1831
|
-
function snapshotPool(cwd: string): Map<string, string> {
|
|
2044
|
+
export function snapshotPool(cwd: string): Map<string, string> {
|
|
1832
2045
|
const dir = workFolderPath(cwd, 'tasks-ready');
|
|
1833
2046
|
const snap = new Map<string, string>();
|
|
1834
2047
|
for (const file of listMarkdown(dir)) {
|
|
@@ -1838,7 +2051,7 @@ function snapshotPool(cwd: string): Map<string, string> {
|
|
|
1838
2051
|
}
|
|
1839
2052
|
|
|
1840
2053
|
/** Read the produced backlog tasks' content keyed by repo-relative path. */
|
|
1841
|
-
function collectEmittedTasks(
|
|
2054
|
+
export function collectEmittedTasks(
|
|
1842
2055
|
cwd: string,
|
|
1843
2056
|
relPaths: string[],
|
|
1844
2057
|
): Record<string, string> {
|
|
@@ -2001,12 +2214,14 @@ function readTitleField(content: string): string | undefined {
|
|
|
2001
2214
|
if (!m) continue;
|
|
2002
2215
|
let value = m[1].trim();
|
|
2003
2216
|
if (value === '') return undefined;
|
|
2004
|
-
// Quoted ⇒ return inner text
|
|
2217
|
+
// Quoted ⇒ return the inner text (a single-quoted `''` decodes to `'`, the
|
|
2218
|
+
// shape `quoteYamlScalar` writes); unquoted ⇒ strip trailing `# ...`.
|
|
2005
2219
|
if (
|
|
2006
|
-
|
|
2007
|
-
(value.startsWith("'
|
|
2220
|
+
value.length >= 2 &&
|
|
2221
|
+
((value.startsWith('"') && value.endsWith('"')) ||
|
|
2222
|
+
(value.startsWith("'") && value.endsWith("'")))
|
|
2008
2223
|
) {
|
|
2009
|
-
return value
|
|
2224
|
+
return unquoteYamlScalar(value);
|
|
2010
2225
|
}
|
|
2011
2226
|
const hash = value.indexOf('#');
|
|
2012
2227
|
if (hash !== -1) value = value.slice(0, hash).trimEnd();
|