dorfl 0.14.3 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advance-ci-template.d.ts +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 +305 -486
- 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 +44 -13
- 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 +25 -0
- package/dist/install-ci-action-pins.d.ts.map +1 -1
- package/dist/install-ci-action-pins.js +25 -0
- package/dist/install-ci-action-pins.js.map +1 -1
- 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 +173 -36
- package/dist/install-ci-core.d.ts.map +1 -1
- package/dist/install-ci-core.js +379 -186
- 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 -293
- 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 +6 -3
- 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 +371 -535
- 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 +50 -14
- 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 +25 -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 +458 -218
- 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 -328
- 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 +6 -3
- package/src/verify.ts +4 -0
- package/src/watch-session.ts +86 -28
- package/src/work-layout.ts +11 -3
- package/src/workspace.ts +210 -14
package/dist/tasking.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import { basename, dirname, join } from 'node:path';
|
|
3
|
-
import { parseFrontmatter } from './frontmatter.js';
|
|
3
|
+
import { parseFrontmatter, unquoteYamlScalar } from './frontmatter.js';
|
|
4
4
|
import { runAsync } from './git.js';
|
|
5
5
|
import { workFolderRel, workFolderPath, workItemPath, workItemRel, isWorkItemFile, } from './work-layout.js';
|
|
6
|
-
import { performIntegration, } from './integration-core.js';
|
|
6
|
+
import { performIntegration, proposePointer, } from './integration-core.js';
|
|
7
|
+
import { proposeRequestNotOpened } from './integrator.js';
|
|
7
8
|
import { resolveTaskingEligibility, } from './tasking-eligibility.js';
|
|
8
9
|
import { acquireTaskingLock, releaseTaskingLock, } from './tasking-lock.js';
|
|
9
10
|
import { releaseItemLock } from './item-lock.js';
|
|
11
|
+
import { refWrite } from './ref-write.js';
|
|
10
12
|
import { NullHarness } from './harness.js';
|
|
11
13
|
import { launchWithOptionalWatch } from './agent-launch.js';
|
|
12
14
|
import { placementFolder, resolvePlacement } from './placement.js';
|
|
@@ -162,33 +164,19 @@ export async function performTask(options) {
|
|
|
162
164
|
if (useLock) {
|
|
163
165
|
await switchToWorkBranch(cwd, arbiter, slug, env);
|
|
164
166
|
}
|
|
165
|
-
// 3.
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const prompt = buildTaskingSpec(slug, specFm.spec);
|
|
179
|
-
let agent;
|
|
180
|
-
try {
|
|
181
|
-
agent = await runTaskAgent(options, cwd, prompt, slug);
|
|
182
|
-
}
|
|
183
|
-
catch (err) {
|
|
184
|
-
agent = {
|
|
185
|
-
ok: false,
|
|
186
|
-
detail: err instanceof Error ? err.message : String(err),
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
if (!agent.ok) {
|
|
190
|
-
const detail = agent.detail ?? `the agent failed to task '${slug}'.`;
|
|
191
|
-
let message = `Agent failed tasking '${slug}' (${detail}).`;
|
|
167
|
+
// 3. + 3.5 THE AGENT HALF (task `ci-split-tasking`): the tasker agent and the
|
|
168
|
+
// tasker review→edit→converge loop. Shared, unchanged, with the CI agent
|
|
169
|
+
// phase (`ci-phase-tasking.ts`), which runs this SAME half in the job that
|
|
170
|
+
// holds no write token and hands the result over instead of integrating.
|
|
171
|
+
const half = await runTaskingAgentHalf(options, {
|
|
172
|
+
cwd,
|
|
173
|
+
slug,
|
|
174
|
+
specFm,
|
|
175
|
+
doer,
|
|
176
|
+
note,
|
|
177
|
+
});
|
|
178
|
+
if (half.kind === 'agent-failed') {
|
|
179
|
+
let message = `Agent failed tasking '${slug}' (${half.detail}).`;
|
|
192
180
|
// RELEASE THE LOCK THE FAILED RUN TOOK (observation
|
|
193
181
|
// `crashed-do-spec-strands-a-tasking-lock-no-verb-releases`).
|
|
194
182
|
//
|
|
@@ -243,13 +231,164 @@ export async function performTask(options) {
|
|
|
243
231
|
note(message);
|
|
244
232
|
return { exitCode: 1, outcome: 'agent-failed', slug, message };
|
|
245
233
|
}
|
|
234
|
+
if (half.kind === 'review-leg-failed') {
|
|
235
|
+
// REVIEW-LEG FAILURE handling (observation
|
|
236
|
+
// `tasker-review-edits-payload-caps-the-verdict-response`): PERSIST the
|
|
237
|
+
// candidate tasks, and BOUNCE through the SAME surface the
|
|
238
|
+
// decomposition-unclear path uses (release the lock + a question sidecar).
|
|
239
|
+
// NEVER a silent approve — a parse failure is always a needs-attention route.
|
|
240
|
+
const candidatePaths = await persistTaskingCandidates(cwd, slug, half.before, arbiter, env, note);
|
|
241
|
+
const reason = reviewFailureReason(slug, half.error, candidatePaths);
|
|
242
|
+
const message = reviewFailureMessage(slug, half.error);
|
|
243
|
+
if (useLock) {
|
|
244
|
+
return await surfaceTaskingBlock({
|
|
245
|
+
slug,
|
|
246
|
+
cwd,
|
|
247
|
+
arbiter,
|
|
248
|
+
reason,
|
|
249
|
+
message,
|
|
250
|
+
lockedBlob,
|
|
251
|
+
release: lock.release,
|
|
252
|
+
mode: resolvedMode,
|
|
253
|
+
provider: options.providerInstance,
|
|
254
|
+
env,
|
|
255
|
+
note,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
note(reason);
|
|
259
|
+
// Human, no-lock path: a clean park-for-human is a success terminal (exit 0).
|
|
260
|
+
return { exitCode: 0, outcome: 'needs-attention', slug, message };
|
|
261
|
+
}
|
|
262
|
+
if (half.kind === 'decomposition-unclear') {
|
|
263
|
+
// DECOMPOSITION UNCLEAR: emit NO guessed tasks — route the held spec to
|
|
264
|
+
// needs-attention with the questions as the reason. The lock release surfaces
|
|
265
|
+
// the spec (a question sidecar + `needsAnswers: true` on main) and releases the
|
|
266
|
+
// `spec:<slug>` lock; the spec body stays in `work/specs/ready/`.
|
|
267
|
+
const reason = decompositionUnclearReason(slug, half.questions);
|
|
268
|
+
if (useLock) {
|
|
269
|
+
return await surfaceTaskingBlock({
|
|
270
|
+
slug,
|
|
271
|
+
cwd,
|
|
272
|
+
arbiter,
|
|
273
|
+
reason,
|
|
274
|
+
message: half.message,
|
|
275
|
+
lockedBlob,
|
|
276
|
+
release: lock.release,
|
|
277
|
+
mode: resolvedMode,
|
|
278
|
+
provider: options.providerInstance,
|
|
279
|
+
env,
|
|
280
|
+
note,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
note(half.message);
|
|
284
|
+
// Human, no-lock path: nothing to release/surface via the seam; a clean
|
|
285
|
+
// park-for-human is still a success terminal (exit 0).
|
|
286
|
+
return {
|
|
287
|
+
exitCode: 0,
|
|
288
|
+
outcome: 'needs-attention',
|
|
289
|
+
slug,
|
|
290
|
+
message: half.message,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
// 4. The RUNNER commits the COMPLETING transition: drop the produced backlog
|
|
294
|
+
// tasks IN + move the spec specs/ready/ -> specs/tasked/ (residence = tasked-ness) — now
|
|
295
|
+
// through the SHARED integrate core (`--propose` PR / `--merge` main), NOT a
|
|
296
|
+
// direct commit to `main`. The agent never does git. (The backlog snapshot is
|
|
297
|
+
// taken AFTER any loop edits, so the runner integrates the IMPROVED tasks,
|
|
298
|
+
// not the pre-loop candidates.)
|
|
299
|
+
const stagedEmitted = newOrChangedStagedTasks(cwd, half.before);
|
|
300
|
+
const emitTasks = collectEmittedTasks(cwd, stagedEmitted);
|
|
301
|
+
const placementDir = taskPlacementDir(options, specFm);
|
|
302
|
+
const loopTag = half.loopTag;
|
|
303
|
+
if (useLock) {
|
|
304
|
+
return await integrateTaskingCandidates({
|
|
305
|
+
slug,
|
|
306
|
+
cwd,
|
|
307
|
+
arbiter,
|
|
308
|
+
stagedEmitted,
|
|
309
|
+
emitTasks,
|
|
310
|
+
poolBefore: half.poolBefore,
|
|
311
|
+
placementDir,
|
|
312
|
+
loopTag,
|
|
313
|
+
lockedBlob,
|
|
314
|
+
release: lock.release,
|
|
315
|
+
mode: resolvedMode,
|
|
316
|
+
review: options.review,
|
|
317
|
+
reviewGate: options.reviewGate,
|
|
318
|
+
acceptanceReviewModel: options.acceptanceReviewModel,
|
|
319
|
+
mergeRetries: options.mergeRetries,
|
|
320
|
+
noPR: options.noPR,
|
|
321
|
+
providerInstance: options.providerInstance,
|
|
322
|
+
env,
|
|
323
|
+
agentEnv,
|
|
324
|
+
note,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
// HUMAN, no-lock path: the human commits on `main` directly (the runner does
|
|
328
|
+
// not own the human's git). We report the produced tasks; moving the spec into
|
|
329
|
+
// `work/specs/tasked/` (residence = tasked-ness) and committing is the human's to
|
|
330
|
+
// do, as with the human `complete`.
|
|
331
|
+
const emitted = stagedEmitted.map((rel) => `${placementDir}/${basename(rel)}`);
|
|
332
|
+
const message = `Tasked '${slug}' -> ${emitted.length} backlog task` +
|
|
333
|
+
`${emitted.length === 1 ? '' : 's'} (human path, no lock). Inspect + commit ` +
|
|
334
|
+
`the produced files (and move the spec into work/specs/tasked/) yourself.`;
|
|
335
|
+
note(message);
|
|
336
|
+
return {
|
|
337
|
+
exitCode: 0,
|
|
338
|
+
outcome: 'tasked',
|
|
339
|
+
slug,
|
|
340
|
+
emitted,
|
|
341
|
+
loop: loopTag,
|
|
342
|
+
message,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* The AGENT half of the tasking path (steps 3 and 3.5): run the tasker agent in
|
|
347
|
+
* `cwd` (on the work branch), then, when a loop seam is wired and the doer is
|
|
348
|
+
* the agent, the tasker review→edit→converge loop, and report what is on disk.
|
|
349
|
+
* It performs NO write to the arbiter and no git transition: only the agents'
|
|
350
|
+
* file edits, the loop's local edits and markers, and the scratch cleanup.
|
|
351
|
+
*/
|
|
352
|
+
export async function runTaskingAgentHalf(options, params) {
|
|
353
|
+
const { cwd, slug, specFm, doer, note } = params;
|
|
354
|
+
const agentEnv = options.agentEnv ?? options.env;
|
|
355
|
+
// 3. INVOKE THE AGENT with the to-task spec. It WRITES
|
|
356
|
+
// `work/tasks/backlog/*.md` task files (the STAGED area — NOT `work/tasks/ready/`,
|
|
357
|
+
// which is the agent-eligible pool the runner owns the promotion into; task
|
|
358
|
+
// `pre-backlog-staging-folder-and-promote-step-a`); it does NO git. We
|
|
359
|
+
// snapshot the staged-tasks folder before/after so the runner (not the
|
|
360
|
+
// agent) captures + commits exactly what was produced.
|
|
361
|
+
const before = snapshotStagedTasks(cwd);
|
|
362
|
+
// Also snapshot the POOL `work/tasks/ready/` BEFORE the agent runs: the runner's
|
|
363
|
+
// final commit must scrub any agent writes there (an attempt to self-place into
|
|
364
|
+
// the pool, spec US #4) before `git add -A` would sweep them in.
|
|
365
|
+
const poolBefore = snapshotPool(cwd);
|
|
366
|
+
// Read the parent-spec self-pointer off `specFm.spec` (populated from the
|
|
367
|
+
// `spec:` key).
|
|
368
|
+
const prompt = buildTaskingSpec(slug, specFm.spec);
|
|
369
|
+
let agent;
|
|
370
|
+
try {
|
|
371
|
+
agent = await runTaskAgent(options, cwd, prompt, slug);
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
agent = {
|
|
375
|
+
ok: false,
|
|
376
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
if (!agent.ok) {
|
|
380
|
+
return {
|
|
381
|
+
kind: 'agent-failed',
|
|
382
|
+
detail: agent.detail ?? `the agent failed to task '${slug}'.`,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
246
385
|
// 3.5 THE TASKER REVIEW→EDIT→CONVERGE LOOP (`slicer-review-edit-loop`, Shape 2 /
|
|
247
386
|
// insertion point A): when a loop seam is wired, run the `review` SKILL as a
|
|
248
387
|
// review→edit→re-review loop that IMPROVES the candidate tasks in place, then
|
|
249
388
|
// determines the disposition (the three outcomes). This plugs in AFTER the
|
|
250
389
|
// candidate tasks are produced and BEFORE they are finalised. The agent makes
|
|
251
390
|
// the review/edit JUDGEMENTS; the loop applies edits to the candidate files
|
|
252
|
-
// and routes the verdict; the runner
|
|
391
|
+
// and routes the verdict; the runner owns the git transition. Only the
|
|
253
392
|
// AGENT path runs the loop — the human tasking path is unaffected.
|
|
254
393
|
let loopDisposition;
|
|
255
394
|
if (options.reviewLoop && doer === 'agent') {
|
|
@@ -259,10 +398,8 @@ export async function performTask(options) {
|
|
|
259
398
|
// parseable verdict (a generic parse failure, or the NAMED cap-truncation
|
|
260
399
|
// class). The pre-fix behaviour THREW here, crashing the run, LEAVING THE
|
|
261
400
|
// LOCK HELD, writing NO question sidecar, and DISCARDING the tasker's
|
|
262
|
-
// candidate tasks. Now: catch it,
|
|
263
|
-
//
|
|
264
|
-
// lock + a question sidecar). NEVER a silent approve — a parse failure is
|
|
265
|
-
// always a needs-attention route. A non-review throw (genuine wiring error)
|
|
401
|
+
// candidate tasks. Now: catch it and report it, so the write half PERSISTS
|
|
402
|
+
// the candidate tasks and BOUNCES. A non-review throw (genuine wiring error)
|
|
266
403
|
// is re-thrown.
|
|
267
404
|
try {
|
|
268
405
|
loopDisposition = await runTaskReviewLoop({
|
|
@@ -286,7 +423,6 @@ export async function performTask(options) {
|
|
|
286
423
|
if (!(err instanceof ReviewParseError)) {
|
|
287
424
|
throw err;
|
|
288
425
|
}
|
|
289
|
-
const reviewErr = err;
|
|
290
426
|
// Reap any review-edits SCRATCH the failed pass left on disk so it is never
|
|
291
427
|
// swept into a later run's integrate commit.
|
|
292
428
|
try {
|
|
@@ -298,56 +434,12 @@ export async function performTask(options) {
|
|
|
298
434
|
catch {
|
|
299
435
|
// Best-effort.
|
|
300
436
|
}
|
|
301
|
-
|
|
302
|
-
const reason = reviewFailureReason(slug, reviewErr, candidatePaths);
|
|
303
|
-
const message = reviewFailureMessage(slug, reviewErr);
|
|
304
|
-
if (useLock) {
|
|
305
|
-
return await surfaceTaskingBlock({
|
|
306
|
-
slug,
|
|
307
|
-
cwd,
|
|
308
|
-
arbiter,
|
|
309
|
-
reason,
|
|
310
|
-
message,
|
|
311
|
-
lockedBlob,
|
|
312
|
-
release: lock.release,
|
|
313
|
-
mode: resolvedMode,
|
|
314
|
-
provider: options.providerInstance,
|
|
315
|
-
env,
|
|
316
|
-
note,
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
note(reason);
|
|
320
|
-
// Human, no-lock path: a clean park-for-human is a success terminal (exit 0).
|
|
321
|
-
return { exitCode: 0, outcome: 'needs-attention', slug, message };
|
|
437
|
+
return { kind: 'review-leg-failed', error: err, before };
|
|
322
438
|
}
|
|
323
|
-
// DECOMPOSITION UNCLEAR: emit NO guessed tasks — route the held spec to
|
|
324
|
-
// needs-attention with the questions as the reason. The lock release amends the
|
|
325
|
-
// `spec:<slug>` unified lock `active → stuck` (the tasking needs-attention surface
|
|
326
|
-
// is the stuck lock now — NO folder write; the spec body stays in `work/specs/ready/`).
|
|
327
439
|
if (loopDisposition.outcome === 'decomposition-unclear') {
|
|
328
|
-
const reason = decompositionUnclearReason(slug, loopDisposition.specQuestions);
|
|
329
|
-
if (useLock) {
|
|
330
|
-
return await surfaceTaskingBlock({
|
|
331
|
-
slug,
|
|
332
|
-
cwd,
|
|
333
|
-
arbiter,
|
|
334
|
-
reason,
|
|
335
|
-
message: loopDisposition.message,
|
|
336
|
-
lockedBlob,
|
|
337
|
-
release: lock.release,
|
|
338
|
-
mode: resolvedMode,
|
|
339
|
-
provider: options.providerInstance,
|
|
340
|
-
env,
|
|
341
|
-
note,
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
note(loopDisposition.message);
|
|
345
|
-
// Human, no-lock path: nothing to release/surface via the seam; a clean
|
|
346
|
-
// park-for-human is still a success terminal (exit 0).
|
|
347
440
|
return {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
slug,
|
|
441
|
+
kind: 'decomposition-unclear',
|
|
442
|
+
questions: loopDisposition.specQuestions,
|
|
351
443
|
message: loopDisposition.message,
|
|
352
444
|
};
|
|
353
445
|
}
|
|
@@ -360,30 +452,34 @@ export async function performTask(options) {
|
|
|
360
452
|
}
|
|
361
453
|
}
|
|
362
454
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
455
|
+
return {
|
|
456
|
+
kind: 'candidates',
|
|
457
|
+
before,
|
|
458
|
+
poolBefore,
|
|
459
|
+
loopTag: loopDisposition?.outcome === 'converged'
|
|
460
|
+
? 'converged'
|
|
461
|
+
: loopDisposition?.outcome === 'uncertain-tasks'
|
|
462
|
+
? 'uncertain-tasks'
|
|
463
|
+
: undefined,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* The RUNNER-DETERMINISTIC PLACEMENT folder for a spec's emitted tasks (task
|
|
468
|
+
* `runner-deterministic-slice-placement-policy-and-precedence`; the
|
|
469
|
+
* untrusted-forces-staging rung was RETIRED by ADR
|
|
470
|
+
* `untrusted-origin-carries-via-stamp-not-forced-staging`): precedence
|
|
471
|
+
* `explicit > configured default > built-in (staging)`. This caller selects the
|
|
472
|
+
* trusted-vs-untrusted configured default by reading the spec's propagated
|
|
473
|
+
* `originTrust:` stamp: an untrusted-origin spec selects `untrustedTasksLandIn`
|
|
474
|
+
* (default staging; `ready` when configured), a trusted/unset spec selects
|
|
475
|
+
* `tasksLandIn`. One knob (`untrustedTasksLandIn`) governs an untrusted task's
|
|
476
|
+
* destination whether born from an issue directly (intake) or from an untrusted
|
|
477
|
+
* spec here. The agent NEVER influences this; it always writes to
|
|
478
|
+
* `work/tasks/backlog/`, and the runner redirects at `stage()` time. Safety for
|
|
479
|
+
* an untrusted task landing in `ready` is the carried stamp (its BUILD is forced
|
|
480
|
+
* to a code PR), not the folder.
|
|
481
|
+
*/
|
|
482
|
+
export function taskPlacementDir(options, specFm) {
|
|
387
483
|
const configuredTasksLanding = specFm.originTrust === 'untrusted'
|
|
388
484
|
? options.untrustedTasksLandIn
|
|
389
485
|
: options.tasksLandIn;
|
|
@@ -391,247 +487,215 @@ export async function performTask(options) {
|
|
|
391
487
|
explicit: landingToSide(options.explicitTasksLandIn),
|
|
392
488
|
configuredDefault: landingToSide(configuredTasksLanding),
|
|
393
489
|
});
|
|
394
|
-
|
|
490
|
+
return placementFolder(TASK_PLACEMENT_SLOTS, placementDecision.choice);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* The WRITE half of a tasking run that produced candidate tasks (step 4): the
|
|
494
|
+
* read-stability stale check, the integrate through the shared core (the tasks
|
|
495
|
+
* + the `specs/ready → specs/tasked` move, `--propose` PR / `--merge` main), the
|
|
496
|
+
* needs-attention routing of a not-landed transition, and the mode-dependent lock
|
|
497
|
+
* release. Shared by the laptop `performTask` (review ON inside the core) and the
|
|
498
|
+
* CI apply phase (`ci-phase-tasking.ts`, review OFF: it ran in the agent phase).
|
|
499
|
+
*/
|
|
500
|
+
export async function integrateTaskingCandidates(params) {
|
|
501
|
+
const { slug, cwd, arbiter, stagedEmitted, emitTasks, poolBefore, placementDir, loopTag, lockedBlob, mode: resolvedMode, env, agentEnv, note, } = params;
|
|
395
502
|
// REWRITE the emitted list to the RUNNER-RESOLVED destination so callers see
|
|
396
503
|
// where the runner actually placed the files (not where the agent wrote them).
|
|
397
504
|
const emitted = stagedEmitted.map((rel) => `${placementDir}/${basename(rel)}`);
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
poolBefore,
|
|
499
|
-
placementDir,
|
|
500
|
-
note,
|
|
501
|
-
env,
|
|
502
|
-
}),
|
|
503
|
-
},
|
|
504
|
-
env,
|
|
505
|
-
// The task-SET acceptance review AGENT launches AMBIENT, never the
|
|
506
|
-
// identity-scoped `env` (an agent must not act as the bot).
|
|
507
|
-
agentEnv,
|
|
508
|
-
note,
|
|
509
|
-
});
|
|
510
|
-
// THE TASK-SET ACCEPTANCE GATE BLOCKED (task `slice-acceptance-gate`): the
|
|
511
|
-
// fresh-context review of the produced SET returned `block`, so the core ran
|
|
512
|
-
// the review BEFORE the stage/integrate and did NOT integrate the tasks
|
|
513
|
-
// (correct). The CORRECT task-path destination is the SAME needs-attention
|
|
514
|
-
// route the lock release owns for the decomposition-unclear verdict: it amends
|
|
515
|
-
// the `spec:<slug>` unified lock `active -> stuck` with the block reason (the
|
|
516
|
-
// tasking needs-attention surface is the stuck lock now — NO folder write; the
|
|
517
|
-
// spec body stays in `work/specs/ready/`). So on a block we route the held spec to
|
|
518
|
-
// needs-attention THROUGH the lock release — the set never lands.
|
|
519
|
-
if (core.outcome === 'review-blocked') {
|
|
520
|
-
const reason = taskGateBlockedReason(slug, core.reviewBlockReason);
|
|
521
|
-
return await surfaceTaskingBlock({
|
|
522
|
-
slug,
|
|
505
|
+
// READ-STABILITY BACKSTOP (the lock's content-identity check, now owned at the
|
|
506
|
+
// integrate seam): the OUTPUT no longer rides the lock release, so the band
|
|
507
|
+
// below would otherwise rebase a concurrent edit of the held spec body CLEANLY
|
|
508
|
+
// into spec/ (a rename+edit merge) while the tasks were cut from the OLD body —
|
|
509
|
+
// the exact silent stale-task drift the lock forbids
|
|
510
|
+
// (`work/notes/observations/tasking-lock-does-not-stabilise-spec-content.md`). So we
|
|
511
|
+
// compare the CURRENTLY held `work/specs/ready/<slug>.md` blob on the arbiter against
|
|
512
|
+
// the snapshot the lock TOOK (`lockedBlob`); ANY change ⇒ STALE ⇒ fail loud,
|
|
513
|
+
// touch NOTHING (the lock stays held; a human re-tasks or routes to
|
|
514
|
+
// needs-attention). It is the SAME content-identity check `releaseTaskingLock`
|
|
515
|
+
// runs — relocated here because this transition, not the release, owns the
|
|
516
|
+
// completing commit now.
|
|
517
|
+
const stale = await heldSpecIsStale(cwd, arbiter, slug, lockedBlob, env);
|
|
518
|
+
if (stale) {
|
|
519
|
+
const specRel = workItemRel('specs-ready', `${slug}.md`);
|
|
520
|
+
const message = `RELEASE CONFLICT for '${slug}': the spec was edited (${specRel} ` +
|
|
521
|
+
`changed on ${arbiter}/main) while the tasking lock was held. The tasking is ` +
|
|
522
|
+
`STALE — re-task from the edited spec or route it to needs-attention. ` +
|
|
523
|
+
`The arbiter was NOT modified (lock still held).`;
|
|
524
|
+
note(message);
|
|
525
|
+
return { exitCode: 4, outcome: 'stale', slug, message };
|
|
526
|
+
}
|
|
527
|
+
// Route the OUTPUT through the SHARED integrate back-half (task
|
|
528
|
+
// `slice-output-through-integration`): the produced backlog tasks + the spec
|
|
529
|
+
// lifecycle move (`work/specs/ready/ -> work/specs/tasked/`, residence = tasked-ness) integrate
|
|
530
|
+
// via `performIntegration` honoring `--propose` (push the work branch + open a
|
|
531
|
+
// PR, NO `main` touch) / `--merge` (land on `main`). Because the integrate-time
|
|
532
|
+
// args resolve ONCE in the shared core, every `do task:` arg applies here by
|
|
533
|
+
// construction. The agent did NO git; the runner (the band) owns the ONE commit.
|
|
534
|
+
const core = await performIntegration({
|
|
535
|
+
cwd,
|
|
536
|
+
arbiter,
|
|
537
|
+
slug,
|
|
538
|
+
// `source`/`recovering` are task-shaped and IGNORED when `lifecycle` is set
|
|
539
|
+
// (a tasking transition never recovers a surfaced needs-attention move).
|
|
540
|
+
source: 'in-progress',
|
|
541
|
+
recovering: false,
|
|
542
|
+
// Skip the build acceptance gate (Gate 1 / verify): a tasking transition has
|
|
543
|
+
// no `verify` floor (the tasker review loop above is its quality gate).
|
|
544
|
+
skipVerify: true,
|
|
545
|
+
// THE TASK-SET ACCEPTANCE GATE (task `slice-acceptance-gate`): the
|
|
546
|
+
// task-path mirror of the build Gate-2, riding THIS shared core's
|
|
547
|
+
// review-before-integrate block. When `review` resolves on, the wired
|
|
548
|
+
// `reviewGate` (production: `harnessTaskReviewGate` with the task-SET
|
|
549
|
+
// prompt) runs a FRESH-CONTEXT review of the produced task SET before it
|
|
550
|
+
// integrates: `approve` lands it, `block` routes the set to needs-attention
|
|
551
|
+
// via the SAME machinery the build block uses (mapped to the tasking
|
|
552
|
+
// `needs-attention` outcome below). It is ONE-SHOT: we pin
|
|
553
|
+
// `reviewMaxRounds: 1` so the gate is a SINGLE reviewer invocation → verdict
|
|
554
|
+
// (terminal pass/fail). The task path NEVER exposes/consults
|
|
555
|
+
// `--review-max-rounds` — a gate is terminal, the rounds bound is an orphan
|
|
556
|
+
// that belongs to a future revise↔review loop (see
|
|
557
|
+
// `work/notes/observations/reviewmaxrounds-on-wrong-concept.md`). This is
|
|
558
|
+
// independently controllable from the tasker improver loop (`reviewLoop` /
|
|
559
|
+
// the `--tasker-loop*` family); toggling one does not affect the other.
|
|
560
|
+
// The CI apply phase passes it OFF (it ran in the agent phase) and hands
|
|
561
|
+
// its approved prose in as `approvedReviewProse`.
|
|
562
|
+
review: params.review,
|
|
563
|
+
reviewGate: params.reviewGate,
|
|
564
|
+
reviewModel: params.acceptanceReviewModel,
|
|
565
|
+
reviewMaxRounds: 1,
|
|
566
|
+
approvedReviewProse: params.approvedReviewProse,
|
|
567
|
+
// The cross-job merge-serialiser CAS-retry cap (config `mergeRetries`) —
|
|
568
|
+
// threaded so a wide-matrix CI's raised cap reaches the tasking-
|
|
569
|
+
// transition's land tail too (task
|
|
570
|
+
// `thread-merge-retries-cross-task-and-ratify-default`). Unset ⇒ falls
|
|
571
|
+
// through to the engine default (byte-for-byte unchanged).
|
|
572
|
+
mergeRetries: params.mergeRetries,
|
|
573
|
+
...(params.mergeJitterMs === undefined
|
|
574
|
+
? {}
|
|
575
|
+
: { mergeJitterMs: params.mergeJitterMs }),
|
|
576
|
+
// The EXPLICITLY-chosen integrate mode proceeds AS-IS on an APPROVE — a
|
|
577
|
+
// `--merge` tasking run lands on main, `--propose` opens a PR. The tasking
|
|
578
|
+
// path's merge-vs-propose decision is the `integration` mode the user typed;
|
|
579
|
+
// `merge` IS the auto-land mode, so a resolved `merge` is never downgraded.
|
|
580
|
+
// The task gate family is `--review`/`--no-review`/`--review-model` only
|
|
581
|
+
// (spec US #6).
|
|
582
|
+
mode: resolvedMode,
|
|
583
|
+
noPR: params.noPR,
|
|
584
|
+
providerInstance: params.providerInstance,
|
|
585
|
+
// PROPOSE-MODE PR BODY (task `slicing-pr-body-summary-threading`): mirror the
|
|
586
|
+
// BUILD path's body threading (`do.ts` — `body: agent.output`). The tasker
|
|
587
|
+
// agent produces no `LaunchResult.output` we can carry, but the slice-SET
|
|
588
|
+
// itself is a summary-worthy artifact: the emitted task slugs+titles, their
|
|
589
|
+
// coverage of the spec's user stories, the dependency graph (keystone +
|
|
590
|
+
// blockedBy edges), and any `needsAnswers` open questions the tasker review
|
|
591
|
+
// loop flagged. Compose it here and thread it into the shared core as `body`
|
|
592
|
+
// so the propose-mode PR carries the summary instead of degrading to
|
|
593
|
+
// `gh pr create --fill`. Empty-set (no emitted tasks) ⇒ undefined ⇒ the
|
|
594
|
+
// provider's `--fill` fallback (no regression on that edge).
|
|
595
|
+
body: composeTaskingProposeBody(slug, emitTasks),
|
|
596
|
+
type: 'tasking',
|
|
597
|
+
lifecycle: {
|
|
598
|
+
// Read the PR title / commit summary from the held spec (before it moves).
|
|
599
|
+
titlePath: workItemPath(cwd, 'specs-ready', slug),
|
|
600
|
+
commitTag: 'tasked',
|
|
601
|
+
// The spec RESTS in `work/specs/tasked/` once this lands (never
|
|
602
|
+
// `work/tasks/done/`, the task-build default), so point the reviewer there.
|
|
603
|
+
proposeHeader: proposePointer('Spec', workItemRel('specs-tasked', `${slug}.md`)),
|
|
604
|
+
stage: () => stageTaskingLifecycle({
|
|
523
605
|
cwd,
|
|
524
|
-
arbiter,
|
|
525
|
-
reason,
|
|
526
|
-
message: `The task acceptance gate disapproved the set produced for '${slug}'; ` +
|
|
527
|
-
`parked it for your attention (no tasks landed). ` +
|
|
528
|
-
`Resolve the findings, then re-task.`,
|
|
529
|
-
lockedBlob,
|
|
530
|
-
release: lock.release,
|
|
531
|
-
mode: resolvedMode,
|
|
532
|
-
provider: options.providerInstance,
|
|
533
|
-
env,
|
|
534
|
-
note,
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
if (core.outcome === 'sidecar-violation') {
|
|
538
|
-
// A co-located `<slug>/` asset sidecar sits beside the FLOWING spec item
|
|
539
|
-
// (WORK-CONTRACT.md rule 8): the tasking transition would `git mv`
|
|
540
|
-
// `specs/ready → specs/tasked` and STRAND the sidecar. The core HARD-BLOCKED
|
|
541
|
-
// before the stage/integrate; route the held spec to needs-attention through
|
|
542
|
-
// the SAME `spec:<slug>` lock-release seam the block path uses (no tasks land),
|
|
543
|
-
// carrying the actionable relocate-to-`docs/spikes/<slug>/` reason.
|
|
544
|
-
const reason = `The spec '${slug}' carries a co-located asset sidecar: ` +
|
|
545
|
-
`${core.reviewBlockReason ?? core.reason ?? ''}`;
|
|
546
|
-
return await surfaceTaskingBlock({
|
|
547
606
|
slug,
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
message: `The spec '${slug}' carries a co-located asset sidecar (WORK-CONTRACT ` +
|
|
552
|
-
`rule 8); parked it for your attention (no tasks landed; relocate it ` +
|
|
553
|
-
`to docs/spikes/${slug}/ and reference by path).`,
|
|
554
|
-
lockedBlob,
|
|
555
|
-
release: lock.release,
|
|
556
|
-
mode: resolvedMode,
|
|
557
|
-
provider: options.providerInstance,
|
|
558
|
-
env,
|
|
607
|
+
emitTasks,
|
|
608
|
+
poolBefore,
|
|
609
|
+
placementDir,
|
|
559
610
|
note,
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
if (core.outcome === 'review-unparseable') {
|
|
563
|
-
// The task-set acceptance gate RAN but its verdict was UNPARSEABLE (malformed
|
|
564
|
-
// JSON). Route the held spec to needs-attention through the SAME lock-release
|
|
565
|
-
// seam the block path uses (the tasking needs-attention surface is the stuck
|
|
566
|
-
// `spec:<slug>` lock; no folder write). It is NOT a block (the gate output was
|
|
567
|
-
// unreadable) — record it as the transient-infra-class re-run signal so the
|
|
568
|
-
// stuck reason reads correctly; nothing landed.
|
|
569
|
-
const reason = `The task acceptance gate for '${slug}' produced an UNPARSEABLE verdict ` +
|
|
570
|
-
`(re-run — transient): ${core.reason ?? ''}`;
|
|
571
|
-
return await surfaceTaskingBlock({
|
|
572
|
-
slug,
|
|
573
|
-
cwd,
|
|
574
|
-
arbiter,
|
|
575
|
-
reason,
|
|
576
|
-
message: `The task acceptance gate produced an unparseable verdict for '${slug}'; ` +
|
|
577
|
-
`parked it for your attention (no tasks landed; re-run).`,
|
|
578
|
-
lockedBlob,
|
|
579
|
-
release: lock.release,
|
|
580
|
-
mode: resolvedMode,
|
|
581
|
-
provider: options.providerInstance,
|
|
582
611
|
env,
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
// residence-in-`specs/tasked/` on `main` now carries tasked-ness and the lock
|
|
593
|
-
// has done its job — RELEASE it (delete the ref). Best-effort + idempotent
|
|
594
|
-
// (`not-held` is fine).
|
|
595
|
-
//
|
|
596
|
-
// `propose`: the move lives ONLY on the pushed work branch / open PR — `main`
|
|
597
|
-
// still holds the spec in `work/specs/ready/`, so residence does NOT yet signal
|
|
598
|
-
// tasked-ness. If we released here, the next in-place scan would see the spec
|
|
599
|
-
// eligible again (ready + not-tasked + lock free) and re-task it EVERY tick,
|
|
600
|
-
// force-recreating the branch (`git switch -C`) and force-pushing the SAME PR —
|
|
601
|
-
// regenerating its review forever until a human merges/closes it. So we KEEP
|
|
602
|
-
// the `spec:<slug>` lock HELD across the open PR: the held lock IS the
|
|
603
|
-
// "in-flight tasking" marker, and `scoreSpecs`/`taskableSpecs` subtract held
|
|
604
|
-
// specs from the taskable pool (symmetric to the task-pool held-slug
|
|
605
|
-
// subtraction). The lock is reaped when the PR resolves: a MERGE lands the
|
|
606
|
-
// durable move (which now carries tasked-ness on its own) and the reap/gc path
|
|
607
|
-
// deletes the merged branch + its lock; a human CLOSING the PR unmerged is a
|
|
608
|
-
// recovery the human owns via `release-lock` (the same model as any other
|
|
609
|
-
// abandoned in-flight item — no liveness heartbeat/auto-steal).
|
|
610
|
-
//
|
|
611
|
-
// MIGRATE step: the ref is keyed under the `spec:<slug>` identity (the
|
|
612
|
-
// `spec-<slug>` lock entry the acquire took).
|
|
613
|
-
if (useLock && resolvedMode === 'merge') {
|
|
614
|
-
await releaseItemLock({ item: `spec:${slug}`, cwd, arbiter, env });
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
return integrationToTaskResult(core, { slug, emitted, loop: loopTag });
|
|
618
|
-
}
|
|
619
|
-
// HUMAN, no-lock path: the human commits on `main` directly (the runner does
|
|
620
|
-
// not own the human's git). We report the produced tasks; moving the spec into
|
|
621
|
-
// `work/specs/tasked/` (residence = tasked-ness) and committing is the human's to
|
|
622
|
-
// do, as with the human `complete`.
|
|
623
|
-
const message = `Tasked '${slug}' -> ${emitted.length} backlog task` +
|
|
624
|
-
`${emitted.length === 1 ? '' : 's'} (human path, no lock). Inspect + commit ` +
|
|
625
|
-
`the produced files (and move the spec into work/specs/tasked/) yourself.`;
|
|
626
|
-
note(message);
|
|
627
|
-
return {
|
|
628
|
-
exitCode: 0,
|
|
629
|
-
outcome: 'tasked',
|
|
612
|
+
}),
|
|
613
|
+
},
|
|
614
|
+
env,
|
|
615
|
+
// The task-SET acceptance review AGENT launches AMBIENT, never the
|
|
616
|
+
// identity-scoped `env` (an agent must not act as the bot).
|
|
617
|
+
agentEnv,
|
|
618
|
+
note,
|
|
619
|
+
});
|
|
620
|
+
const surfaceBlock = (reason, message) => surfaceTaskingBlock({
|
|
630
621
|
slug,
|
|
631
|
-
|
|
632
|
-
|
|
622
|
+
cwd,
|
|
623
|
+
arbiter,
|
|
624
|
+
reason,
|
|
633
625
|
message,
|
|
634
|
-
|
|
626
|
+
lockedBlob,
|
|
627
|
+
release: params.release,
|
|
628
|
+
mode: resolvedMode,
|
|
629
|
+
provider: params.providerInstance,
|
|
630
|
+
env,
|
|
631
|
+
note,
|
|
632
|
+
});
|
|
633
|
+
// THE TASK-SET ACCEPTANCE GATE BLOCKED (task `slice-acceptance-gate`): the
|
|
634
|
+
// fresh-context review of the produced SET returned `block`, so the core ran
|
|
635
|
+
// the review BEFORE the stage/integrate and did NOT integrate the tasks
|
|
636
|
+
// (correct). The CORRECT task-path destination is the SAME needs-attention
|
|
637
|
+
// route the lock release owns for the decomposition-unclear verdict: it surfaces
|
|
638
|
+
// the spec and releases the `spec:<slug>` lock with the block reason. So on a
|
|
639
|
+
// block we route the held spec to needs-attention THROUGH the lock release —
|
|
640
|
+
// the set never lands.
|
|
641
|
+
if (core.outcome === 'review-blocked') {
|
|
642
|
+
return await surfaceBlock(taskGateBlockedReason(slug, core.reviewBlockReason), `The task acceptance gate disapproved the set produced for '${slug}'; ` +
|
|
643
|
+
`parked it for your attention (no tasks landed). ` +
|
|
644
|
+
`Resolve the findings, then re-task.`);
|
|
645
|
+
}
|
|
646
|
+
if (core.outcome === 'sidecar-violation') {
|
|
647
|
+
// A co-located `<slug>/` asset sidecar sits beside the FLOWING spec item
|
|
648
|
+
// (WORK-CONTRACT.md rule 8): the tasking transition would `git mv`
|
|
649
|
+
// `specs/ready → specs/tasked` and STRAND the sidecar. The core HARD-BLOCKED
|
|
650
|
+
// before the stage/integrate; route the held spec to needs-attention through
|
|
651
|
+
// the SAME `spec:<slug>` lock-release seam the block path uses (no tasks land),
|
|
652
|
+
// carrying the actionable relocate-to-`docs/spikes/<slug>/` reason.
|
|
653
|
+
return await surfaceBlock(`The spec '${slug}' carries a co-located asset sidecar: ` +
|
|
654
|
+
`${core.reviewBlockReason ?? core.reason ?? ''}`, `The spec '${slug}' carries a co-located asset sidecar (WORK-CONTRACT ` +
|
|
655
|
+
`rule 8); parked it for your attention (no tasks landed; relocate it ` +
|
|
656
|
+
`to docs/spikes/${slug}/ and reference by path).`);
|
|
657
|
+
}
|
|
658
|
+
if (core.outcome === 'review-unparseable') {
|
|
659
|
+
// The task-set acceptance gate RAN but its verdict was UNPARSEABLE (malformed
|
|
660
|
+
// JSON). Route the held spec to needs-attention through the SAME lock-release
|
|
661
|
+
// seam the block path uses. It is NOT a block (the gate output was
|
|
662
|
+
// unreadable) — record it as the transient-infra-class re-run signal so the
|
|
663
|
+
// stuck reason reads correctly; nothing landed.
|
|
664
|
+
return await surfaceBlock(taskGateUnparseableReason(slug, core.reason), `The task acceptance gate produced an unparseable verdict for '${slug}'; ` +
|
|
665
|
+
`parked it for your attention (no tasks landed; re-run).`);
|
|
666
|
+
}
|
|
667
|
+
if (core.outcome === 'completed') {
|
|
668
|
+
// LOCK LIFETIME IS MODE-DEPENDENT (fix
|
|
669
|
+
// `propose-tasking-releases-lock-so-spec-is-retasked-and-pr-force-pushed-every-tick`;
|
|
670
|
+
// the "hold-across-the-PR" capstone the older interim half deferred).
|
|
671
|
+
//
|
|
672
|
+
// `merge`: the durable `specs/ready → specs/tasked` `main` move HAS landed, so
|
|
673
|
+
// residence-in-`specs/tasked/` on `main` now carries tasked-ness and the lock
|
|
674
|
+
// has done its job — RELEASE it (delete the ref). Best-effort + idempotent
|
|
675
|
+
// (`not-held` is fine).
|
|
676
|
+
//
|
|
677
|
+
// `propose`: the move lives ONLY on the pushed work branch / open PR — `main`
|
|
678
|
+
// still holds the spec in `work/specs/ready/`, so residence does NOT yet signal
|
|
679
|
+
// tasked-ness. If we released here, the next in-place scan would see the spec
|
|
680
|
+
// eligible again (ready + not-tasked + lock free) and re-task it EVERY tick,
|
|
681
|
+
// force-recreating the branch (`git switch -C`) and force-pushing the SAME PR —
|
|
682
|
+
// regenerating its review forever until a human merges/closes it. So we KEEP
|
|
683
|
+
// the `spec:<slug>` lock HELD across the open PR: the held lock IS the
|
|
684
|
+
// "in-flight tasking" marker, and `scoreSpecs`/`taskableSpecs` subtract held
|
|
685
|
+
// specs from the taskable pool (symmetric to the task-pool held-slug
|
|
686
|
+
// subtraction). The lock is reaped when the PR resolves: a MERGE lands the
|
|
687
|
+
// durable move (which now carries tasked-ness on its own) and the reap/gc path
|
|
688
|
+
// deletes the merged branch + its lock; a human CLOSING the PR unmerged is a
|
|
689
|
+
// recovery the human owns via `release-lock` (the same model as any other
|
|
690
|
+
// abandoned in-flight item — no liveness heartbeat/auto-steal).
|
|
691
|
+
//
|
|
692
|
+
// MIGRATE step: the ref is keyed under the `spec:<slug>` identity (the
|
|
693
|
+
// `spec-<slug>` lock entry the acquire took).
|
|
694
|
+
if (resolvedMode === 'merge') {
|
|
695
|
+
await releaseItemLock({ item: `spec:${slug}`, cwd, arbiter, env });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return integrationToTaskResult(core, { slug, emitted, loop: loopTag });
|
|
635
699
|
}
|
|
636
700
|
/**
|
|
637
701
|
* Map a non-`released` lock-release result onto the {@link TaskResult} contract
|
|
@@ -670,9 +734,16 @@ function releaseFailureToResult(released, slug) {
|
|
|
670
734
|
function integrationToTaskResult(core, ctx) {
|
|
671
735
|
const { slug, emitted, loop } = ctx;
|
|
672
736
|
if (core.outcome === 'completed') {
|
|
737
|
+
// Say "opened a PR" ONLY when the provider opened one (task
|
|
738
|
+
// `intake-reports-the-pr-it-actually-opened`); a degraded `gh pr create`
|
|
739
|
+
// pushed the branch but opened nothing, and the shared core printed why.
|
|
740
|
+
const notOpened = proposeRequestNotOpened(core.integration);
|
|
673
741
|
const landed = core.integration?.mode === 'merge'
|
|
674
742
|
? 'landed them on the arbiter main'
|
|
675
|
-
:
|
|
743
|
+
: notOpened !== undefined
|
|
744
|
+
? `pushed them on branch ${core.integration?.pushedRef ?? core.branch} ` +
|
|
745
|
+
'but opened NO PR (main untouched; see the provider note above)'
|
|
746
|
+
: 'opened a PR carrying them (main untouched)';
|
|
676
747
|
const message = `Tasked '${slug}' -> ${emitted.length} backlog task` +
|
|
677
748
|
`${emitted.length === 1 ? '' : 's'}; the runner integrated the transition ` +
|
|
678
749
|
`through the shared core (moved work/specs/ready/ -> work/specs/tasked/, the ` +
|
|
@@ -706,7 +777,14 @@ function integrationToTaskResult(core, ctx) {
|
|
|
706
777
|
* pre-existing local `work/<slug>` (a re-run) is force-recreated off fresh main.
|
|
707
778
|
* The agent runs in-place on this branch (branch ≠ worktree).
|
|
708
779
|
*/
|
|
709
|
-
async function switchToWorkBranch(cwd, arbiter, slug, env
|
|
780
|
+
export async function switchToWorkBranch(cwd, arbiter, slug, env,
|
|
781
|
+
/**
|
|
782
|
+
* The commit to cut the branch from; default the freshly fetched
|
|
783
|
+
* `<arbiter>/main`. The CI phases pass the lock job's `baseSha` (task
|
|
784
|
+
* `ci-split-tasking`), so the agent and apply jobs work on the tree the lock
|
|
785
|
+
* job classified.
|
|
786
|
+
*/
|
|
787
|
+
base) {
|
|
710
788
|
// The tasking path is the parent-spec namespace (`do spec:<slug>`): the branch
|
|
711
789
|
// is `work/spec-<slug>`, distinct from a same-slug task-build's `work/task-<slug>`.
|
|
712
790
|
// MIGRATE step (spec `prd-to-spec-vocabulary-cutover-and-migration-command`):
|
|
@@ -714,7 +792,7 @@ async function switchToWorkBranch(cwd, arbiter, slug, env) {
|
|
|
714
792
|
// parses the legacy `work/prd-<slug>` form, so in-flight branches keep resolving).
|
|
715
793
|
const branch = workBranchRef('spec', slug);
|
|
716
794
|
await gitHard(['fetch', '--quiet', arbiter], cwd, env);
|
|
717
|
-
await gitHard(['switch', '--quiet', '-C', branch, `${arbiter}/main`], cwd, env);
|
|
795
|
+
await gitHard(['switch', '--quiet', '-C', branch, base ?? `${arbiter}/main`], cwd, env);
|
|
718
796
|
}
|
|
719
797
|
/**
|
|
720
798
|
* The READ-STABILITY content-identity STALE CHECK (the lock's backstop, owned at
|
|
@@ -876,12 +954,21 @@ async function gitHard(args, cwd, env) {
|
|
|
876
954
|
* generic line when absent. DISTINCT from the improver loop's
|
|
877
955
|
* {@link decompositionUnclearReason} (which carries the loop's open questions).
|
|
878
956
|
*/
|
|
879
|
-
function taskGateBlockedReason(slug, findingsReason) {
|
|
957
|
+
export function taskGateBlockedReason(slug, findingsReason) {
|
|
880
958
|
const head = `The task acceptance gate (fresh-context review of the produced SET) blocked ` +
|
|
881
959
|
`'${slug}'. The spec is parked for your attention with no tasks landed; ` +
|
|
882
960
|
`resolve the blocking findings, then re-task.`;
|
|
883
961
|
return findingsReason ? `${head}\n\n${findingsReason}` : head;
|
|
884
962
|
}
|
|
963
|
+
/**
|
|
964
|
+
* The needs-attention REASON for a task-SET ACCEPTANCE GATE whose verdict was
|
|
965
|
+
* UNPARSEABLE (the gate ran; its output could not be read): NOT a block, the
|
|
966
|
+
* transient-infra re-run signal. `detail` is the gate's parse-failure reason.
|
|
967
|
+
*/
|
|
968
|
+
export function taskGateUnparseableReason(slug, detail) {
|
|
969
|
+
return (`The task acceptance gate for '${slug}' produced an UNPARSEABLE verdict ` +
|
|
970
|
+
`(re-run — transient): ${detail ?? ''}`);
|
|
971
|
+
}
|
|
885
972
|
/**
|
|
886
973
|
* Shared terminal for a tasking transition that DID NOT land — a disapproving
|
|
887
974
|
* task-set review, a co-located sidecar violation, an unparseable verdict, or a
|
|
@@ -903,7 +990,7 @@ function taskGateBlockedReason(slug, findingsReason) {
|
|
|
903
990
|
* is GREEN — a park-for-human is not a failure). Only a surface that FAILED to
|
|
904
991
|
* publish maps to a non-zero {@link releaseFailureToResult}.
|
|
905
992
|
*/
|
|
906
|
-
async function surfaceTaskingBlock(params) {
|
|
993
|
+
export async function surfaceTaskingBlock(params) {
|
|
907
994
|
const { slug, cwd, arbiter, reason, message, lockedBlob, release, mode, provider, env, note, } = params;
|
|
908
995
|
// 1. Close a stale OPEN PR (propose only, only-if-exists) with the review as
|
|
909
996
|
// the closing comment; keep the branch for a later approving re-task to
|
|
@@ -940,7 +1027,7 @@ async function surfaceTaskingBlock(params) {
|
|
|
940
1027
|
* spec is parked for the human with these open questions + a question sidecar on
|
|
941
1028
|
* main, no guessed tasks). Prose only — recorded as the parked question's reason.
|
|
942
1029
|
*/
|
|
943
|
-
function decompositionUnclearReason(slug, questions) {
|
|
1030
|
+
export function decompositionUnclearReason(slug, questions) {
|
|
944
1031
|
const head = `The tasker review→edit loop could not converge on a sound decomposition of ` +
|
|
945
1032
|
`'${slug}' (--tasker-loop-max exhausted with unresolved blockers). The spec is parked ` +
|
|
946
1033
|
`for your attention with no guessed tasks; resolve:`;
|
|
@@ -960,7 +1047,7 @@ function decompositionUnclearReason(slug, questions) {
|
|
|
960
1047
|
* The candidate tasks the tasker produced are PERSISTED on the work branch (see
|
|
961
1048
|
* {@link persistTaskingCandidates}) so a human can recover them.
|
|
962
1049
|
*/
|
|
963
|
-
function reviewFailureReason(slug, err, candidatePaths) {
|
|
1050
|
+
export function reviewFailureReason(slug, err, candidatePaths) {
|
|
964
1051
|
const named = err instanceof ReviewOutputCappedError
|
|
965
1052
|
? `The tasker review leg failed on '${slug}': ${err.message}. This is a ` +
|
|
966
1053
|
"STRUCTURAL cap-truncation (the review edits payload shared the model's " +
|
|
@@ -979,7 +1066,7 @@ function reviewFailureReason(slug, err, candidatePaths) {
|
|
|
979
1066
|
return `${named}${tasks}`;
|
|
980
1067
|
}
|
|
981
1068
|
/** The human-readable terminal MESSAGE for a review-leg failure (the result's `message`). */
|
|
982
|
-
function reviewFailureMessage(slug, err) {
|
|
1069
|
+
export function reviewFailureMessage(slug, err) {
|
|
983
1070
|
if (err instanceof ReviewOutputCappedError) {
|
|
984
1071
|
return (`Tasking '${slug}' parked: the review leg hit the model output cap ` +
|
|
985
1072
|
`(${err.outputTokens} tokens) and was truncated before emitting its ` +
|
|
@@ -1000,7 +1087,7 @@ function reviewFailureMessage(slug, err) {
|
|
|
1000
1087
|
* candidate paths persisted (empty when there were none). Never throws — a git
|
|
1001
1088
|
* failure is noted and the bounce proceeds (the lock is still released).
|
|
1002
1089
|
*/
|
|
1003
|
-
async function persistTaskingCandidates(cwd, slug, before, arbiter, env, note) {
|
|
1090
|
+
export async function persistTaskingCandidates(cwd, slug, before, arbiter, env, note) {
|
|
1004
1091
|
const candidatePaths = newOrChangedStagedTasks(cwd, before);
|
|
1005
1092
|
if (candidatePaths.length === 0) {
|
|
1006
1093
|
return [];
|
|
@@ -1029,7 +1116,12 @@ async function persistTaskingCandidates(cwd, slug, before, arbiter, env, note) {
|
|
|
1029
1116
|
// teardown on an isolated run (the branch ref survives locally too, but a push
|
|
1030
1117
|
// makes them remote-recoverable). A push failure is noted, never fatal.
|
|
1031
1118
|
try {
|
|
1032
|
-
const pushed = await
|
|
1119
|
+
const pushed = await refWrite.pushTaskingCandidatesBranch({
|
|
1120
|
+
arbiter,
|
|
1121
|
+
branch,
|
|
1122
|
+
cwd,
|
|
1123
|
+
env,
|
|
1124
|
+
});
|
|
1033
1125
|
if (pushed.status !== 0) {
|
|
1034
1126
|
note(`Could not push the candidate-tasks work branch '${branch}' to ${arbiter} ` +
|
|
1035
1127
|
`(best-effort; the local commit still persists for recovery).`);
|
|
@@ -1085,7 +1177,7 @@ function appendQuestionsBlock(content, questions) {
|
|
|
1085
1177
|
* cross-spec `taskedAfter` ordering, resolved against `work/specs/tasked/` residence of
|
|
1086
1178
|
* the specs present in the checkout.
|
|
1087
1179
|
*/
|
|
1088
|
-
function resolveAgentGate(cwd, slug, specFm, autoTask, explicit) {
|
|
1180
|
+
export function resolveAgentGate(cwd, slug, specFm, autoTask, explicit) {
|
|
1089
1181
|
return resolveTaskingEligibility({
|
|
1090
1182
|
humanOnly: specFm.humanOnly,
|
|
1091
1183
|
needsAnswers: specFm.needsAnswers,
|
|
@@ -1096,7 +1188,7 @@ function resolveAgentGate(cwd, slug, specFm, autoTask, explicit) {
|
|
|
1096
1188
|
});
|
|
1097
1189
|
}
|
|
1098
1190
|
/** Build an HONEST gate-refusal message naming WHY the agent skipped the spec. */
|
|
1099
|
-
function gateRefusalReason(slug, specFm, eligibility, options) {
|
|
1191
|
+
export function gateRefusalReason(slug, specFm, eligibility, options) {
|
|
1100
1192
|
const reasons = [];
|
|
1101
1193
|
if (specFm.humanOnly === true) {
|
|
1102
1194
|
reasons.push('the spec is humanOnly (a human must drive its tasking)');
|
|
@@ -1234,7 +1326,7 @@ async function runTaskAgent(options, cwd, prompt, slug) {
|
|
|
1234
1326
|
return { ok: launched.ok, detail: launched.detail };
|
|
1235
1327
|
}
|
|
1236
1328
|
/** A snapshot of {@link STAGED_TASKS_DIR}: filename → file content. */
|
|
1237
|
-
function snapshotStagedTasks(cwd) {
|
|
1329
|
+
export function snapshotStagedTasks(cwd) {
|
|
1238
1330
|
const dir = join(cwd, STAGED_TASKS_DIR);
|
|
1239
1331
|
const snap = new Map();
|
|
1240
1332
|
for (const file of listMarkdown(dir)) {
|
|
@@ -1249,7 +1341,7 @@ function snapshotStagedTasks(cwd) {
|
|
|
1249
1341
|
* re-committed.) The agent's staging folder is `work/tasks/backlog/`; writes to
|
|
1250
1342
|
* the pool `work/tasks/ready/` are scrubbed at stage time, never picked up here.
|
|
1251
1343
|
*/
|
|
1252
|
-
function newOrChangedStagedTasks(cwd, before) {
|
|
1344
|
+
export function newOrChangedStagedTasks(cwd, before) {
|
|
1253
1345
|
const dir = join(cwd, STAGED_TASKS_DIR);
|
|
1254
1346
|
const changed = [];
|
|
1255
1347
|
for (const file of listMarkdown(dir)) {
|
|
@@ -1261,7 +1353,7 @@ function newOrChangedStagedTasks(cwd, before) {
|
|
|
1261
1353
|
return changed.sort();
|
|
1262
1354
|
}
|
|
1263
1355
|
/** Snapshot the POOL `work/tasks/ready/` (for the agent-write fence at stage time). */
|
|
1264
|
-
function snapshotPool(cwd) {
|
|
1356
|
+
export function snapshotPool(cwd) {
|
|
1265
1357
|
const dir = workFolderPath(cwd, 'tasks-ready');
|
|
1266
1358
|
const snap = new Map();
|
|
1267
1359
|
for (const file of listMarkdown(dir)) {
|
|
@@ -1270,7 +1362,7 @@ function snapshotPool(cwd) {
|
|
|
1270
1362
|
return snap;
|
|
1271
1363
|
}
|
|
1272
1364
|
/** Read the produced backlog tasks' content keyed by repo-relative path. */
|
|
1273
|
-
function collectEmittedTasks(cwd, relPaths) {
|
|
1365
|
+
export function collectEmittedTasks(cwd, relPaths) {
|
|
1274
1366
|
const out = {};
|
|
1275
1367
|
for (const rel of relPaths) {
|
|
1276
1368
|
out[rel] = readFileSync(join(cwd, rel), 'utf8');
|
|
@@ -1410,10 +1502,12 @@ function readTitleField(content) {
|
|
|
1410
1502
|
let value = m[1].trim();
|
|
1411
1503
|
if (value === '')
|
|
1412
1504
|
return undefined;
|
|
1413
|
-
// Quoted ⇒ return inner text
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1505
|
+
// Quoted ⇒ return the inner text (a single-quoted `''` decodes to `'`, the
|
|
1506
|
+
// shape `quoteYamlScalar` writes); unquoted ⇒ strip trailing `# ...`.
|
|
1507
|
+
if (value.length >= 2 &&
|
|
1508
|
+
((value.startsWith('"') && value.endsWith('"')) ||
|
|
1509
|
+
(value.startsWith("'") && value.endsWith("'")))) {
|
|
1510
|
+
return unquoteYamlScalar(value);
|
|
1417
1511
|
}
|
|
1418
1512
|
const hash = value.indexOf('#');
|
|
1419
1513
|
if (hash !== -1)
|