dorfl 0.14.3 → 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 +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 +148 -37
- package/dist/install-ci-core.d.ts.map +1 -1
- package/dist/install-ci-core.js +278 -190
- 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 +352 -223
- 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
|
@@ -0,0 +1,1328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The build path split into the three CI phases** (spec
|
|
3
|
+
* `ci-agent-job-without-write-token` §3 build row and §8, ADR
|
|
4
|
+
* `ci-agent-job-holds-no-write-token`, tasks `ci-split-build-path` and
|
|
5
|
+
* `ci-split-build-path-non-integrate-intents`).
|
|
6
|
+
*
|
|
7
|
+
* `advance task:<slug>` / `do <slug>` with `--phase` run ONE of three halves of
|
|
8
|
+
* the one build pipeline (`performDo`), never a second implementation:
|
|
9
|
+
*
|
|
10
|
+
* - **lock** ({@link performBuildLockPhase}): fetch `<arbiter>/main` (its tip
|
|
11
|
+
* is `baseSha`, and the tree the item is classified in, never the run's
|
|
12
|
+
* commit); claim (`acquireItemLock`, `action: implement`); publish
|
|
13
|
+
* `acquired`, `needsAgent`, `rung`, `baseSha`, `lockSha`, `continueTip`,
|
|
14
|
+
* `handoffName` (it carries `github.run_attempt`, so a "Re-run all jobs"
|
|
15
|
+
* attempt never collides with an earlier attempt's artifact) and
|
|
16
|
+
* `agentTimeoutMinutes` (from the config at `baseSha`). An item that is not
|
|
17
|
+
* claimable at `baseSha` (it already advanced, or its lock is held) is a
|
|
18
|
+
* no-op that writes nothing.
|
|
19
|
+
* - **agent** ({@link performBuildAgentPhase}): check, read-only, that the lock
|
|
20
|
+
* ref still equals `lockSha`, then run `performDo` without its claim under
|
|
21
|
+
* the phase recorder: the continue rebase (LOCAL only, decision 7), the
|
|
22
|
+
* build agent, the STOP and deadline detection, the gate, the Gate-2 review,
|
|
23
|
+
* the done-move commit, the local rebase and the fresh-worktree gate run
|
|
24
|
+
* exactly as today, and the pipeline halts at its first write, which names
|
|
25
|
+
* the intent handed over ({@link handOverHalt}):
|
|
26
|
+
* - the land half (`integrationLand.land`): `integrate`, with the
|
|
27
|
+
* work-branch bundle, the PR title and body and the Gate-2 review prose;
|
|
28
|
+
* - a needs-attention bounce (red gate, blocked review, rebase conflict):
|
|
29
|
+
* `needs-attention`, the wip committed locally and bundled;
|
|
30
|
+
* - the deadline checkpoint's branch save: `deadline-checkpoint`, the
|
|
31
|
+
* checkpoint commit bundled;
|
|
32
|
+
* - the agent's deliberate STOP: `stop`; an agent failure: `agent-failed`
|
|
33
|
+
* (both with the wip bundled, when there is any).
|
|
34
|
+
* - **apply** ({@link performBuildApplyPhase}): refuse to write unless the
|
|
35
|
+
* lock ref still equals `lockSha` (so a "Re-run failed jobs" of a finished
|
|
36
|
+
* run writes nothing); act on the agent job's result first (task
|
|
37
|
+
* `ci-split-agent-result-and-reruns`): anything but `success` never reads
|
|
38
|
+
* the handoff, and surfaces the item (failure, timeout, an unexpected skip)
|
|
39
|
+
* or only releases the lock (a real cancel); validate the handoff as hostile
|
|
40
|
+
* (`validateApplyHandoff`); push the handoff's Git LFS objects before any
|
|
41
|
+
* ref, for every intent (decision 6, `pushLfsObjects`); when the lock job saw a kept branch, push the
|
|
42
|
+
* bundle's tip with a lease on that `continueTip` first (decision 7; a
|
|
43
|
+
* stale lease writes nothing); then resume at the write half of the intent:
|
|
44
|
+
* the land half (`landIntegration`, the unchanged compare-and-swap loop, then
|
|
45
|
+
* the review comment), the needs-attention route, `routeDeadlineCheckpoint`
|
|
46
|
+
* (with `maxAutoCheckpoints` from the config at `baseSha`),
|
|
47
|
+
* `saveAgentStop` or `saveAgentFailure`. Every lock release in the apply
|
|
48
|
+
* phase is leased on `lockSha` (`leaseLockReleases`), and the lock is
|
|
49
|
+
* released once the work is durably on `main` (a propose PR keeps it held,
|
|
50
|
+
* exactly as `complete` does today).
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import {existsSync} from 'node:fs';
|
|
54
|
+
import {join} from 'node:path';
|
|
55
|
+
import {classifyTick} from './advance-classify.js';
|
|
56
|
+
import {readItemSignals} from './advance.js';
|
|
57
|
+
import {
|
|
58
|
+
HANDOFF_LIMITS,
|
|
59
|
+
HandoffRejected,
|
|
60
|
+
handoffName as deriveHandoffName,
|
|
61
|
+
type AgentFailedProducts,
|
|
62
|
+
type HandoffRecord,
|
|
63
|
+
type NeedsAttentionProducts,
|
|
64
|
+
type StopProducts,
|
|
65
|
+
} from './ci-handoff-format.js';
|
|
66
|
+
import {writeHandoff} from './ci-handoff.js';
|
|
67
|
+
import {validateApplyHandoff} from './ci-handoff-apply.js';
|
|
68
|
+
import type {LockOutputs} from './ci-lock-outputs.js';
|
|
69
|
+
import type {AgentJobResult, GithubApiGet} from './ci-agent-result.js';
|
|
70
|
+
import {
|
|
71
|
+
PhaseDriverError,
|
|
72
|
+
agentTimeoutMinutesAt,
|
|
73
|
+
arbiterLockSha,
|
|
74
|
+
arbiterRefSha,
|
|
75
|
+
boundHandoffText,
|
|
76
|
+
checkLockOwnership,
|
|
77
|
+
emitLockOutputs,
|
|
78
|
+
fetchArbiterMain,
|
|
79
|
+
noSmudgeEnv,
|
|
80
|
+
pushHandoffLfs,
|
|
81
|
+
releaseLockLeased,
|
|
82
|
+
repoConfigAt,
|
|
83
|
+
requireHeldLock,
|
|
84
|
+
resolveAgentResult,
|
|
85
|
+
runnerTempFrom,
|
|
86
|
+
taskSourceAtBase,
|
|
87
|
+
withBaseWorktree,
|
|
88
|
+
type HeldLock,
|
|
89
|
+
} from './ci-phase-driver.js';
|
|
90
|
+
import {performClaim} from './claim-cas.js';
|
|
91
|
+
import {
|
|
92
|
+
deadlineSurfaceReason,
|
|
93
|
+
performDo,
|
|
94
|
+
routeDeadlineCheckpoint,
|
|
95
|
+
saveAgentFailure,
|
|
96
|
+
saveAgentStop,
|
|
97
|
+
type BuildBoundary,
|
|
98
|
+
type DoOptions,
|
|
99
|
+
type DoResult,
|
|
100
|
+
} from './do.js';
|
|
101
|
+
import {
|
|
102
|
+
checkGatePreconditions,
|
|
103
|
+
detectLockfileOnDisk,
|
|
104
|
+
} from './gate-readiness.js';
|
|
105
|
+
import {run, runAsync} from './git.js';
|
|
106
|
+
import {selectProvider} from './github.js';
|
|
107
|
+
import {identityEnv} from './identity.js';
|
|
108
|
+
import {leaseLockReleases, markLockKeptForProposePr} from './item-lock.js';
|
|
109
|
+
import {
|
|
110
|
+
arbiterUrl,
|
|
111
|
+
integrationLand,
|
|
112
|
+
type IntegrationCoreResult,
|
|
113
|
+
type IntegrationLandInput,
|
|
114
|
+
} from './integration-core.js';
|
|
115
|
+
import {proposeRequestNotOpened} from './integrator.js';
|
|
116
|
+
import {ledgerRead} from './ledger-read.js';
|
|
117
|
+
import {ledgerWrite} from './ledger-write.js';
|
|
118
|
+
import {
|
|
119
|
+
commitAbortedWork,
|
|
120
|
+
type RouteToNeedsAttentionOptions,
|
|
121
|
+
} from './needs-attention.js';
|
|
122
|
+
import {refWrite} from './ref-write.js';
|
|
123
|
+
import type {Phase} from './phase.js';
|
|
124
|
+
import {
|
|
125
|
+
createPhaseRecorder,
|
|
126
|
+
recordingReviewProvider,
|
|
127
|
+
runAgentPhase,
|
|
128
|
+
type RecordedWriteIntent,
|
|
129
|
+
type WriteSeamCall,
|
|
130
|
+
} from './phase-recorder.js';
|
|
131
|
+
import {
|
|
132
|
+
SlugResolutionError,
|
|
133
|
+
resolveAdvanceArg,
|
|
134
|
+
resolveSlug,
|
|
135
|
+
workBranchRef,
|
|
136
|
+
} from './slug-namespace.js';
|
|
137
|
+
import {workItemRel} from './work-layout.js';
|
|
138
|
+
|
|
139
|
+
const DEFAULT_ARBITER = 'origin';
|
|
140
|
+
|
|
141
|
+
/** The verb a build phase runs under (they classify the argument differently). */
|
|
142
|
+
export type BuildPhaseVerb = 'do' | 'advance';
|
|
143
|
+
|
|
144
|
+
/** Options of a build phase run: the `do` pipeline's options plus the phase inputs. */
|
|
145
|
+
export interface BuildPhaseOptions extends DoOptions {
|
|
146
|
+
/** Which of the three jobs this process is. */
|
|
147
|
+
phase: Phase;
|
|
148
|
+
/** `do <slug>` or `advance task:<slug>`. */
|
|
149
|
+
verb: BuildPhaseVerb;
|
|
150
|
+
/**
|
|
151
|
+
* The handoff directory: the agent phase writes the artifact here (it must be
|
|
152
|
+
* empty or absent); the apply phase reads it (it must be under
|
|
153
|
+
* {@link runnerTemp}).
|
|
154
|
+
*/
|
|
155
|
+
handoffDir?: string;
|
|
156
|
+
/** `$RUNNER_TEMP` (the apply phase); defaults to the env's. */
|
|
157
|
+
runnerTemp?: string;
|
|
158
|
+
/** The lock job's outputs (agent / apply); defaults to `DORFL_LOCK_OUTPUTS`. */
|
|
159
|
+
lockOutputs?: LockOutputs;
|
|
160
|
+
/** `github.run_attempt` (the lock phase names the artifact with it); default the env's, else 1. */
|
|
161
|
+
runAttempt?: string;
|
|
162
|
+
/** The `$GITHUB_OUTPUT` file the lock phase appends to; default the env's. */
|
|
163
|
+
githubOutput?: string;
|
|
164
|
+
/** The merge-mode CAS-loop jitter (tests pass 0). */
|
|
165
|
+
mergeJitterMs?: number;
|
|
166
|
+
/**
|
|
167
|
+
* apply: `needs.agent.result` (`--agent-result`), REQUIRED. Only `success`
|
|
168
|
+
* reads the handoff (decision 5, task `ci-split-agent-result-and-reruns`).
|
|
169
|
+
*/
|
|
170
|
+
agentResult?: AgentJobResult;
|
|
171
|
+
/**
|
|
172
|
+
* apply: the agent job's timeout (`--agent-timeout-minutes`, the trusted
|
|
173
|
+
* lock output); defaults to the lock outputs' `agentTimeoutMinutes`.
|
|
174
|
+
*/
|
|
175
|
+
agentTimeoutMinutes?: number;
|
|
176
|
+
/** apply: the Actions API reader (tests stub it); default `fetch` with `GITHUB_TOKEN`. */
|
|
177
|
+
actionsApi?: GithubApiGet;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** How a build phase run ended. */
|
|
181
|
+
export type BuildPhaseOutcome =
|
|
182
|
+
/** lock: the item is claimed; the agent phase runs next. */
|
|
183
|
+
| 'locked'
|
|
184
|
+
/** lock: nothing to do at the arbiter's `main` (not claimable, or not a build). */
|
|
185
|
+
| 'no-op'
|
|
186
|
+
/** lock: another run holds the item's lock. */
|
|
187
|
+
| 'lost'
|
|
188
|
+
/** agent / apply: the lock ref no longer equals `lockSha`; nothing was done. */
|
|
189
|
+
| 'stale-lock'
|
|
190
|
+
/** agent: the handoff was written (see `intent`). */
|
|
191
|
+
| 'handed-over'
|
|
192
|
+
/** apply: the work landed on `main` and the lock was released. */
|
|
193
|
+
| 'landed'
|
|
194
|
+
/** apply: the branch was pushed (and a PR requested); the lock stays held. */
|
|
195
|
+
| 'proposed'
|
|
196
|
+
/** apply: the item was surfaced to needs-attention. */
|
|
197
|
+
| 'surfaced'
|
|
198
|
+
/** apply: surfacing the item did not land on the arbiter (it stays locked). */
|
|
199
|
+
| 'surface-unmoved'
|
|
200
|
+
/** apply: a deadline checkpoint was saved and the lock released to continue. */
|
|
201
|
+
| 'auto-continued'
|
|
202
|
+
/** apply: the agent job was cancelled (not timed out); the lock was only released. */
|
|
203
|
+
| 'released'
|
|
204
|
+
/** apply: the cancelled agent job's leased lock release was refused. */
|
|
205
|
+
| 'release-refused'
|
|
206
|
+
/**
|
|
207
|
+
* apply: the kept work branch moved on the arbiter since the lock job saw it
|
|
208
|
+
* (`continueTip`), so the leased continue push was refused; nothing was written.
|
|
209
|
+
*/
|
|
210
|
+
| 'stale-lease'
|
|
211
|
+
/** apply: the handoff broke a rule; nothing from it was written. */
|
|
212
|
+
| 'rejected'
|
|
213
|
+
/** A usage or environment problem, or a path this task does not split yet. */
|
|
214
|
+
| 'usage-error';
|
|
215
|
+
|
|
216
|
+
/** The result of one build phase run. */
|
|
217
|
+
export interface BuildPhaseResult {
|
|
218
|
+
/**
|
|
219
|
+
* The job's exit code: a HANDLED outcome is 0 (a surface that landed, a
|
|
220
|
+
* rejected handoff that was surfaced, a lock lost to another run, a stale
|
|
221
|
+
* lock on a re-run); only an unhandled one is non-zero. See "A handled
|
|
222
|
+
* outcome is green" in `ci-phase-driver.ts`.
|
|
223
|
+
*/
|
|
224
|
+
exitCode: 0 | 1 | 2 | 3;
|
|
225
|
+
outcome: BuildPhaseOutcome;
|
|
226
|
+
/**
|
|
227
|
+
* The result line: the CLI prints it (`>> ` or `error: `), so the phase does
|
|
228
|
+
* NOT also `note` it (each line appears once in the job log).
|
|
229
|
+
*/
|
|
230
|
+
message: string;
|
|
231
|
+
slug?: string;
|
|
232
|
+
/** lock: the facts published. */
|
|
233
|
+
lockOutputs?: LockOutputs;
|
|
234
|
+
/** agent: the handed-over intent kind. */
|
|
235
|
+
intent?: HandoffRecord['intent']['kind'];
|
|
236
|
+
/** apply: the land half's result, when it ran. */
|
|
237
|
+
land?: IntegrationCoreResult;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Run one phase of the build path. The CLI calls this for `do <slug> --phase`
|
|
242
|
+
* and `advance task:<slug> --phase`.
|
|
243
|
+
*/
|
|
244
|
+
export async function performBuildPhase(
|
|
245
|
+
options: BuildPhaseOptions,
|
|
246
|
+
): Promise<BuildPhaseResult> {
|
|
247
|
+
try {
|
|
248
|
+
switch (options.phase) {
|
|
249
|
+
case 'lock':
|
|
250
|
+
return await performBuildLockPhase(options);
|
|
251
|
+
case 'agent':
|
|
252
|
+
return await performBuildAgentPhase(options);
|
|
253
|
+
case 'apply':
|
|
254
|
+
return await performBuildApplyPhase(options);
|
|
255
|
+
}
|
|
256
|
+
} catch (err) {
|
|
257
|
+
if (err instanceof PhaseDriverError || err instanceof SlugResolutionError) {
|
|
258
|
+
return {exitCode: 1, outcome: 'usage-error', message: err.message};
|
|
259
|
+
}
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// lock
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
/** The task slug a build phase acts on, from the (trusted) workflow argument. */
|
|
269
|
+
function buildSlug(options: BuildPhaseOptions, repoPath: string): string {
|
|
270
|
+
if (options.verb === 'advance') {
|
|
271
|
+
const resolved = resolveAdvanceArg({
|
|
272
|
+
arg: options.arg,
|
|
273
|
+
repoPath,
|
|
274
|
+
read: options.read ?? ledgerRead,
|
|
275
|
+
});
|
|
276
|
+
if (resolved.namespace !== 'task') {
|
|
277
|
+
throw new PhaseDriverError(
|
|
278
|
+
`--phase splits only the build path so far; ${options.arg} is not a task`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return resolved.slug;
|
|
282
|
+
}
|
|
283
|
+
const resolved = resolveSlug({
|
|
284
|
+
arg: options.arg,
|
|
285
|
+
repoPath,
|
|
286
|
+
read: options.read ?? ledgerRead,
|
|
287
|
+
});
|
|
288
|
+
if (resolved.namespace !== 'task') {
|
|
289
|
+
throw new PhaseDriverError(
|
|
290
|
+
`--phase splits only the build path so far; ${options.arg} is not a task`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
return resolved.slug;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The lock phase: classify the item at the arbiter's `main`, claim it and
|
|
298
|
+
* publish the trusted facts. Runs no agent and no repository code. An item that
|
|
299
|
+
* already advanced (not claimable at `baseSha`), or whose lock another run
|
|
300
|
+
* holds, is refused BEFORE any write.
|
|
301
|
+
*/
|
|
302
|
+
export async function performBuildLockPhase(
|
|
303
|
+
options: BuildPhaseOptions,
|
|
304
|
+
): Promise<BuildPhaseResult> {
|
|
305
|
+
const note = options.note ?? (() => {});
|
|
306
|
+
const arbiter = options.arbiter ?? DEFAULT_ARBITER;
|
|
307
|
+
const cwd = options.cwd;
|
|
308
|
+
const env = identityEnv(options.identity, options.env ?? process.env);
|
|
309
|
+
const githubOutput = options.githubOutput ?? env.GITHUB_OUTPUT;
|
|
310
|
+
const publish = (facts: LockOutputs): LockOutputs => {
|
|
311
|
+
emitLockOutputs(facts, {githubOutput, note});
|
|
312
|
+
return facts;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const baseSha = await fetchArbiterMain({cwd, arbiter, env});
|
|
316
|
+
const verdict = await withBaseWorktree(
|
|
317
|
+
{cwd, baseSha, env},
|
|
318
|
+
async (base): Promise<{slug: string; skip?: string}> => {
|
|
319
|
+
const slug = buildSlug(options, base);
|
|
320
|
+
if (options.verb === 'advance') {
|
|
321
|
+
const item = `task:${slug}`;
|
|
322
|
+
const signals = readItemSignals({
|
|
323
|
+
repoPath: base,
|
|
324
|
+
type: 'task',
|
|
325
|
+
slug,
|
|
326
|
+
item,
|
|
327
|
+
});
|
|
328
|
+
const kind = classifyTick({type: 'task', ...signals}).kind;
|
|
329
|
+
if (kind !== 'build-task') {
|
|
330
|
+
if (kind === 'no-op') {
|
|
331
|
+
return {slug, skip: `no-op for ${item} at ${baseSha}`};
|
|
332
|
+
}
|
|
333
|
+
throw new PhaseDriverError(
|
|
334
|
+
`--phase splits only the build path so far; ${item} classifies ` +
|
|
335
|
+
`as ${kind} at ${baseSha}`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const folders =
|
|
340
|
+
options.allowBacklog === true
|
|
341
|
+
? (['tasks-ready', 'tasks-backlog'] as const)
|
|
342
|
+
: (['tasks-ready'] as const);
|
|
343
|
+
const claimable = folders.some((f) =>
|
|
344
|
+
existsSync(join(base, workItemRel(f, `${slug}.md`))),
|
|
345
|
+
);
|
|
346
|
+
if (!claimable) {
|
|
347
|
+
return {
|
|
348
|
+
slug,
|
|
349
|
+
skip:
|
|
350
|
+
`'${slug}' is not claimable at ${arbiter}/main (${baseSha}): it ` +
|
|
351
|
+
'already advanced, or was removed',
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const guard = checkGatePreconditions({
|
|
355
|
+
freshWorktreeGate: options.freshWorktreeGate,
|
|
356
|
+
prepare: options.prepare,
|
|
357
|
+
verify: options.verify,
|
|
358
|
+
lockfile: detectLockfileOnDisk(base),
|
|
359
|
+
});
|
|
360
|
+
if (guard !== undefined) throw new PhaseDriverError(guard.message);
|
|
361
|
+
return {slug};
|
|
362
|
+
},
|
|
363
|
+
);
|
|
364
|
+
const slug = verdict.slug;
|
|
365
|
+
const item = `task:${slug}`;
|
|
366
|
+
if (verdict.skip !== undefined) {
|
|
367
|
+
return {
|
|
368
|
+
exitCode: 0,
|
|
369
|
+
outcome: 'no-op',
|
|
370
|
+
slug,
|
|
371
|
+
message: verdict.skip,
|
|
372
|
+
lockOutputs: publish({acquired: false, baseSha}),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if ((await arbiterLockSha({cwd, arbiter, item, env})) !== undefined) {
|
|
376
|
+
const message = `'${slug}' is already locked on ${arbiter}; backing off.`;
|
|
377
|
+
return {
|
|
378
|
+
exitCode: 0,
|
|
379
|
+
outcome: 'lost',
|
|
380
|
+
slug,
|
|
381
|
+
message,
|
|
382
|
+
lockOutputs: publish({acquired: false, rung: 'build-task', baseSha}),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const claim = await performClaim({
|
|
387
|
+
slug,
|
|
388
|
+
cwd,
|
|
389
|
+
arbiter,
|
|
390
|
+
allowBacklog: options.allowBacklog === true,
|
|
391
|
+
env,
|
|
392
|
+
note,
|
|
393
|
+
});
|
|
394
|
+
if (claim.exitCode !== 0) {
|
|
395
|
+
const outcome = claim.outcome === 'lost' ? 'lost' : 'usage-error';
|
|
396
|
+
return {
|
|
397
|
+
// A claim lost to another run is handled, so green ("A handled
|
|
398
|
+
// outcome is green", `ci-phase-driver.ts`).
|
|
399
|
+
exitCode: outcome === 'lost' ? 0 : claim.exitCode,
|
|
400
|
+
outcome,
|
|
401
|
+
slug,
|
|
402
|
+
message: claim.message,
|
|
403
|
+
lockOutputs: publish({acquired: false, rung: 'build-task', baseSha}),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const lockSha = await arbiterLockSha({cwd, arbiter, item, env});
|
|
407
|
+
if (lockSha === undefined) {
|
|
408
|
+
throw new Error(`the lock of ${item} vanished right after the claim`);
|
|
409
|
+
}
|
|
410
|
+
const continueTip = await arbiterRefSha({
|
|
411
|
+
cwd,
|
|
412
|
+
arbiter,
|
|
413
|
+
ref: `refs/heads/${workBranchRef('task', slug)}`,
|
|
414
|
+
env,
|
|
415
|
+
});
|
|
416
|
+
const facts = publish({
|
|
417
|
+
acquired: true,
|
|
418
|
+
needsAgent: true,
|
|
419
|
+
rung: 'build-task',
|
|
420
|
+
baseSha,
|
|
421
|
+
lockSha,
|
|
422
|
+
continueTip,
|
|
423
|
+
handoffName: deriveHandoffName(
|
|
424
|
+
item,
|
|
425
|
+
options.runAttempt ?? env.GITHUB_RUN_ATTEMPT ?? '1',
|
|
426
|
+
),
|
|
427
|
+
agentTimeoutMinutes: agentTimeoutMinutesAt(cwd, baseSha, env),
|
|
428
|
+
});
|
|
429
|
+
return {
|
|
430
|
+
exitCode: 0,
|
|
431
|
+
outcome: 'locked',
|
|
432
|
+
slug,
|
|
433
|
+
message: `locked ${item} at ${baseSha} (lock ${lockSha})`,
|
|
434
|
+
lockOutputs: facts,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
// agent
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
/** Every write seam call the build pipeline can reach (all are recorded). */
|
|
443
|
+
function buildRecordableCalls(): WriteSeamCall[] {
|
|
444
|
+
return [
|
|
445
|
+
'integrationLand.land',
|
|
446
|
+
...Object.keys(ledgerWrite).map((m) => `ledgerWrite.${m}` as const),
|
|
447
|
+
...Object.keys(refWrite).map((m) => `refWrite.${m}` as const),
|
|
448
|
+
'reviewProvider.openRequest',
|
|
449
|
+
'reviewProvider.postPRComment',
|
|
450
|
+
'reviewProvider.postPRCommentOnBranch',
|
|
451
|
+
'reviewProvider.closeRequestOnBranch',
|
|
452
|
+
];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** The `agent-failed` detail for a halt at a write the build path does not carry. */
|
|
456
|
+
function haltDetail(intent: RecordedWriteIntent): string {
|
|
457
|
+
const input = intent.input as {reason?: unknown} | undefined;
|
|
458
|
+
const reason =
|
|
459
|
+
input !== null &&
|
|
460
|
+
typeof input === 'object' &&
|
|
461
|
+
typeof input.reason === 'string'
|
|
462
|
+
? `: ${input.reason}`
|
|
463
|
+
: '';
|
|
464
|
+
return (
|
|
465
|
+
`the build stopped at ${intent.seam}.${intent.method}${reason} ` +
|
|
466
|
+
'(this outcome is not carried through the CI phases)'
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** What the agent phase writes: the record, and the work branch to bundle. */
|
|
471
|
+
interface Handover {
|
|
472
|
+
record: HandoffRecord;
|
|
473
|
+
bundle?: {repo: string; workBranch: string; baseSha: string};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** The context {@link handOverHalt} maps a halt in. */
|
|
477
|
+
interface HaltContext {
|
|
478
|
+
item: string;
|
|
479
|
+
slug: string;
|
|
480
|
+
arbiter: string;
|
|
481
|
+
baseSha: string;
|
|
482
|
+
env: NodeJS.ProcessEnv;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* The work branch to bundle as the WIP of a non-integrate intent, or
|
|
487
|
+
* `undefined` when it carries nothing to recover: the checkout is not on the
|
|
488
|
+
* item's work branch, or the branch has no commit that `<arbiter>/main` lacks
|
|
489
|
+
* (the apply phase refuses a bundle without a new commit).
|
|
490
|
+
*/
|
|
491
|
+
function wipBundle(
|
|
492
|
+
cwd: string,
|
|
493
|
+
ctx: HaltContext,
|
|
494
|
+
): Handover['bundle'] | undefined {
|
|
495
|
+
const workBranch = workBranchRef('task', ctx.slug);
|
|
496
|
+
const head = run('git', ['symbolic-ref', '--quiet', 'HEAD'], cwd, {
|
|
497
|
+
env: ctx.env,
|
|
498
|
+
});
|
|
499
|
+
if (head.status !== 0 || head.stdout.trim() !== `refs/heads/${workBranch}`) {
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
const ahead = run(
|
|
503
|
+
'git',
|
|
504
|
+
[
|
|
505
|
+
'rev-list',
|
|
506
|
+
'--count',
|
|
507
|
+
`refs/heads/${workBranch}`,
|
|
508
|
+
`^refs/remotes/${ctx.arbiter}/main`,
|
|
509
|
+
`^${ctx.baseSha}`,
|
|
510
|
+
],
|
|
511
|
+
cwd,
|
|
512
|
+
{env: ctx.env},
|
|
513
|
+
);
|
|
514
|
+
if (ahead.status !== 0 || Number(ahead.stdout.trim()) === 0) {
|
|
515
|
+
return undefined;
|
|
516
|
+
}
|
|
517
|
+
return {repo: cwd, workBranch, baseSha: ctx.baseSha};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** A bounded reason (or question) for the handoff record. */
|
|
521
|
+
function reasonText(text: string): string {
|
|
522
|
+
return boundHandoffText(text, HANDOFF_LIMITS.reasonChars);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** The `needs-attention` handover of a bounce's recorded input. */
|
|
526
|
+
function needsAttention(
|
|
527
|
+
ctx: HaltContext,
|
|
528
|
+
input: {reason: string; questions?: string[]},
|
|
529
|
+
bundle: Handover['bundle'] | undefined,
|
|
530
|
+
): Handover {
|
|
531
|
+
return {
|
|
532
|
+
record: {
|
|
533
|
+
schema: 1,
|
|
534
|
+
item: ctx.item,
|
|
535
|
+
intent: {kind: 'needs-attention'},
|
|
536
|
+
products: {
|
|
537
|
+
reason: reasonText(input.reason),
|
|
538
|
+
...(input.questions === undefined || input.questions.length === 0
|
|
539
|
+
? {}
|
|
540
|
+
: {questions: input.questions.map(reasonText)}),
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
bundle,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Map the build's FIRST WRITE (the recorded halt) to the handoff (task
|
|
549
|
+
* `ci-split-build-path-non-integrate-intents`). The seam call names the write;
|
|
550
|
+
* the pipeline's {@link BuildBoundary} annotation names the outcome when several
|
|
551
|
+
* share one seam call. A bounce's LOCAL half (the wip commit, which the seam
|
|
552
|
+
* would have made before pushing) runs here, so the wip travels in the bundle.
|
|
553
|
+
*/
|
|
554
|
+
function handOverHalt(intent: RecordedWriteIntent, ctx: HaltContext): Handover {
|
|
555
|
+
const call = `${intent.seam}.${intent.method}`;
|
|
556
|
+
const boundary = intent.annotation as BuildBoundary | undefined;
|
|
557
|
+
|
|
558
|
+
if (call === 'integrationLand.land') {
|
|
559
|
+
const land = intent.input as IntegrationLandInput;
|
|
560
|
+
if (land.branch !== workBranchRef('task', ctx.slug)) {
|
|
561
|
+
throw new Error(
|
|
562
|
+
`the land half names the branch ${land.branch}, not ` +
|
|
563
|
+
workBranchRef('task', ctx.slug),
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
const limit = HANDOFF_LIMITS.commentChars;
|
|
567
|
+
return {
|
|
568
|
+
record: {
|
|
569
|
+
schema: 1,
|
|
570
|
+
item: ctx.item,
|
|
571
|
+
intent: {kind: 'integrate'},
|
|
572
|
+
products: {
|
|
573
|
+
prTitle: land.title,
|
|
574
|
+
prBody: boundHandoffText(land.body ?? '', limit),
|
|
575
|
+
...(land.reviewProse === undefined
|
|
576
|
+
? {}
|
|
577
|
+
: {reviewProse: boundHandoffText(land.reviewProse, limit)}),
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
bundle: {repo: land.cwd, workBranch: land.branch, baseSha: ctx.baseSha},
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// The deadline checkpoint: its marker commit is already made (locally) when
|
|
585
|
+
// the branch save is recorded.
|
|
586
|
+
if (
|
|
587
|
+
call === 'refWrite.saveWorkBranch' &&
|
|
588
|
+
boundary?.kind === 'deadline-checkpoint'
|
|
589
|
+
) {
|
|
590
|
+
const input = intent.input as RouteToNeedsAttentionOptions;
|
|
591
|
+
commitAbortedWork({cwd: input.cwd, slug: ctx.slug, env: ctx.env});
|
|
592
|
+
const bundle = wipBundle(input.cwd, ctx);
|
|
593
|
+
if (!boundary.predecessorGone) {
|
|
594
|
+
const reason = deadlineSurfaceReason({
|
|
595
|
+
slug: ctx.slug,
|
|
596
|
+
kind: 'unreaped',
|
|
597
|
+
detail: boundary.reapDetail,
|
|
598
|
+
});
|
|
599
|
+
return needsAttention(ctx, {reason}, bundle);
|
|
600
|
+
}
|
|
601
|
+
if (bundle === undefined) {
|
|
602
|
+
// Nothing on the branch: no progress, so today's route surfaces.
|
|
603
|
+
const reason = deadlineSurfaceReason({
|
|
604
|
+
slug: ctx.slug,
|
|
605
|
+
kind: 'no-progress',
|
|
606
|
+
});
|
|
607
|
+
return needsAttention(ctx, {reason}, undefined);
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
record: {
|
|
611
|
+
schema: 1,
|
|
612
|
+
item: ctx.item,
|
|
613
|
+
intent: {kind: 'deadline-checkpoint'},
|
|
614
|
+
products: {},
|
|
615
|
+
},
|
|
616
|
+
bundle,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// The empty-diff STOP surfaces a question on main without a branch save.
|
|
621
|
+
if (boundary?.kind === 'stop' && boundary.stopKind === 'empty-diff') {
|
|
622
|
+
return {
|
|
623
|
+
record: {
|
|
624
|
+
schema: 1,
|
|
625
|
+
item: ctx.item,
|
|
626
|
+
intent: {kind: 'stop'},
|
|
627
|
+
products: {
|
|
628
|
+
reason: reasonText(boundary.reason),
|
|
629
|
+
stopKind: 'empty-diff',
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (call === 'ledgerWrite.applyNeedsAttentionTransition') {
|
|
636
|
+
const input = intent.input as RouteToNeedsAttentionOptions;
|
|
637
|
+
commitAbortedWork({cwd: input.cwd, slug: ctx.slug, env: ctx.env});
|
|
638
|
+
const bundle =
|
|
639
|
+
input.pushBranch === false ? undefined : wipBundle(input.cwd, ctx);
|
|
640
|
+
if (boundary?.kind === 'agent-failed') {
|
|
641
|
+
return {record: agentFailed(ctx.item, boundary.failureDetail), bundle};
|
|
642
|
+
}
|
|
643
|
+
if (boundary?.kind === 'stop') {
|
|
644
|
+
return {
|
|
645
|
+
record: {
|
|
646
|
+
schema: 1,
|
|
647
|
+
item: ctx.item,
|
|
648
|
+
intent: {kind: 'stop'},
|
|
649
|
+
products: {
|
|
650
|
+
reason: reasonText(boundary.reason),
|
|
651
|
+
stopKind: boundary.stopKind,
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
bundle,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
return needsAttention(ctx, input, bundle);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// A tree-less surface (the continue rebase conflicted at onboarding): the
|
|
661
|
+
// kept branch is untouched on the arbiter, so nothing is bundled.
|
|
662
|
+
if (call === 'ledgerWrite.applyTreelessNeedsAttentionTransition') {
|
|
663
|
+
const input = intent.input as {reason: string; questions?: string[]};
|
|
664
|
+
return needsAttention(ctx, input, undefined);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return {record: agentFailed(ctx.item, haltDetail(intent))};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* The agent phase: after a read-only lock-ownership check, run the build
|
|
672
|
+
* pipeline (`performDo`, without its claim) under the phase recorder, halt at
|
|
673
|
+
* the first write, and write the handoff. Holds a read-only token: it never
|
|
674
|
+
* writes to the arbiter.
|
|
675
|
+
*/
|
|
676
|
+
export async function performBuildAgentPhase(
|
|
677
|
+
options: BuildPhaseOptions,
|
|
678
|
+
): Promise<BuildPhaseResult> {
|
|
679
|
+
const note = options.note ?? (() => {});
|
|
680
|
+
const arbiter = options.arbiter ?? DEFAULT_ARBITER;
|
|
681
|
+
const cwd = options.cwd;
|
|
682
|
+
const env = options.env ?? process.env;
|
|
683
|
+
const held = requireHeldLock({
|
|
684
|
+
lockOutputs: options.lockOutputs,
|
|
685
|
+
env,
|
|
686
|
+
rung: 'build-task',
|
|
687
|
+
});
|
|
688
|
+
if (options.handoffDir === undefined) {
|
|
689
|
+
throw new PhaseDriverError('the agent phase needs a handoff directory');
|
|
690
|
+
}
|
|
691
|
+
const slug = buildSlug(options, options.repoPath ?? cwd);
|
|
692
|
+
const item = `task:${slug}`;
|
|
693
|
+
|
|
694
|
+
const ownership = await checkLockOwnership({
|
|
695
|
+
cwd,
|
|
696
|
+
arbiter,
|
|
697
|
+
item,
|
|
698
|
+
lockSha: held.lockSha,
|
|
699
|
+
env,
|
|
700
|
+
});
|
|
701
|
+
if (!ownership.owned) {
|
|
702
|
+
return {
|
|
703
|
+
exitCode: 0,
|
|
704
|
+
outcome: 'stale-lock',
|
|
705
|
+
slug,
|
|
706
|
+
message: ownership.message,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const recorder = createPhaseRecorder({record: buildRecordableCalls()});
|
|
711
|
+
const ctx: HaltContext = {item, slug, arbiter, baseSha: held.baseSha, env};
|
|
712
|
+
let handover: Handover;
|
|
713
|
+
try {
|
|
714
|
+
// EVERY review provider the pipeline can reach records (never only an
|
|
715
|
+
// injected one): without an injected instance, resolve the one the
|
|
716
|
+
// integration core would select from the arbiter URL, and wrap it.
|
|
717
|
+
const provider =
|
|
718
|
+
options.providerInstance ??
|
|
719
|
+
selectProvider({arbiterUrl: await arbiterUrl(cwd, arbiter, env)});
|
|
720
|
+
const outcome = await runAgentPhase(recorder, () =>
|
|
721
|
+
performDo({
|
|
722
|
+
...options,
|
|
723
|
+
phase: 'agent',
|
|
724
|
+
providerInstance: recordingReviewProvider(provider, recorder),
|
|
725
|
+
}),
|
|
726
|
+
);
|
|
727
|
+
handover = outcome.halted
|
|
728
|
+
? handOverHalt(outcome.intent, ctx)
|
|
729
|
+
: {
|
|
730
|
+
record: agentFailed(
|
|
731
|
+
item,
|
|
732
|
+
`the build ended without reaching a write: ${outcome.result.message}`,
|
|
733
|
+
),
|
|
734
|
+
};
|
|
735
|
+
} catch (err) {
|
|
736
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
737
|
+
handover = {record: agentFailed(item, `the build failed: ${message}`)};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const written = writeHandoff({
|
|
741
|
+
dir: options.handoffDir,
|
|
742
|
+
rung: 'build-task',
|
|
743
|
+
record: handover.record,
|
|
744
|
+
bundle: handover.bundle,
|
|
745
|
+
});
|
|
746
|
+
if (written.lfsMissing.length > 0) {
|
|
747
|
+
note(
|
|
748
|
+
`the local LFS store lacks ${written.lfsMissing.length} object(s) a new ` +
|
|
749
|
+
`commit points at (${written.lfsMissing.slice(0, 5).join(', ')}); the ` +
|
|
750
|
+
'apply job will reject the handoff',
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
const kind = handover.record.intent.kind;
|
|
754
|
+
const message = `handed over ${kind} for ${item}`;
|
|
755
|
+
return {
|
|
756
|
+
exitCode: 0,
|
|
757
|
+
outcome: 'handed-over',
|
|
758
|
+
slug,
|
|
759
|
+
message,
|
|
760
|
+
intent: kind,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function agentFailed(item: string, detail: string): HandoffRecord {
|
|
765
|
+
return {
|
|
766
|
+
schema: 1,
|
|
767
|
+
item,
|
|
768
|
+
intent: {kind: 'agent-failed'},
|
|
769
|
+
products: {
|
|
770
|
+
failureDetail: boundHandoffText(detail, HANDOFF_LIMITS.reasonChars),
|
|
771
|
+
},
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// ---------------------------------------------------------------------------
|
|
776
|
+
// apply
|
|
777
|
+
// ---------------------------------------------------------------------------
|
|
778
|
+
|
|
779
|
+
/** Surface the item to needs-attention (tree-less) with `reason`. */
|
|
780
|
+
async function surface(params: {
|
|
781
|
+
cwd: string;
|
|
782
|
+
arbiter: string;
|
|
783
|
+
slug: string;
|
|
784
|
+
reason: string;
|
|
785
|
+
env: NodeJS.ProcessEnv;
|
|
786
|
+
note: (message: string) => void;
|
|
787
|
+
outcome: BuildPhaseOutcome;
|
|
788
|
+
}): Promise<BuildPhaseResult> {
|
|
789
|
+
const surfaced = await ledgerWrite.applyTreelessNeedsAttentionTransition({
|
|
790
|
+
cwd: params.cwd,
|
|
791
|
+
slug: params.slug,
|
|
792
|
+
reason: params.reason,
|
|
793
|
+
arbiter: params.arbiter,
|
|
794
|
+
env: params.env,
|
|
795
|
+
note: params.note,
|
|
796
|
+
});
|
|
797
|
+
const where = surfaced.moved
|
|
798
|
+
? 'surfaced it to needs-attention'
|
|
799
|
+
: `could not surface it (${surfaced.reasonNotMoved ?? 'unknown'})`;
|
|
800
|
+
// A clean surface is green ("A handled outcome is green",
|
|
801
|
+
// `ci-phase-driver.ts`); only a surface that did not land stays red.
|
|
802
|
+
return {
|
|
803
|
+
exitCode: surfaced.moved ? 0 : 1,
|
|
804
|
+
outcome: surfaced.moved ? params.outcome : 'surface-unmoved',
|
|
805
|
+
slug: params.slug,
|
|
806
|
+
message: `${params.reason}; ${where}.`,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* The apply phase: check the lock is still this run's, validate the handoff as
|
|
812
|
+
* hostile, then resume the build at the intent's write half. Runs no agent and
|
|
813
|
+
* no repository code; every write goes through dorfl's seams.
|
|
814
|
+
*/
|
|
815
|
+
export async function performBuildApplyPhase(
|
|
816
|
+
options: BuildPhaseOptions,
|
|
817
|
+
): Promise<BuildPhaseResult> {
|
|
818
|
+
const note = options.note ?? (() => {});
|
|
819
|
+
const arbiter = options.arbiter ?? DEFAULT_ARBITER;
|
|
820
|
+
const cwd = options.cwd;
|
|
821
|
+
const env = noSmudgeEnv(
|
|
822
|
+
identityEnv(options.identity, options.env ?? process.env),
|
|
823
|
+
);
|
|
824
|
+
const held: HeldLock = requireHeldLock({
|
|
825
|
+
lockOutputs: options.lockOutputs,
|
|
826
|
+
env,
|
|
827
|
+
rung: 'build-task',
|
|
828
|
+
});
|
|
829
|
+
if (options.agentResult === undefined) {
|
|
830
|
+
throw new PhaseDriverError(
|
|
831
|
+
'the apply phase needs the agent job result (--agent-result, ' +
|
|
832
|
+
'needs.agent.result): only success reads the handoff',
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
const agentResult = options.agentResult;
|
|
836
|
+
const slug = buildSlug(options, options.repoPath ?? cwd);
|
|
837
|
+
const item = `task:${slug}`;
|
|
838
|
+
|
|
839
|
+
// Lock ownership BEFORE the first write (spec §8): a lock that was released,
|
|
840
|
+
// reaped or re-taken since the lock job ran means this run writes nothing.
|
|
841
|
+
// This is also what makes a "Re-run failed jobs" of a finished run safe: it
|
|
842
|
+
// replays the old lock outputs and artifact, but the first apply already
|
|
843
|
+
// released or surfaced the item, so the lock is gone and nothing is written:
|
|
844
|
+
// a handled outcome, so the re-run is green.
|
|
845
|
+
const ownership = await checkLockOwnership({
|
|
846
|
+
cwd,
|
|
847
|
+
arbiter,
|
|
848
|
+
item,
|
|
849
|
+
lockSha: held.lockSha,
|
|
850
|
+
env,
|
|
851
|
+
});
|
|
852
|
+
if (!ownership.owned) {
|
|
853
|
+
return {
|
|
854
|
+
exitCode: 0,
|
|
855
|
+
outcome: 'stale-lock',
|
|
856
|
+
slug,
|
|
857
|
+
message: ownership.message,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Every lock release from here on (the land's, the surface's, the
|
|
862
|
+
// return-to-backlog's) is leased on the sha this run owns: none of them may
|
|
863
|
+
// delete a lock another run took since the ownership check.
|
|
864
|
+
const restoreLease = leaseLockReleases(item, held.lockSha);
|
|
865
|
+
try {
|
|
866
|
+
const ctx: ApplyContext = {options, held, slug, item, arbiter, cwd, env};
|
|
867
|
+
// The agent job's result (decision 5) BEFORE any artifact is read: only
|
|
868
|
+
// `success` reads it; the build path has no agent-less rung, so a
|
|
869
|
+
// `skipped` agent job is never the deterministic case here.
|
|
870
|
+
const decision = await resolveAgentResult({
|
|
871
|
+
result: agentResult,
|
|
872
|
+
held,
|
|
873
|
+
agentTimeoutMinutes: options.agentTimeoutMinutes,
|
|
874
|
+
api: options.actionsApi,
|
|
875
|
+
env,
|
|
876
|
+
});
|
|
877
|
+
switch (decision.action) {
|
|
878
|
+
case 'read-handoff':
|
|
879
|
+
return await applyOwned(ctx);
|
|
880
|
+
case 'deterministic':
|
|
881
|
+
return await surfaceAgentResult(
|
|
882
|
+
ctx,
|
|
883
|
+
'the agent job was skipped (needsAgent: false), but the build ' +
|
|
884
|
+
'path has no rung that runs without an agent; the handoff was not read',
|
|
885
|
+
);
|
|
886
|
+
case 'surface':
|
|
887
|
+
return await surfaceAgentResult(ctx, decision.reason);
|
|
888
|
+
case 'release':
|
|
889
|
+
return await releaseAfterCancel(ctx, decision.reason);
|
|
890
|
+
}
|
|
891
|
+
} finally {
|
|
892
|
+
restoreLease();
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/** What {@link applyOwned} works with (the apply phase, past the ownership check). */
|
|
897
|
+
interface ApplyContext {
|
|
898
|
+
options: BuildPhaseOptions;
|
|
899
|
+
held: HeldLock;
|
|
900
|
+
slug: string;
|
|
901
|
+
item: string;
|
|
902
|
+
arbiter: string;
|
|
903
|
+
cwd: string;
|
|
904
|
+
env: NodeJS.ProcessEnv;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* The apply phase once the lock is known to be this run's: validate the
|
|
909
|
+
* handoff, publish a continued branch with its lease, then resume at the
|
|
910
|
+
* intent's write half.
|
|
911
|
+
*/
|
|
912
|
+
async function applyOwned(ctx: ApplyContext): Promise<BuildPhaseResult> {
|
|
913
|
+
const {options, held, slug, item, arbiter, cwd, env} = ctx;
|
|
914
|
+
const note = options.note ?? (() => {});
|
|
915
|
+
|
|
916
|
+
let validated;
|
|
917
|
+
try {
|
|
918
|
+
if (options.handoffDir === undefined) {
|
|
919
|
+
throw new HandoffRejected('layout', 'no handoff directory was given');
|
|
920
|
+
}
|
|
921
|
+
validated = validateApplyHandoff({
|
|
922
|
+
dir: options.handoffDir,
|
|
923
|
+
runnerTemp: runnerTempFrom(options.runnerTemp, env),
|
|
924
|
+
repo: cwd,
|
|
925
|
+
trust: {
|
|
926
|
+
item,
|
|
927
|
+
rung: 'build-task',
|
|
928
|
+
baseSha: held.baseSha,
|
|
929
|
+
arbiter,
|
|
930
|
+
integrationMode: options.integration ?? 'propose',
|
|
931
|
+
},
|
|
932
|
+
env,
|
|
933
|
+
});
|
|
934
|
+
} catch (err) {
|
|
935
|
+
if (!(err instanceof HandoffRejected)) throw err;
|
|
936
|
+
note(err.message);
|
|
937
|
+
return surface({
|
|
938
|
+
cwd,
|
|
939
|
+
arbiter,
|
|
940
|
+
slug,
|
|
941
|
+
reason: err.message,
|
|
942
|
+
env,
|
|
943
|
+
note,
|
|
944
|
+
outcome: 'rejected',
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const record = validated.handoff.record;
|
|
949
|
+
const bundle = validated.bundle;
|
|
950
|
+
|
|
951
|
+
// The LFS objects go FIRST (decision 6), before any ref of any intent (the
|
|
952
|
+
// continue push, the land's pushes, a WIP branch save), so a ref never lands
|
|
953
|
+
// pointing at a missing object.
|
|
954
|
+
const lfs = await pushHandoffLfs({
|
|
955
|
+
cwd,
|
|
956
|
+
arbiter,
|
|
957
|
+
objects: validated.lfsObjects,
|
|
958
|
+
env,
|
|
959
|
+
note,
|
|
960
|
+
});
|
|
961
|
+
if (!lfs.ok) {
|
|
962
|
+
return surface({
|
|
963
|
+
cwd,
|
|
964
|
+
arbiter,
|
|
965
|
+
slug,
|
|
966
|
+
reason: lfs.reason,
|
|
967
|
+
env,
|
|
968
|
+
note,
|
|
969
|
+
outcome: lfs.rejected ? 'rejected' : 'surfaced',
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (bundle !== undefined) {
|
|
974
|
+
await gitHard(
|
|
975
|
+
['checkout', '--quiet', '-B', bundle.workBranch, bundle.tip],
|
|
976
|
+
cwd,
|
|
977
|
+
env,
|
|
978
|
+
);
|
|
979
|
+
// The CONTINUE push (decision 7): the agent job rebased the kept branch
|
|
980
|
+
// locally; publish it with a lease on the tip the LOCK job observed, before
|
|
981
|
+
// any other write. A branch that moved since then refuses the push, and
|
|
982
|
+
// this run writes nothing at all.
|
|
983
|
+
if (held.continueTip !== undefined) {
|
|
984
|
+
const pushed = await refWrite.pushLeasedWorkBranch({
|
|
985
|
+
arbiter,
|
|
986
|
+
branch: bundle.workBranch,
|
|
987
|
+
commit: bundle.tip,
|
|
988
|
+
expectedTip: held.continueTip,
|
|
989
|
+
cwd,
|
|
990
|
+
env,
|
|
991
|
+
});
|
|
992
|
+
if (pushed.status !== 0) {
|
|
993
|
+
const message =
|
|
994
|
+
`${bundle.workBranch} on ${arbiter} moved since the lock job saw it ` +
|
|
995
|
+
`at ${held.continueTip}, so the leased push of the continued branch ` +
|
|
996
|
+
`was refused (${pushed.stderr.trim()}). Nothing was written and the ` +
|
|
997
|
+
`lock of ${item} is still held: inspect the branch, then \`dorfl ` +
|
|
998
|
+
`requeue ${slug}\` to retry the item.`;
|
|
999
|
+
return {exitCode: 1, outcome: 'stale-lease', slug, message};
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const pushBranch = bundle !== undefined;
|
|
1005
|
+
const branch = workBranchRef('task', slug);
|
|
1006
|
+
switch (record.intent.kind) {
|
|
1007
|
+
case 'integrate':
|
|
1008
|
+
return applyIntegrate(ctx, validated);
|
|
1009
|
+
case 'needs-attention': {
|
|
1010
|
+
const {reason, questions} = record.products as NeedsAttentionProducts;
|
|
1011
|
+
const routed = pushBranch
|
|
1012
|
+
? await ledgerWrite.applyNeedsAttentionTransition({
|
|
1013
|
+
cwd,
|
|
1014
|
+
slug,
|
|
1015
|
+
reason,
|
|
1016
|
+
questions,
|
|
1017
|
+
arbiter,
|
|
1018
|
+
env,
|
|
1019
|
+
note,
|
|
1020
|
+
})
|
|
1021
|
+
: await ledgerWrite.applyTreelessNeedsAttentionTransition({
|
|
1022
|
+
cwd,
|
|
1023
|
+
slug,
|
|
1024
|
+
reason,
|
|
1025
|
+
questions,
|
|
1026
|
+
arbiter,
|
|
1027
|
+
env,
|
|
1028
|
+
note,
|
|
1029
|
+
});
|
|
1030
|
+
const message = routed.moved
|
|
1031
|
+
? `Surfaced '${slug}' to needs-attention: ${reason}`
|
|
1032
|
+
: `Could not surface '${slug}' (${routed.reasonNotMoved ?? 'unknown'}): ${reason}`;
|
|
1033
|
+
return {
|
|
1034
|
+
exitCode: routed.moved ? 0 : 1,
|
|
1035
|
+
outcome: routed.moved ? 'surfaced' : 'surface-unmoved',
|
|
1036
|
+
slug,
|
|
1037
|
+
message,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
case 'stop': {
|
|
1041
|
+
const stop = record.products as StopProducts;
|
|
1042
|
+
return fromDoResult(
|
|
1043
|
+
await saveAgentStop({
|
|
1044
|
+
slug,
|
|
1045
|
+
branch,
|
|
1046
|
+
cwd,
|
|
1047
|
+
arbiter,
|
|
1048
|
+
reason: stop.reason,
|
|
1049
|
+
kind: stop.stopKind,
|
|
1050
|
+
pushBranch,
|
|
1051
|
+
env,
|
|
1052
|
+
note,
|
|
1053
|
+
}),
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
case 'agent-failed':
|
|
1057
|
+
return fromDoResult(
|
|
1058
|
+
await saveAgentFailure({
|
|
1059
|
+
slug,
|
|
1060
|
+
branch,
|
|
1061
|
+
cwd,
|
|
1062
|
+
arbiter,
|
|
1063
|
+
detail: (record.products as AgentFailedProducts).failureDetail,
|
|
1064
|
+
pushBranch,
|
|
1065
|
+
env,
|
|
1066
|
+
note,
|
|
1067
|
+
}),
|
|
1068
|
+
);
|
|
1069
|
+
case 'deadline-checkpoint':
|
|
1070
|
+
return fromDoResult(
|
|
1071
|
+
await routeDeadlineCheckpoint({
|
|
1072
|
+
slug,
|
|
1073
|
+
branch,
|
|
1074
|
+
cwd,
|
|
1075
|
+
arbiter,
|
|
1076
|
+
maxAutoCheckpoints: maxAutoCheckpointsAt(
|
|
1077
|
+
cwd,
|
|
1078
|
+
held.baseSha,
|
|
1079
|
+
options.maxAutoCheckpoints,
|
|
1080
|
+
env,
|
|
1081
|
+
),
|
|
1082
|
+
env,
|
|
1083
|
+
note,
|
|
1084
|
+
}),
|
|
1085
|
+
);
|
|
1086
|
+
default:
|
|
1087
|
+
return surface({
|
|
1088
|
+
cwd,
|
|
1089
|
+
arbiter,
|
|
1090
|
+
slug,
|
|
1091
|
+
reason:
|
|
1092
|
+
`the agent job handed over ${record.intent.kind}, which the build ` +
|
|
1093
|
+
'path does not carry',
|
|
1094
|
+
env,
|
|
1095
|
+
note,
|
|
1096
|
+
outcome: 'surfaced',
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Surface the item to needs-attention because the agent job did not succeed
|
|
1103
|
+
* (decision 5): tree-less (nothing from the agent job is pushed: its artifact is
|
|
1104
|
+
* not read), the lock released leased on `lockSha`. A kept work branch stays as
|
|
1105
|
+
* it is on the arbiter.
|
|
1106
|
+
*/
|
|
1107
|
+
async function surfaceAgentResult(
|
|
1108
|
+
ctx: ApplyContext,
|
|
1109
|
+
reason: string,
|
|
1110
|
+
): Promise<BuildPhaseResult> {
|
|
1111
|
+
const {options, slug, arbiter, cwd, env} = ctx;
|
|
1112
|
+
const note = options.note ?? (() => {});
|
|
1113
|
+
const routed = await ledgerWrite.applyTreelessNeedsAttentionTransition({
|
|
1114
|
+
cwd,
|
|
1115
|
+
slug,
|
|
1116
|
+
reason,
|
|
1117
|
+
arbiter,
|
|
1118
|
+
env,
|
|
1119
|
+
note,
|
|
1120
|
+
});
|
|
1121
|
+
const message = routed.moved
|
|
1122
|
+
? `Surfaced '${slug}' to needs-attention: ${reason}`
|
|
1123
|
+
: `Could not surface '${slug}' (${routed.reasonNotMoved ?? 'unknown'}): ${reason}`;
|
|
1124
|
+
return {
|
|
1125
|
+
exitCode: routed.moved ? 0 : 1,
|
|
1126
|
+
outcome: routed.moved ? 'surfaced' : 'surface-unmoved',
|
|
1127
|
+
slug,
|
|
1128
|
+
message,
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* The agent job was cancelled before its timeout (decision 5): only release the
|
|
1134
|
+
* lock, leased on `lockSha`, so the next run picks the item up again.
|
|
1135
|
+
*/
|
|
1136
|
+
async function releaseAfterCancel(
|
|
1137
|
+
ctx: ApplyContext,
|
|
1138
|
+
reason: string,
|
|
1139
|
+
): Promise<BuildPhaseResult> {
|
|
1140
|
+
const {options, held, slug, item, arbiter, cwd, env} = ctx;
|
|
1141
|
+
const note = options.note ?? (() => {});
|
|
1142
|
+
const released = await releaseLockLeased({
|
|
1143
|
+
cwd,
|
|
1144
|
+
arbiter,
|
|
1145
|
+
item,
|
|
1146
|
+
expectedSha: held.lockSha,
|
|
1147
|
+
env,
|
|
1148
|
+
});
|
|
1149
|
+
note(released.message);
|
|
1150
|
+
const message = released.released
|
|
1151
|
+
? `Released '${slug}': ${reason}.`
|
|
1152
|
+
: `Could not release '${slug}' (${released.message}): ${reason}.`;
|
|
1153
|
+
return {
|
|
1154
|
+
exitCode: released.released ? 0 : 1,
|
|
1155
|
+
outcome: released.released ? 'released' : 'release-refused',
|
|
1156
|
+
slug,
|
|
1157
|
+
message,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/** Map a reused `do` write half's result to the apply phase's. */
|
|
1162
|
+
function fromDoResult(r: DoResult): BuildPhaseResult {
|
|
1163
|
+
const outcome: BuildPhaseOutcome =
|
|
1164
|
+
r.outcome === 'deadline-auto-continued'
|
|
1165
|
+
? 'auto-continued'
|
|
1166
|
+
: r.routedToNeedsAttention === true
|
|
1167
|
+
? 'surfaced'
|
|
1168
|
+
: 'surface-unmoved';
|
|
1169
|
+
return {exitCode: r.exitCode, outcome, slug: r.slug, message: r.message};
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* The `maxAutoCheckpoints` ceiling from the repository config AT `baseSha`
|
|
1174
|
+
* (trusted, the arbiter's `main` the lock job classified at), never from the
|
|
1175
|
+
* bundle: a work branch could raise its own ceiling otherwise. Falls back to
|
|
1176
|
+
* the resolved option, then to the default, when the file does not set a
|
|
1177
|
+
* valid value.
|
|
1178
|
+
*/
|
|
1179
|
+
function maxAutoCheckpointsAt(
|
|
1180
|
+
cwd: string,
|
|
1181
|
+
baseSha: string,
|
|
1182
|
+
fallback: number | undefined,
|
|
1183
|
+
env: NodeJS.ProcessEnv,
|
|
1184
|
+
): number {
|
|
1185
|
+
const cap = repoConfigAt(cwd, baseSha, env)?.maxAutoCheckpoints;
|
|
1186
|
+
if (typeof cap === 'number' && Number.isInteger(cap) && cap >= 1) {
|
|
1187
|
+
return cap;
|
|
1188
|
+
}
|
|
1189
|
+
return fallback ?? 5;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/** The `integrate` write half: the land loop, the review comment, the release. */
|
|
1193
|
+
async function applyIntegrate(
|
|
1194
|
+
ctx: ApplyContext,
|
|
1195
|
+
validated: ReturnType<typeof validateApplyHandoff>,
|
|
1196
|
+
): Promise<BuildPhaseResult> {
|
|
1197
|
+
const {options, held, slug, item, arbiter, cwd, env} = ctx;
|
|
1198
|
+
const note = options.note ?? (() => {});
|
|
1199
|
+
const record = validated.handoff.record;
|
|
1200
|
+
if (record.intent.kind !== 'integrate' || validated.bundle === undefined) {
|
|
1201
|
+
throw new Error('applyIntegrate needs an integrate handoff with a bundle');
|
|
1202
|
+
}
|
|
1203
|
+
const products = record.products as {
|
|
1204
|
+
prTitle: string;
|
|
1205
|
+
prBody: string;
|
|
1206
|
+
reviewProse?: string;
|
|
1207
|
+
};
|
|
1208
|
+
const branch = validated.bundle.workBranch;
|
|
1209
|
+
const tip = validated.bundle.tip;
|
|
1210
|
+
if (validated.forcedPropose) {
|
|
1211
|
+
note(
|
|
1212
|
+
`Untrusted-origin task '${slug}': forcing the BUILD transition to ` +
|
|
1213
|
+
'propose (recomputed from the task at the base).',
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
const commitMessage = (
|
|
1217
|
+
await gitHard(['log', '-1', '--format=%s', tip], cwd, env)
|
|
1218
|
+
).trim();
|
|
1219
|
+
const land = await integrationLand.land({
|
|
1220
|
+
cwd,
|
|
1221
|
+
arbiter,
|
|
1222
|
+
slug,
|
|
1223
|
+
branch,
|
|
1224
|
+
lifecycle: false,
|
|
1225
|
+
source: taskSourceAtBase(cwd, held.baseSha, slug, env),
|
|
1226
|
+
surfaceArbiter: arbiter,
|
|
1227
|
+
commitMessage,
|
|
1228
|
+
env,
|
|
1229
|
+
note,
|
|
1230
|
+
mode: validated.mode,
|
|
1231
|
+
noPR: options.noPR,
|
|
1232
|
+
providerInstance: options.providerInstance,
|
|
1233
|
+
title: products.prTitle,
|
|
1234
|
+
body: proposeBody(products.prBody, validated.ledgerReport),
|
|
1235
|
+
reviewProse: products.reviewProse,
|
|
1236
|
+
mergeRetries: options.mergeRetries,
|
|
1237
|
+
mergeJitterMs: options.mergeJitterMs,
|
|
1238
|
+
// The landed-vs-gated report (decision 2): the agent job gated the
|
|
1239
|
+
// bundle tip, so a merge land that lands another tree says so.
|
|
1240
|
+
gatedTip: tip,
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
if (land.outcome !== 'completed') {
|
|
1244
|
+
return {
|
|
1245
|
+
exitCode: land.routedToNeedsAttention ? 0 : 1,
|
|
1246
|
+
outcome: land.routedToNeedsAttention ? 'surfaced' : 'usage-error',
|
|
1247
|
+
slug,
|
|
1248
|
+
message: land.reason ?? `the land ended as ${land.outcome}`,
|
|
1249
|
+
land,
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
const integration = land.integration!;
|
|
1253
|
+
if (integration.mergedToMain === true || integration.alreadyLanded === true) {
|
|
1254
|
+
const released = await releaseLockLeased({
|
|
1255
|
+
cwd,
|
|
1256
|
+
arbiter,
|
|
1257
|
+
item,
|
|
1258
|
+
expectedSha: held.lockSha,
|
|
1259
|
+
env,
|
|
1260
|
+
});
|
|
1261
|
+
note(released.message);
|
|
1262
|
+
return {
|
|
1263
|
+
exitCode: 0,
|
|
1264
|
+
outcome: 'landed',
|
|
1265
|
+
slug,
|
|
1266
|
+
message: `Completed '${slug}': merged to ${arbiter}/main.`,
|
|
1267
|
+
land,
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
// The lock stays held until the PR merges; stamp it as kept for a propose
|
|
1271
|
+
// PR (task `wire-merge-questions-into-the-advance-tick`, decision 4), leased
|
|
1272
|
+
// on this run's `lockSha`, so the merge-question surfacer and an answered
|
|
1273
|
+
// `merge` can tell it from a live build. Best-effort: an unmarked kept lock
|
|
1274
|
+
// only keeps the branch out of the merge-question loop.
|
|
1275
|
+
const marked = await markLockKeptForProposePr({
|
|
1276
|
+
item,
|
|
1277
|
+
cwd,
|
|
1278
|
+
arbiter,
|
|
1279
|
+
expectedSha: held.lockSha,
|
|
1280
|
+
env,
|
|
1281
|
+
});
|
|
1282
|
+
if (marked.outcome !== 'transitioned') note(marked.message);
|
|
1283
|
+
// Say "for review" ONLY when a PR was opened (task
|
|
1284
|
+
// `intake-reports-the-pr-it-actually-opened`): a degraded `gh pr create`
|
|
1285
|
+
// pushed the branch but opened nothing, and the land already printed why.
|
|
1286
|
+
const notOpened = proposeRequestNotOpened(integration);
|
|
1287
|
+
return {
|
|
1288
|
+
exitCode: 0,
|
|
1289
|
+
outcome: 'proposed',
|
|
1290
|
+
slug,
|
|
1291
|
+
message:
|
|
1292
|
+
notOpened === undefined
|
|
1293
|
+
? `Completed '${slug}': pushed ${branch} for review; the lock stays ` +
|
|
1294
|
+
'held until the PR merges.'
|
|
1295
|
+
: `Completed '${slug}': pushed ${branch} but opened NO PR ` +
|
|
1296
|
+
`(${notOpened}); the lock stays held until the branch lands.`,
|
|
1297
|
+
land,
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* The propose PR body: the agent's summary (from the validated record) with the
|
|
1303
|
+
* bundle validation's ledger report appended (user story 23). The report is
|
|
1304
|
+
* never cut; the summary is shortened to keep the whole under the comment limit.
|
|
1305
|
+
*/
|
|
1306
|
+
function proposeBody(
|
|
1307
|
+
prBody: string,
|
|
1308
|
+
ledgerReport: string | undefined,
|
|
1309
|
+
): string | undefined {
|
|
1310
|
+
if (ledgerReport === undefined) return prBody === '' ? undefined : prBody;
|
|
1311
|
+
const room = HANDOFF_LIMITS.commentChars - ledgerReport.length - 2;
|
|
1312
|
+
const summary = boundHandoffText(prBody, Math.max(0, room));
|
|
1313
|
+
return summary === '' ? ledgerReport : `${summary}\n\n${ledgerReport}`;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
async function gitHard(
|
|
1317
|
+
args: string[],
|
|
1318
|
+
cwd: string,
|
|
1319
|
+
env: NodeJS.ProcessEnv,
|
|
1320
|
+
): Promise<string> {
|
|
1321
|
+
const r = await runAsync('git', args, cwd, {env});
|
|
1322
|
+
if (r.status !== 0) {
|
|
1323
|
+
throw new Error(
|
|
1324
|
+
`git ${args.join(' ')} failed (exit ${r.status}): ${r.stderr.trim()}`,
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
return r.stdout;
|
|
1328
|
+
}
|