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
|
@@ -0,0 +1,989 @@
|
|
|
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
|
+
import { existsSync } from 'node:fs';
|
|
53
|
+
import { join } from 'node:path';
|
|
54
|
+
import { classifyTick } from './advance-classify.js';
|
|
55
|
+
import { readItemSignals } from './advance.js';
|
|
56
|
+
import { HANDOFF_LIMITS, HandoffRejected, handoffName as deriveHandoffName, } from './ci-handoff-format.js';
|
|
57
|
+
import { writeHandoff } from './ci-handoff.js';
|
|
58
|
+
import { validateApplyHandoff } from './ci-handoff-apply.js';
|
|
59
|
+
import { PhaseDriverError, agentTimeoutMinutesAt, arbiterLockSha, arbiterRefSha, boundHandoffText, checkLockOwnership, emitLockOutputs, fetchArbiterMain, noSmudgeEnv, pushHandoffLfs, releaseLockLeased, repoConfigAt, requireHeldLock, resolveAgentResult, runnerTempFrom, taskSourceAtBase, withBaseWorktree, } from './ci-phase-driver.js';
|
|
60
|
+
import { performClaim } from './claim-cas.js';
|
|
61
|
+
import { deadlineSurfaceReason, performDo, routeDeadlineCheckpoint, saveAgentFailure, saveAgentStop, } from './do.js';
|
|
62
|
+
import { checkGatePreconditions, detectLockfileOnDisk, } from './gate-readiness.js';
|
|
63
|
+
import { run, runAsync } from './git.js';
|
|
64
|
+
import { selectProvider } from './github.js';
|
|
65
|
+
import { identityEnv } from './identity.js';
|
|
66
|
+
import { leaseLockReleases, markLockKeptForProposePr } from './item-lock.js';
|
|
67
|
+
import { arbiterUrl, integrationLand, } from './integration-core.js';
|
|
68
|
+
import { proposeRequestNotOpened } from './integrator.js';
|
|
69
|
+
import { ledgerRead } from './ledger-read.js';
|
|
70
|
+
import { ledgerWrite } from './ledger-write.js';
|
|
71
|
+
import { commitAbortedWork, } from './needs-attention.js';
|
|
72
|
+
import { refWrite } from './ref-write.js';
|
|
73
|
+
import { createPhaseRecorder, recordingReviewProvider, runAgentPhase, } from './phase-recorder.js';
|
|
74
|
+
import { SlugResolutionError, resolveAdvanceArg, resolveSlug, workBranchRef, } from './slug-namespace.js';
|
|
75
|
+
import { workItemRel } from './work-layout.js';
|
|
76
|
+
const DEFAULT_ARBITER = 'origin';
|
|
77
|
+
/**
|
|
78
|
+
* Run one phase of the build path. The CLI calls this for `do <slug> --phase`
|
|
79
|
+
* and `advance task:<slug> --phase`.
|
|
80
|
+
*/
|
|
81
|
+
export async function performBuildPhase(options) {
|
|
82
|
+
try {
|
|
83
|
+
switch (options.phase) {
|
|
84
|
+
case 'lock':
|
|
85
|
+
return await performBuildLockPhase(options);
|
|
86
|
+
case 'agent':
|
|
87
|
+
return await performBuildAgentPhase(options);
|
|
88
|
+
case 'apply':
|
|
89
|
+
return await performBuildApplyPhase(options);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (err instanceof PhaseDriverError || err instanceof SlugResolutionError) {
|
|
94
|
+
return { exitCode: 1, outcome: 'usage-error', message: err.message };
|
|
95
|
+
}
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// lock
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
/** The task slug a build phase acts on, from the (trusted) workflow argument. */
|
|
103
|
+
function buildSlug(options, repoPath) {
|
|
104
|
+
if (options.verb === 'advance') {
|
|
105
|
+
const resolved = resolveAdvanceArg({
|
|
106
|
+
arg: options.arg,
|
|
107
|
+
repoPath,
|
|
108
|
+
read: options.read ?? ledgerRead,
|
|
109
|
+
});
|
|
110
|
+
if (resolved.namespace !== 'task') {
|
|
111
|
+
throw new PhaseDriverError(`--phase splits only the build path so far; ${options.arg} is not a task`);
|
|
112
|
+
}
|
|
113
|
+
return resolved.slug;
|
|
114
|
+
}
|
|
115
|
+
const resolved = resolveSlug({
|
|
116
|
+
arg: options.arg,
|
|
117
|
+
repoPath,
|
|
118
|
+
read: options.read ?? ledgerRead,
|
|
119
|
+
});
|
|
120
|
+
if (resolved.namespace !== 'task') {
|
|
121
|
+
throw new PhaseDriverError(`--phase splits only the build path so far; ${options.arg} is not a task`);
|
|
122
|
+
}
|
|
123
|
+
return resolved.slug;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The lock phase: classify the item at the arbiter's `main`, claim it and
|
|
127
|
+
* publish the trusted facts. Runs no agent and no repository code. An item that
|
|
128
|
+
* already advanced (not claimable at `baseSha`), or whose lock another run
|
|
129
|
+
* holds, is refused BEFORE any write.
|
|
130
|
+
*/
|
|
131
|
+
export async function performBuildLockPhase(options) {
|
|
132
|
+
const note = options.note ?? (() => { });
|
|
133
|
+
const arbiter = options.arbiter ?? DEFAULT_ARBITER;
|
|
134
|
+
const cwd = options.cwd;
|
|
135
|
+
const env = identityEnv(options.identity, options.env ?? process.env);
|
|
136
|
+
const githubOutput = options.githubOutput ?? env.GITHUB_OUTPUT;
|
|
137
|
+
const publish = (facts) => {
|
|
138
|
+
emitLockOutputs(facts, { githubOutput, note });
|
|
139
|
+
return facts;
|
|
140
|
+
};
|
|
141
|
+
const baseSha = await fetchArbiterMain({ cwd, arbiter, env });
|
|
142
|
+
const verdict = await withBaseWorktree({ cwd, baseSha, env }, async (base) => {
|
|
143
|
+
const slug = buildSlug(options, base);
|
|
144
|
+
if (options.verb === 'advance') {
|
|
145
|
+
const item = `task:${slug}`;
|
|
146
|
+
const signals = readItemSignals({
|
|
147
|
+
repoPath: base,
|
|
148
|
+
type: 'task',
|
|
149
|
+
slug,
|
|
150
|
+
item,
|
|
151
|
+
});
|
|
152
|
+
const kind = classifyTick({ type: 'task', ...signals }).kind;
|
|
153
|
+
if (kind !== 'build-task') {
|
|
154
|
+
if (kind === 'no-op') {
|
|
155
|
+
return { slug, skip: `no-op for ${item} at ${baseSha}` };
|
|
156
|
+
}
|
|
157
|
+
throw new PhaseDriverError(`--phase splits only the build path so far; ${item} classifies ` +
|
|
158
|
+
`as ${kind} at ${baseSha}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const folders = options.allowBacklog === true
|
|
162
|
+
? ['tasks-ready', 'tasks-backlog']
|
|
163
|
+
: ['tasks-ready'];
|
|
164
|
+
const claimable = folders.some((f) => existsSync(join(base, workItemRel(f, `${slug}.md`))));
|
|
165
|
+
if (!claimable) {
|
|
166
|
+
return {
|
|
167
|
+
slug,
|
|
168
|
+
skip: `'${slug}' is not claimable at ${arbiter}/main (${baseSha}): it ` +
|
|
169
|
+
'already advanced, or was removed',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const guard = checkGatePreconditions({
|
|
173
|
+
freshWorktreeGate: options.freshWorktreeGate,
|
|
174
|
+
prepare: options.prepare,
|
|
175
|
+
verify: options.verify,
|
|
176
|
+
lockfile: detectLockfileOnDisk(base),
|
|
177
|
+
});
|
|
178
|
+
if (guard !== undefined)
|
|
179
|
+
throw new PhaseDriverError(guard.message);
|
|
180
|
+
return { slug };
|
|
181
|
+
});
|
|
182
|
+
const slug = verdict.slug;
|
|
183
|
+
const item = `task:${slug}`;
|
|
184
|
+
if (verdict.skip !== undefined) {
|
|
185
|
+
return {
|
|
186
|
+
exitCode: 0,
|
|
187
|
+
outcome: 'no-op',
|
|
188
|
+
slug,
|
|
189
|
+
message: verdict.skip,
|
|
190
|
+
lockOutputs: publish({ acquired: false, baseSha }),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if ((await arbiterLockSha({ cwd, arbiter, item, env })) !== undefined) {
|
|
194
|
+
const message = `'${slug}' is already locked on ${arbiter}; backing off.`;
|
|
195
|
+
return {
|
|
196
|
+
exitCode: 0,
|
|
197
|
+
outcome: 'lost',
|
|
198
|
+
slug,
|
|
199
|
+
message,
|
|
200
|
+
lockOutputs: publish({ acquired: false, rung: 'build-task', baseSha }),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const claim = await performClaim({
|
|
204
|
+
slug,
|
|
205
|
+
cwd,
|
|
206
|
+
arbiter,
|
|
207
|
+
allowBacklog: options.allowBacklog === true,
|
|
208
|
+
env,
|
|
209
|
+
note,
|
|
210
|
+
});
|
|
211
|
+
if (claim.exitCode !== 0) {
|
|
212
|
+
const outcome = claim.outcome === 'lost' ? 'lost' : 'usage-error';
|
|
213
|
+
return {
|
|
214
|
+
// A claim lost to another run is handled, so green ("A handled
|
|
215
|
+
// outcome is green", `ci-phase-driver.ts`).
|
|
216
|
+
exitCode: outcome === 'lost' ? 0 : claim.exitCode,
|
|
217
|
+
outcome,
|
|
218
|
+
slug,
|
|
219
|
+
message: claim.message,
|
|
220
|
+
lockOutputs: publish({ acquired: false, rung: 'build-task', baseSha }),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const lockSha = await arbiterLockSha({ cwd, arbiter, item, env });
|
|
224
|
+
if (lockSha === undefined) {
|
|
225
|
+
throw new Error(`the lock of ${item} vanished right after the claim`);
|
|
226
|
+
}
|
|
227
|
+
const continueTip = await arbiterRefSha({
|
|
228
|
+
cwd,
|
|
229
|
+
arbiter,
|
|
230
|
+
ref: `refs/heads/${workBranchRef('task', slug)}`,
|
|
231
|
+
env,
|
|
232
|
+
});
|
|
233
|
+
const facts = publish({
|
|
234
|
+
acquired: true,
|
|
235
|
+
needsAgent: true,
|
|
236
|
+
rung: 'build-task',
|
|
237
|
+
baseSha,
|
|
238
|
+
lockSha,
|
|
239
|
+
continueTip,
|
|
240
|
+
handoffName: deriveHandoffName(item, options.runAttempt ?? env.GITHUB_RUN_ATTEMPT ?? '1'),
|
|
241
|
+
agentTimeoutMinutes: agentTimeoutMinutesAt(cwd, baseSha, env),
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
exitCode: 0,
|
|
245
|
+
outcome: 'locked',
|
|
246
|
+
slug,
|
|
247
|
+
message: `locked ${item} at ${baseSha} (lock ${lockSha})`,
|
|
248
|
+
lockOutputs: facts,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// agent
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
/** Every write seam call the build pipeline can reach (all are recorded). */
|
|
255
|
+
function buildRecordableCalls() {
|
|
256
|
+
return [
|
|
257
|
+
'integrationLand.land',
|
|
258
|
+
...Object.keys(ledgerWrite).map((m) => `ledgerWrite.${m}`),
|
|
259
|
+
...Object.keys(refWrite).map((m) => `refWrite.${m}`),
|
|
260
|
+
'reviewProvider.openRequest',
|
|
261
|
+
'reviewProvider.postPRComment',
|
|
262
|
+
'reviewProvider.postPRCommentOnBranch',
|
|
263
|
+
'reviewProvider.closeRequestOnBranch',
|
|
264
|
+
];
|
|
265
|
+
}
|
|
266
|
+
/** The `agent-failed` detail for a halt at a write the build path does not carry. */
|
|
267
|
+
function haltDetail(intent) {
|
|
268
|
+
const input = intent.input;
|
|
269
|
+
const reason = input !== null &&
|
|
270
|
+
typeof input === 'object' &&
|
|
271
|
+
typeof input.reason === 'string'
|
|
272
|
+
? `: ${input.reason}`
|
|
273
|
+
: '';
|
|
274
|
+
return (`the build stopped at ${intent.seam}.${intent.method}${reason} ` +
|
|
275
|
+
'(this outcome is not carried through the CI phases)');
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* The work branch to bundle as the WIP of a non-integrate intent, or
|
|
279
|
+
* `undefined` when it carries nothing to recover: the checkout is not on the
|
|
280
|
+
* item's work branch, or the branch has no commit that `<arbiter>/main` lacks
|
|
281
|
+
* (the apply phase refuses a bundle without a new commit).
|
|
282
|
+
*/
|
|
283
|
+
function wipBundle(cwd, ctx) {
|
|
284
|
+
const workBranch = workBranchRef('task', ctx.slug);
|
|
285
|
+
const head = run('git', ['symbolic-ref', '--quiet', 'HEAD'], cwd, {
|
|
286
|
+
env: ctx.env,
|
|
287
|
+
});
|
|
288
|
+
if (head.status !== 0 || head.stdout.trim() !== `refs/heads/${workBranch}`) {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
const ahead = run('git', [
|
|
292
|
+
'rev-list',
|
|
293
|
+
'--count',
|
|
294
|
+
`refs/heads/${workBranch}`,
|
|
295
|
+
`^refs/remotes/${ctx.arbiter}/main`,
|
|
296
|
+
`^${ctx.baseSha}`,
|
|
297
|
+
], cwd, { env: ctx.env });
|
|
298
|
+
if (ahead.status !== 0 || Number(ahead.stdout.trim()) === 0) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
return { repo: cwd, workBranch, baseSha: ctx.baseSha };
|
|
302
|
+
}
|
|
303
|
+
/** A bounded reason (or question) for the handoff record. */
|
|
304
|
+
function reasonText(text) {
|
|
305
|
+
return boundHandoffText(text, HANDOFF_LIMITS.reasonChars);
|
|
306
|
+
}
|
|
307
|
+
/** The `needs-attention` handover of a bounce's recorded input. */
|
|
308
|
+
function needsAttention(ctx, input, bundle) {
|
|
309
|
+
return {
|
|
310
|
+
record: {
|
|
311
|
+
schema: 1,
|
|
312
|
+
item: ctx.item,
|
|
313
|
+
intent: { kind: 'needs-attention' },
|
|
314
|
+
products: {
|
|
315
|
+
reason: reasonText(input.reason),
|
|
316
|
+
...(input.questions === undefined || input.questions.length === 0
|
|
317
|
+
? {}
|
|
318
|
+
: { questions: input.questions.map(reasonText) }),
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
bundle,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Map the build's FIRST WRITE (the recorded halt) to the handoff (task
|
|
326
|
+
* `ci-split-build-path-non-integrate-intents`). The seam call names the write;
|
|
327
|
+
* the pipeline's {@link BuildBoundary} annotation names the outcome when several
|
|
328
|
+
* share one seam call. A bounce's LOCAL half (the wip commit, which the seam
|
|
329
|
+
* would have made before pushing) runs here, so the wip travels in the bundle.
|
|
330
|
+
*/
|
|
331
|
+
function handOverHalt(intent, ctx) {
|
|
332
|
+
const call = `${intent.seam}.${intent.method}`;
|
|
333
|
+
const boundary = intent.annotation;
|
|
334
|
+
if (call === 'integrationLand.land') {
|
|
335
|
+
const land = intent.input;
|
|
336
|
+
if (land.branch !== workBranchRef('task', ctx.slug)) {
|
|
337
|
+
throw new Error(`the land half names the branch ${land.branch}, not ` +
|
|
338
|
+
workBranchRef('task', ctx.slug));
|
|
339
|
+
}
|
|
340
|
+
const limit = HANDOFF_LIMITS.commentChars;
|
|
341
|
+
return {
|
|
342
|
+
record: {
|
|
343
|
+
schema: 1,
|
|
344
|
+
item: ctx.item,
|
|
345
|
+
intent: { kind: 'integrate' },
|
|
346
|
+
products: {
|
|
347
|
+
prTitle: land.title,
|
|
348
|
+
prBody: boundHandoffText(land.body ?? '', limit),
|
|
349
|
+
...(land.reviewProse === undefined
|
|
350
|
+
? {}
|
|
351
|
+
: { reviewProse: boundHandoffText(land.reviewProse, limit) }),
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
bundle: { repo: land.cwd, workBranch: land.branch, baseSha: ctx.baseSha },
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
// The deadline checkpoint: its marker commit is already made (locally) when
|
|
358
|
+
// the branch save is recorded.
|
|
359
|
+
if (call === 'refWrite.saveWorkBranch' &&
|
|
360
|
+
boundary?.kind === 'deadline-checkpoint') {
|
|
361
|
+
const input = intent.input;
|
|
362
|
+
commitAbortedWork({ cwd: input.cwd, slug: ctx.slug, env: ctx.env });
|
|
363
|
+
const bundle = wipBundle(input.cwd, ctx);
|
|
364
|
+
if (!boundary.predecessorGone) {
|
|
365
|
+
const reason = deadlineSurfaceReason({
|
|
366
|
+
slug: ctx.slug,
|
|
367
|
+
kind: 'unreaped',
|
|
368
|
+
detail: boundary.reapDetail,
|
|
369
|
+
});
|
|
370
|
+
return needsAttention(ctx, { reason }, bundle);
|
|
371
|
+
}
|
|
372
|
+
if (bundle === undefined) {
|
|
373
|
+
// Nothing on the branch: no progress, so today's route surfaces.
|
|
374
|
+
const reason = deadlineSurfaceReason({
|
|
375
|
+
slug: ctx.slug,
|
|
376
|
+
kind: 'no-progress',
|
|
377
|
+
});
|
|
378
|
+
return needsAttention(ctx, { reason }, undefined);
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
record: {
|
|
382
|
+
schema: 1,
|
|
383
|
+
item: ctx.item,
|
|
384
|
+
intent: { kind: 'deadline-checkpoint' },
|
|
385
|
+
products: {},
|
|
386
|
+
},
|
|
387
|
+
bundle,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
// The empty-diff STOP surfaces a question on main without a branch save.
|
|
391
|
+
if (boundary?.kind === 'stop' && boundary.stopKind === 'empty-diff') {
|
|
392
|
+
return {
|
|
393
|
+
record: {
|
|
394
|
+
schema: 1,
|
|
395
|
+
item: ctx.item,
|
|
396
|
+
intent: { kind: 'stop' },
|
|
397
|
+
products: {
|
|
398
|
+
reason: reasonText(boundary.reason),
|
|
399
|
+
stopKind: 'empty-diff',
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
if (call === 'ledgerWrite.applyNeedsAttentionTransition') {
|
|
405
|
+
const input = intent.input;
|
|
406
|
+
commitAbortedWork({ cwd: input.cwd, slug: ctx.slug, env: ctx.env });
|
|
407
|
+
const bundle = input.pushBranch === false ? undefined : wipBundle(input.cwd, ctx);
|
|
408
|
+
if (boundary?.kind === 'agent-failed') {
|
|
409
|
+
return { record: agentFailed(ctx.item, boundary.failureDetail), bundle };
|
|
410
|
+
}
|
|
411
|
+
if (boundary?.kind === 'stop') {
|
|
412
|
+
return {
|
|
413
|
+
record: {
|
|
414
|
+
schema: 1,
|
|
415
|
+
item: ctx.item,
|
|
416
|
+
intent: { kind: 'stop' },
|
|
417
|
+
products: {
|
|
418
|
+
reason: reasonText(boundary.reason),
|
|
419
|
+
stopKind: boundary.stopKind,
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
bundle,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
return needsAttention(ctx, input, bundle);
|
|
426
|
+
}
|
|
427
|
+
// A tree-less surface (the continue rebase conflicted at onboarding): the
|
|
428
|
+
// kept branch is untouched on the arbiter, so nothing is bundled.
|
|
429
|
+
if (call === 'ledgerWrite.applyTreelessNeedsAttentionTransition') {
|
|
430
|
+
const input = intent.input;
|
|
431
|
+
return needsAttention(ctx, input, undefined);
|
|
432
|
+
}
|
|
433
|
+
return { record: agentFailed(ctx.item, haltDetail(intent)) };
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* The agent phase: after a read-only lock-ownership check, run the build
|
|
437
|
+
* pipeline (`performDo`, without its claim) under the phase recorder, halt at
|
|
438
|
+
* the first write, and write the handoff. Holds a read-only token: it never
|
|
439
|
+
* writes to the arbiter.
|
|
440
|
+
*/
|
|
441
|
+
export async function performBuildAgentPhase(options) {
|
|
442
|
+
const note = options.note ?? (() => { });
|
|
443
|
+
const arbiter = options.arbiter ?? DEFAULT_ARBITER;
|
|
444
|
+
const cwd = options.cwd;
|
|
445
|
+
const env = options.env ?? process.env;
|
|
446
|
+
const held = requireHeldLock({
|
|
447
|
+
lockOutputs: options.lockOutputs,
|
|
448
|
+
env,
|
|
449
|
+
rung: 'build-task',
|
|
450
|
+
});
|
|
451
|
+
if (options.handoffDir === undefined) {
|
|
452
|
+
throw new PhaseDriverError('the agent phase needs a handoff directory');
|
|
453
|
+
}
|
|
454
|
+
const slug = buildSlug(options, options.repoPath ?? cwd);
|
|
455
|
+
const item = `task:${slug}`;
|
|
456
|
+
const ownership = await checkLockOwnership({
|
|
457
|
+
cwd,
|
|
458
|
+
arbiter,
|
|
459
|
+
item,
|
|
460
|
+
lockSha: held.lockSha,
|
|
461
|
+
env,
|
|
462
|
+
});
|
|
463
|
+
if (!ownership.owned) {
|
|
464
|
+
return {
|
|
465
|
+
exitCode: 0,
|
|
466
|
+
outcome: 'stale-lock',
|
|
467
|
+
slug,
|
|
468
|
+
message: ownership.message,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const recorder = createPhaseRecorder({ record: buildRecordableCalls() });
|
|
472
|
+
const ctx = { item, slug, arbiter, baseSha: held.baseSha, env };
|
|
473
|
+
let handover;
|
|
474
|
+
try {
|
|
475
|
+
// EVERY review provider the pipeline can reach records (never only an
|
|
476
|
+
// injected one): without an injected instance, resolve the one the
|
|
477
|
+
// integration core would select from the arbiter URL, and wrap it.
|
|
478
|
+
const provider = options.providerInstance ??
|
|
479
|
+
selectProvider({ arbiterUrl: await arbiterUrl(cwd, arbiter, env) });
|
|
480
|
+
const outcome = await runAgentPhase(recorder, () => performDo({
|
|
481
|
+
...options,
|
|
482
|
+
phase: 'agent',
|
|
483
|
+
providerInstance: recordingReviewProvider(provider, recorder),
|
|
484
|
+
}));
|
|
485
|
+
handover = outcome.halted
|
|
486
|
+
? handOverHalt(outcome.intent, ctx)
|
|
487
|
+
: {
|
|
488
|
+
record: agentFailed(item, `the build ended without reaching a write: ${outcome.result.message}`),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
catch (err) {
|
|
492
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
493
|
+
handover = { record: agentFailed(item, `the build failed: ${message}`) };
|
|
494
|
+
}
|
|
495
|
+
const written = writeHandoff({
|
|
496
|
+
dir: options.handoffDir,
|
|
497
|
+
rung: 'build-task',
|
|
498
|
+
record: handover.record,
|
|
499
|
+
bundle: handover.bundle,
|
|
500
|
+
});
|
|
501
|
+
if (written.lfsMissing.length > 0) {
|
|
502
|
+
note(`the local LFS store lacks ${written.lfsMissing.length} object(s) a new ` +
|
|
503
|
+
`commit points at (${written.lfsMissing.slice(0, 5).join(', ')}); the ` +
|
|
504
|
+
'apply job will reject the handoff');
|
|
505
|
+
}
|
|
506
|
+
const kind = handover.record.intent.kind;
|
|
507
|
+
const message = `handed over ${kind} for ${item}`;
|
|
508
|
+
return {
|
|
509
|
+
exitCode: 0,
|
|
510
|
+
outcome: 'handed-over',
|
|
511
|
+
slug,
|
|
512
|
+
message,
|
|
513
|
+
intent: kind,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function agentFailed(item, detail) {
|
|
517
|
+
return {
|
|
518
|
+
schema: 1,
|
|
519
|
+
item,
|
|
520
|
+
intent: { kind: 'agent-failed' },
|
|
521
|
+
products: {
|
|
522
|
+
failureDetail: boundHandoffText(detail, HANDOFF_LIMITS.reasonChars),
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
// ---------------------------------------------------------------------------
|
|
527
|
+
// apply
|
|
528
|
+
// ---------------------------------------------------------------------------
|
|
529
|
+
/** Surface the item to needs-attention (tree-less) with `reason`. */
|
|
530
|
+
async function surface(params) {
|
|
531
|
+
const surfaced = await ledgerWrite.applyTreelessNeedsAttentionTransition({
|
|
532
|
+
cwd: params.cwd,
|
|
533
|
+
slug: params.slug,
|
|
534
|
+
reason: params.reason,
|
|
535
|
+
arbiter: params.arbiter,
|
|
536
|
+
env: params.env,
|
|
537
|
+
note: params.note,
|
|
538
|
+
});
|
|
539
|
+
const where = surfaced.moved
|
|
540
|
+
? 'surfaced it to needs-attention'
|
|
541
|
+
: `could not surface it (${surfaced.reasonNotMoved ?? 'unknown'})`;
|
|
542
|
+
// A clean surface is green ("A handled outcome is green",
|
|
543
|
+
// `ci-phase-driver.ts`); only a surface that did not land stays red.
|
|
544
|
+
return {
|
|
545
|
+
exitCode: surfaced.moved ? 0 : 1,
|
|
546
|
+
outcome: surfaced.moved ? params.outcome : 'surface-unmoved',
|
|
547
|
+
slug: params.slug,
|
|
548
|
+
message: `${params.reason}; ${where}.`,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* The apply phase: check the lock is still this run's, validate the handoff as
|
|
553
|
+
* hostile, then resume the build at the intent's write half. Runs no agent and
|
|
554
|
+
* no repository code; every write goes through dorfl's seams.
|
|
555
|
+
*/
|
|
556
|
+
export async function performBuildApplyPhase(options) {
|
|
557
|
+
const note = options.note ?? (() => { });
|
|
558
|
+
const arbiter = options.arbiter ?? DEFAULT_ARBITER;
|
|
559
|
+
const cwd = options.cwd;
|
|
560
|
+
const env = noSmudgeEnv(identityEnv(options.identity, options.env ?? process.env));
|
|
561
|
+
const held = requireHeldLock({
|
|
562
|
+
lockOutputs: options.lockOutputs,
|
|
563
|
+
env,
|
|
564
|
+
rung: 'build-task',
|
|
565
|
+
});
|
|
566
|
+
if (options.agentResult === undefined) {
|
|
567
|
+
throw new PhaseDriverError('the apply phase needs the agent job result (--agent-result, ' +
|
|
568
|
+
'needs.agent.result): only success reads the handoff');
|
|
569
|
+
}
|
|
570
|
+
const agentResult = options.agentResult;
|
|
571
|
+
const slug = buildSlug(options, options.repoPath ?? cwd);
|
|
572
|
+
const item = `task:${slug}`;
|
|
573
|
+
// Lock ownership BEFORE the first write (spec §8): a lock that was released,
|
|
574
|
+
// reaped or re-taken since the lock job ran means this run writes nothing.
|
|
575
|
+
// This is also what makes a "Re-run failed jobs" of a finished run safe: it
|
|
576
|
+
// replays the old lock outputs and artifact, but the first apply already
|
|
577
|
+
// released or surfaced the item, so the lock is gone and nothing is written:
|
|
578
|
+
// a handled outcome, so the re-run is green.
|
|
579
|
+
const ownership = await checkLockOwnership({
|
|
580
|
+
cwd,
|
|
581
|
+
arbiter,
|
|
582
|
+
item,
|
|
583
|
+
lockSha: held.lockSha,
|
|
584
|
+
env,
|
|
585
|
+
});
|
|
586
|
+
if (!ownership.owned) {
|
|
587
|
+
return {
|
|
588
|
+
exitCode: 0,
|
|
589
|
+
outcome: 'stale-lock',
|
|
590
|
+
slug,
|
|
591
|
+
message: ownership.message,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
// Every lock release from here on (the land's, the surface's, the
|
|
595
|
+
// return-to-backlog's) is leased on the sha this run owns: none of them may
|
|
596
|
+
// delete a lock another run took since the ownership check.
|
|
597
|
+
const restoreLease = leaseLockReleases(item, held.lockSha);
|
|
598
|
+
try {
|
|
599
|
+
const ctx = { options, held, slug, item, arbiter, cwd, env };
|
|
600
|
+
// The agent job's result (decision 5) BEFORE any artifact is read: only
|
|
601
|
+
// `success` reads it; the build path has no agent-less rung, so a
|
|
602
|
+
// `skipped` agent job is never the deterministic case here.
|
|
603
|
+
const decision = await resolveAgentResult({
|
|
604
|
+
result: agentResult,
|
|
605
|
+
held,
|
|
606
|
+
agentTimeoutMinutes: options.agentTimeoutMinutes,
|
|
607
|
+
api: options.actionsApi,
|
|
608
|
+
env,
|
|
609
|
+
});
|
|
610
|
+
switch (decision.action) {
|
|
611
|
+
case 'read-handoff':
|
|
612
|
+
return await applyOwned(ctx);
|
|
613
|
+
case 'deterministic':
|
|
614
|
+
return await surfaceAgentResult(ctx, 'the agent job was skipped (needsAgent: false), but the build ' +
|
|
615
|
+
'path has no rung that runs without an agent; the handoff was not read');
|
|
616
|
+
case 'surface':
|
|
617
|
+
return await surfaceAgentResult(ctx, decision.reason);
|
|
618
|
+
case 'release':
|
|
619
|
+
return await releaseAfterCancel(ctx, decision.reason);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
finally {
|
|
623
|
+
restoreLease();
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* The apply phase once the lock is known to be this run's: validate the
|
|
628
|
+
* handoff, publish a continued branch with its lease, then resume at the
|
|
629
|
+
* intent's write half.
|
|
630
|
+
*/
|
|
631
|
+
async function applyOwned(ctx) {
|
|
632
|
+
const { options, held, slug, item, arbiter, cwd, env } = ctx;
|
|
633
|
+
const note = options.note ?? (() => { });
|
|
634
|
+
let validated;
|
|
635
|
+
try {
|
|
636
|
+
if (options.handoffDir === undefined) {
|
|
637
|
+
throw new HandoffRejected('layout', 'no handoff directory was given');
|
|
638
|
+
}
|
|
639
|
+
validated = validateApplyHandoff({
|
|
640
|
+
dir: options.handoffDir,
|
|
641
|
+
runnerTemp: runnerTempFrom(options.runnerTemp, env),
|
|
642
|
+
repo: cwd,
|
|
643
|
+
trust: {
|
|
644
|
+
item,
|
|
645
|
+
rung: 'build-task',
|
|
646
|
+
baseSha: held.baseSha,
|
|
647
|
+
arbiter,
|
|
648
|
+
integrationMode: options.integration ?? 'propose',
|
|
649
|
+
},
|
|
650
|
+
env,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
catch (err) {
|
|
654
|
+
if (!(err instanceof HandoffRejected))
|
|
655
|
+
throw err;
|
|
656
|
+
note(err.message);
|
|
657
|
+
return surface({
|
|
658
|
+
cwd,
|
|
659
|
+
arbiter,
|
|
660
|
+
slug,
|
|
661
|
+
reason: err.message,
|
|
662
|
+
env,
|
|
663
|
+
note,
|
|
664
|
+
outcome: 'rejected',
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
const record = validated.handoff.record;
|
|
668
|
+
const bundle = validated.bundle;
|
|
669
|
+
// The LFS objects go FIRST (decision 6), before any ref of any intent (the
|
|
670
|
+
// continue push, the land's pushes, a WIP branch save), so a ref never lands
|
|
671
|
+
// pointing at a missing object.
|
|
672
|
+
const lfs = await pushHandoffLfs({
|
|
673
|
+
cwd,
|
|
674
|
+
arbiter,
|
|
675
|
+
objects: validated.lfsObjects,
|
|
676
|
+
env,
|
|
677
|
+
note,
|
|
678
|
+
});
|
|
679
|
+
if (!lfs.ok) {
|
|
680
|
+
return surface({
|
|
681
|
+
cwd,
|
|
682
|
+
arbiter,
|
|
683
|
+
slug,
|
|
684
|
+
reason: lfs.reason,
|
|
685
|
+
env,
|
|
686
|
+
note,
|
|
687
|
+
outcome: lfs.rejected ? 'rejected' : 'surfaced',
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
if (bundle !== undefined) {
|
|
691
|
+
await gitHard(['checkout', '--quiet', '-B', bundle.workBranch, bundle.tip], cwd, env);
|
|
692
|
+
// The CONTINUE push (decision 7): the agent job rebased the kept branch
|
|
693
|
+
// locally; publish it with a lease on the tip the LOCK job observed, before
|
|
694
|
+
// any other write. A branch that moved since then refuses the push, and
|
|
695
|
+
// this run writes nothing at all.
|
|
696
|
+
if (held.continueTip !== undefined) {
|
|
697
|
+
const pushed = await refWrite.pushLeasedWorkBranch({
|
|
698
|
+
arbiter,
|
|
699
|
+
branch: bundle.workBranch,
|
|
700
|
+
commit: bundle.tip,
|
|
701
|
+
expectedTip: held.continueTip,
|
|
702
|
+
cwd,
|
|
703
|
+
env,
|
|
704
|
+
});
|
|
705
|
+
if (pushed.status !== 0) {
|
|
706
|
+
const message = `${bundle.workBranch} on ${arbiter} moved since the lock job saw it ` +
|
|
707
|
+
`at ${held.continueTip}, so the leased push of the continued branch ` +
|
|
708
|
+
`was refused (${pushed.stderr.trim()}). Nothing was written and the ` +
|
|
709
|
+
`lock of ${item} is still held: inspect the branch, then \`dorfl ` +
|
|
710
|
+
`requeue ${slug}\` to retry the item.`;
|
|
711
|
+
return { exitCode: 1, outcome: 'stale-lease', slug, message };
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
const pushBranch = bundle !== undefined;
|
|
716
|
+
const branch = workBranchRef('task', slug);
|
|
717
|
+
switch (record.intent.kind) {
|
|
718
|
+
case 'integrate':
|
|
719
|
+
return applyIntegrate(ctx, validated);
|
|
720
|
+
case 'needs-attention': {
|
|
721
|
+
const { reason, questions } = record.products;
|
|
722
|
+
const routed = pushBranch
|
|
723
|
+
? await ledgerWrite.applyNeedsAttentionTransition({
|
|
724
|
+
cwd,
|
|
725
|
+
slug,
|
|
726
|
+
reason,
|
|
727
|
+
questions,
|
|
728
|
+
arbiter,
|
|
729
|
+
env,
|
|
730
|
+
note,
|
|
731
|
+
})
|
|
732
|
+
: await ledgerWrite.applyTreelessNeedsAttentionTransition({
|
|
733
|
+
cwd,
|
|
734
|
+
slug,
|
|
735
|
+
reason,
|
|
736
|
+
questions,
|
|
737
|
+
arbiter,
|
|
738
|
+
env,
|
|
739
|
+
note,
|
|
740
|
+
});
|
|
741
|
+
const message = routed.moved
|
|
742
|
+
? `Surfaced '${slug}' to needs-attention: ${reason}`
|
|
743
|
+
: `Could not surface '${slug}' (${routed.reasonNotMoved ?? 'unknown'}): ${reason}`;
|
|
744
|
+
return {
|
|
745
|
+
exitCode: routed.moved ? 0 : 1,
|
|
746
|
+
outcome: routed.moved ? 'surfaced' : 'surface-unmoved',
|
|
747
|
+
slug,
|
|
748
|
+
message,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
case 'stop': {
|
|
752
|
+
const stop = record.products;
|
|
753
|
+
return fromDoResult(await saveAgentStop({
|
|
754
|
+
slug,
|
|
755
|
+
branch,
|
|
756
|
+
cwd,
|
|
757
|
+
arbiter,
|
|
758
|
+
reason: stop.reason,
|
|
759
|
+
kind: stop.stopKind,
|
|
760
|
+
pushBranch,
|
|
761
|
+
env,
|
|
762
|
+
note,
|
|
763
|
+
}));
|
|
764
|
+
}
|
|
765
|
+
case 'agent-failed':
|
|
766
|
+
return fromDoResult(await saveAgentFailure({
|
|
767
|
+
slug,
|
|
768
|
+
branch,
|
|
769
|
+
cwd,
|
|
770
|
+
arbiter,
|
|
771
|
+
detail: record.products.failureDetail,
|
|
772
|
+
pushBranch,
|
|
773
|
+
env,
|
|
774
|
+
note,
|
|
775
|
+
}));
|
|
776
|
+
case 'deadline-checkpoint':
|
|
777
|
+
return fromDoResult(await routeDeadlineCheckpoint({
|
|
778
|
+
slug,
|
|
779
|
+
branch,
|
|
780
|
+
cwd,
|
|
781
|
+
arbiter,
|
|
782
|
+
maxAutoCheckpoints: maxAutoCheckpointsAt(cwd, held.baseSha, options.maxAutoCheckpoints, env),
|
|
783
|
+
env,
|
|
784
|
+
note,
|
|
785
|
+
}));
|
|
786
|
+
default:
|
|
787
|
+
return surface({
|
|
788
|
+
cwd,
|
|
789
|
+
arbiter,
|
|
790
|
+
slug,
|
|
791
|
+
reason: `the agent job handed over ${record.intent.kind}, which the build ` +
|
|
792
|
+
'path does not carry',
|
|
793
|
+
env,
|
|
794
|
+
note,
|
|
795
|
+
outcome: 'surfaced',
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Surface the item to needs-attention because the agent job did not succeed
|
|
801
|
+
* (decision 5): tree-less (nothing from the agent job is pushed: its artifact is
|
|
802
|
+
* not read), the lock released leased on `lockSha`. A kept work branch stays as
|
|
803
|
+
* it is on the arbiter.
|
|
804
|
+
*/
|
|
805
|
+
async function surfaceAgentResult(ctx, reason) {
|
|
806
|
+
const { options, slug, arbiter, cwd, env } = ctx;
|
|
807
|
+
const note = options.note ?? (() => { });
|
|
808
|
+
const routed = await ledgerWrite.applyTreelessNeedsAttentionTransition({
|
|
809
|
+
cwd,
|
|
810
|
+
slug,
|
|
811
|
+
reason,
|
|
812
|
+
arbiter,
|
|
813
|
+
env,
|
|
814
|
+
note,
|
|
815
|
+
});
|
|
816
|
+
const message = routed.moved
|
|
817
|
+
? `Surfaced '${slug}' to needs-attention: ${reason}`
|
|
818
|
+
: `Could not surface '${slug}' (${routed.reasonNotMoved ?? 'unknown'}): ${reason}`;
|
|
819
|
+
return {
|
|
820
|
+
exitCode: routed.moved ? 0 : 1,
|
|
821
|
+
outcome: routed.moved ? 'surfaced' : 'surface-unmoved',
|
|
822
|
+
slug,
|
|
823
|
+
message,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* The agent job was cancelled before its timeout (decision 5): only release the
|
|
828
|
+
* lock, leased on `lockSha`, so the next run picks the item up again.
|
|
829
|
+
*/
|
|
830
|
+
async function releaseAfterCancel(ctx, reason) {
|
|
831
|
+
const { options, held, slug, item, arbiter, cwd, env } = ctx;
|
|
832
|
+
const note = options.note ?? (() => { });
|
|
833
|
+
const released = await releaseLockLeased({
|
|
834
|
+
cwd,
|
|
835
|
+
arbiter,
|
|
836
|
+
item,
|
|
837
|
+
expectedSha: held.lockSha,
|
|
838
|
+
env,
|
|
839
|
+
});
|
|
840
|
+
note(released.message);
|
|
841
|
+
const message = released.released
|
|
842
|
+
? `Released '${slug}': ${reason}.`
|
|
843
|
+
: `Could not release '${slug}' (${released.message}): ${reason}.`;
|
|
844
|
+
return {
|
|
845
|
+
exitCode: released.released ? 0 : 1,
|
|
846
|
+
outcome: released.released ? 'released' : 'release-refused',
|
|
847
|
+
slug,
|
|
848
|
+
message,
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
/** Map a reused `do` write half's result to the apply phase's. */
|
|
852
|
+
function fromDoResult(r) {
|
|
853
|
+
const outcome = r.outcome === 'deadline-auto-continued'
|
|
854
|
+
? 'auto-continued'
|
|
855
|
+
: r.routedToNeedsAttention === true
|
|
856
|
+
? 'surfaced'
|
|
857
|
+
: 'surface-unmoved';
|
|
858
|
+
return { exitCode: r.exitCode, outcome, slug: r.slug, message: r.message };
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* The `maxAutoCheckpoints` ceiling from the repository config AT `baseSha`
|
|
862
|
+
* (trusted, the arbiter's `main` the lock job classified at), never from the
|
|
863
|
+
* bundle: a work branch could raise its own ceiling otherwise. Falls back to
|
|
864
|
+
* the resolved option, then to the default, when the file does not set a
|
|
865
|
+
* valid value.
|
|
866
|
+
*/
|
|
867
|
+
function maxAutoCheckpointsAt(cwd, baseSha, fallback, env) {
|
|
868
|
+
const cap = repoConfigAt(cwd, baseSha, env)?.maxAutoCheckpoints;
|
|
869
|
+
if (typeof cap === 'number' && Number.isInteger(cap) && cap >= 1) {
|
|
870
|
+
return cap;
|
|
871
|
+
}
|
|
872
|
+
return fallback ?? 5;
|
|
873
|
+
}
|
|
874
|
+
/** The `integrate` write half: the land loop, the review comment, the release. */
|
|
875
|
+
async function applyIntegrate(ctx, validated) {
|
|
876
|
+
const { options, held, slug, item, arbiter, cwd, env } = ctx;
|
|
877
|
+
const note = options.note ?? (() => { });
|
|
878
|
+
const record = validated.handoff.record;
|
|
879
|
+
if (record.intent.kind !== 'integrate' || validated.bundle === undefined) {
|
|
880
|
+
throw new Error('applyIntegrate needs an integrate handoff with a bundle');
|
|
881
|
+
}
|
|
882
|
+
const products = record.products;
|
|
883
|
+
const branch = validated.bundle.workBranch;
|
|
884
|
+
const tip = validated.bundle.tip;
|
|
885
|
+
if (validated.forcedPropose) {
|
|
886
|
+
note(`Untrusted-origin task '${slug}': forcing the BUILD transition to ` +
|
|
887
|
+
'propose (recomputed from the task at the base).');
|
|
888
|
+
}
|
|
889
|
+
const commitMessage = (await gitHard(['log', '-1', '--format=%s', tip], cwd, env)).trim();
|
|
890
|
+
const land = await integrationLand.land({
|
|
891
|
+
cwd,
|
|
892
|
+
arbiter,
|
|
893
|
+
slug,
|
|
894
|
+
branch,
|
|
895
|
+
lifecycle: false,
|
|
896
|
+
source: taskSourceAtBase(cwd, held.baseSha, slug, env),
|
|
897
|
+
surfaceArbiter: arbiter,
|
|
898
|
+
commitMessage,
|
|
899
|
+
env,
|
|
900
|
+
note,
|
|
901
|
+
mode: validated.mode,
|
|
902
|
+
noPR: options.noPR,
|
|
903
|
+
providerInstance: options.providerInstance,
|
|
904
|
+
title: products.prTitle,
|
|
905
|
+
body: proposeBody(products.prBody, validated.ledgerReport),
|
|
906
|
+
reviewProse: products.reviewProse,
|
|
907
|
+
mergeRetries: options.mergeRetries,
|
|
908
|
+
mergeJitterMs: options.mergeJitterMs,
|
|
909
|
+
// The landed-vs-gated report (decision 2): the agent job gated the
|
|
910
|
+
// bundle tip, so a merge land that lands another tree says so.
|
|
911
|
+
gatedTip: tip,
|
|
912
|
+
});
|
|
913
|
+
if (land.outcome !== 'completed') {
|
|
914
|
+
return {
|
|
915
|
+
exitCode: land.routedToNeedsAttention ? 0 : 1,
|
|
916
|
+
outcome: land.routedToNeedsAttention ? 'surfaced' : 'usage-error',
|
|
917
|
+
slug,
|
|
918
|
+
message: land.reason ?? `the land ended as ${land.outcome}`,
|
|
919
|
+
land,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
const integration = land.integration;
|
|
923
|
+
if (integration.mergedToMain === true || integration.alreadyLanded === true) {
|
|
924
|
+
const released = await releaseLockLeased({
|
|
925
|
+
cwd,
|
|
926
|
+
arbiter,
|
|
927
|
+
item,
|
|
928
|
+
expectedSha: held.lockSha,
|
|
929
|
+
env,
|
|
930
|
+
});
|
|
931
|
+
note(released.message);
|
|
932
|
+
return {
|
|
933
|
+
exitCode: 0,
|
|
934
|
+
outcome: 'landed',
|
|
935
|
+
slug,
|
|
936
|
+
message: `Completed '${slug}': merged to ${arbiter}/main.`,
|
|
937
|
+
land,
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
// The lock stays held until the PR merges; stamp it as kept for a propose
|
|
941
|
+
// PR (task `wire-merge-questions-into-the-advance-tick`, decision 4), leased
|
|
942
|
+
// on this run's `lockSha`, so the merge-question surfacer and an answered
|
|
943
|
+
// `merge` can tell it from a live build. Best-effort: an unmarked kept lock
|
|
944
|
+
// only keeps the branch out of the merge-question loop.
|
|
945
|
+
const marked = await markLockKeptForProposePr({
|
|
946
|
+
item,
|
|
947
|
+
cwd,
|
|
948
|
+
arbiter,
|
|
949
|
+
expectedSha: held.lockSha,
|
|
950
|
+
env,
|
|
951
|
+
});
|
|
952
|
+
if (marked.outcome !== 'transitioned')
|
|
953
|
+
note(marked.message);
|
|
954
|
+
// Say "for review" ONLY when a PR was opened (task
|
|
955
|
+
// `intake-reports-the-pr-it-actually-opened`): a degraded `gh pr create`
|
|
956
|
+
// pushed the branch but opened nothing, and the land already printed why.
|
|
957
|
+
const notOpened = proposeRequestNotOpened(integration);
|
|
958
|
+
return {
|
|
959
|
+
exitCode: 0,
|
|
960
|
+
outcome: 'proposed',
|
|
961
|
+
slug,
|
|
962
|
+
message: notOpened === undefined
|
|
963
|
+
? `Completed '${slug}': pushed ${branch} for review; the lock stays ` +
|
|
964
|
+
'held until the PR merges.'
|
|
965
|
+
: `Completed '${slug}': pushed ${branch} but opened NO PR ` +
|
|
966
|
+
`(${notOpened}); the lock stays held until the branch lands.`,
|
|
967
|
+
land,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* The propose PR body: the agent's summary (from the validated record) with the
|
|
972
|
+
* bundle validation's ledger report appended (user story 23). The report is
|
|
973
|
+
* never cut; the summary is shortened to keep the whole under the comment limit.
|
|
974
|
+
*/
|
|
975
|
+
function proposeBody(prBody, ledgerReport) {
|
|
976
|
+
if (ledgerReport === undefined)
|
|
977
|
+
return prBody === '' ? undefined : prBody;
|
|
978
|
+
const room = HANDOFF_LIMITS.commentChars - ledgerReport.length - 2;
|
|
979
|
+
const summary = boundHandoffText(prBody, Math.max(0, room));
|
|
980
|
+
return summary === '' ? ledgerReport : `${summary}\n\n${ledgerReport}`;
|
|
981
|
+
}
|
|
982
|
+
async function gitHard(args, cwd, env) {
|
|
983
|
+
const r = await runAsync('git', args, cwd, { env });
|
|
984
|
+
if (r.status !== 0) {
|
|
985
|
+
throw new Error(`git ${args.join(' ')} failed (exit ${r.status}): ${r.stderr.trim()}`);
|
|
986
|
+
}
|
|
987
|
+
return r.stdout;
|
|
988
|
+
}
|
|
989
|
+
//# sourceMappingURL=ci-phase-build.js.map
|