dorfl 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advance-ci-template.d.ts +17 -16
- package/dist/advance-ci-template.d.ts.map +1 -1
- package/dist/advance-ci-template.js +42 -53
- package/dist/advance-ci-template.js.map +1 -1
- package/dist/advance-drivers.d.ts +8 -0
- package/dist/advance-drivers.d.ts.map +1 -1
- package/dist/advance-drivers.js +28 -4
- package/dist/advance-drivers.js.map +1 -1
- package/dist/advance-isolated.d.ts.map +1 -1
- package/dist/advance-isolated.js +3 -2
- package/dist/advance-isolated.js.map +1 -1
- package/dist/advance-lifecycle-template.d.ts +41 -40
- package/dist/advance-lifecycle-template.d.ts.map +1 -1
- package/dist/advance-lifecycle-template.js +309 -489
- package/dist/advance-lifecycle-template.js.map +1 -1
- package/dist/advance-loop-driver.d.ts.map +1 -1
- package/dist/advance-loop-driver.js +3 -2
- package/dist/advance-loop-driver.js.map +1 -1
- package/dist/advance-treeless-publish.d.ts +31 -0
- package/dist/advance-treeless-publish.d.ts.map +1 -1
- package/dist/advance-treeless-publish.js +40 -0
- package/dist/advance-treeless-publish.js.map +1 -1
- package/dist/advance.d.ts +63 -3
- package/dist/advance.d.ts.map +1 -1
- package/dist/advance.js +80 -12
- package/dist/advance.js.map +1 -1
- package/dist/advancing-lock.d.ts +9 -0
- package/dist/advancing-lock.d.ts.map +1 -1
- package/dist/advancing-lock.js +21 -1
- package/dist/advancing-lock.js.map +1 -1
- package/dist/agent-env.d.ts +61 -0
- package/dist/agent-env.d.ts.map +1 -0
- package/dist/agent-env.js +144 -0
- package/dist/agent-env.js.map +1 -0
- package/dist/agent-stop.d.ts +35 -0
- package/dist/agent-stop.d.ts.map +1 -1
- package/dist/agent-stop.js +62 -0
- package/dist/agent-stop.js.map +1 -1
- package/dist/apply-merge-action.d.ts +76 -34
- package/dist/apply-merge-action.d.ts.map +1 -1
- package/dist/apply-merge-action.js +339 -129
- package/dist/apply-merge-action.js.map +1 -1
- package/dist/apply-stuck-action.js +2 -2
- package/dist/apply-stuck-action.js.map +1 -1
- package/dist/ci-agent-result.d.ts +151 -0
- package/dist/ci-agent-result.d.ts.map +1 -0
- package/dist/ci-agent-result.js +314 -0
- package/dist/ci-agent-result.js.map +1 -0
- package/dist/ci-handoff-apply.d.ts +162 -0
- package/dist/ci-handoff-apply.d.ts.map +1 -0
- package/dist/ci-handoff-apply.js +497 -0
- package/dist/ci-handoff-apply.js.map +1 -0
- package/dist/ci-handoff-format.d.ts +369 -0
- package/dist/ci-handoff-format.d.ts.map +1 -0
- package/dist/ci-handoff-format.js +646 -0
- package/dist/ci-handoff-format.js.map +1 -0
- package/dist/ci-handoff-lfs.d.ts +146 -0
- package/dist/ci-handoff-lfs.d.ts.map +1 -0
- package/dist/ci-handoff-lfs.js +397 -0
- package/dist/ci-handoff-lfs.js.map +1 -0
- package/dist/ci-handoff.d.ts +99 -0
- package/dist/ci-handoff.d.ts.map +1 -0
- package/dist/ci-handoff.js +277 -0
- package/dist/ci-handoff.js.map +1 -0
- package/dist/ci-lock-outputs.d.ts +93 -0
- package/dist/ci-lock-outputs.d.ts.map +1 -0
- package/dist/ci-lock-outputs.js +223 -0
- package/dist/ci-lock-outputs.js.map +1 -0
- package/dist/ci-phase-build.d.ts +178 -0
- package/dist/ci-phase-build.d.ts.map +1 -0
- package/dist/ci-phase-build.js +989 -0
- package/dist/ci-phase-build.js.map +1 -0
- package/dist/ci-phase-driver.d.ts +247 -0
- package/dist/ci-phase-driver.d.ts.map +1 -0
- package/dist/ci-phase-driver.js +370 -0
- package/dist/ci-phase-driver.js.map +1 -0
- package/dist/ci-phase-intake.d.ts +172 -0
- package/dist/ci-phase-intake.d.ts.map +1 -0
- package/dist/ci-phase-intake.js +555 -0
- package/dist/ci-phase-intake.js.map +1 -0
- package/dist/ci-phase-tasking.d.ts +205 -0
- package/dist/ci-phase-tasking.d.ts.map +1 -0
- package/dist/ci-phase-tasking.js +906 -0
- package/dist/ci-phase-tasking.js.map +1 -0
- package/dist/ci-phase-treeless.d.ts +224 -0
- package/dist/ci-phase-treeless.d.ts.map +1 -0
- package/dist/ci-phase-treeless.js +1482 -0
- package/dist/ci-phase-treeless.js.map +1 -0
- package/dist/ci-read-token.d.ts +47 -0
- package/dist/ci-read-token.d.ts.map +1 -0
- package/dist/ci-read-token.js +80 -0
- package/dist/ci-read-token.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +447 -22
- package/dist/cli.js.map +1 -1
- package/dist/close-job-template.d.ts +10 -1
- package/dist/close-job-template.d.ts.map +1 -1
- package/dist/close-job-template.js +46 -14
- package/dist/close-job-template.js.map +1 -1
- package/dist/close-job.d.ts +20 -5
- package/dist/close-job.d.ts.map +1 -1
- package/dist/close-job.js +65 -12
- package/dist/close-job.js.map +1 -1
- package/dist/complete.d.ts.map +1 -1
- package/dist/complete.js +20 -1
- package/dist/complete.js.map +1 -1
- package/dist/config.d.ts +41 -44
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -4
- package/dist/config.js.map +1 -1
- package/dist/continue-branch.d.ts +63 -15
- package/dist/continue-branch.d.ts.map +1 -1
- package/dist/continue-branch.js +75 -8
- package/dist/continue-branch.js.map +1 -1
- package/dist/crashed-run-save.d.ts +73 -0
- package/dist/crashed-run-save.d.ts.map +1 -0
- package/dist/crashed-run-save.js +86 -0
- package/dist/crashed-run-save.js.map +1 -0
- package/dist/cwd-section.d.ts.map +1 -1
- package/dist/cwd-section.js +10 -2
- package/dist/cwd-section.js.map +1 -1
- package/dist/decision-engine.d.ts +10 -0
- package/dist/decision-engine.d.ts.map +1 -1
- package/dist/decision-engine.js.map +1 -1
- package/dist/do-config.d.ts +2 -24
- package/dist/do-config.d.ts.map +1 -1
- package/dist/do-config.js +1 -33
- package/dist/do-config.js.map +1 -1
- package/dist/do.d.ts +149 -1
- package/dist/do.d.ts.map +1 -1
- package/dist/do.js +115 -25
- package/dist/do.js.map +1 -1
- package/dist/dorfl-item-template.d.ts +98 -0
- package/dist/dorfl-item-template.d.ts.map +1 -0
- package/dist/dorfl-item-template.js +504 -0
- package/dist/dorfl-item-template.js.map +1 -0
- package/dist/env-config.d.ts.map +1 -1
- package/dist/env-config.js +7 -5
- package/dist/env-config.js.map +1 -1
- package/dist/frontmatter.d.ts +52 -0
- package/dist/frontmatter.d.ts.map +1 -1
- package/dist/frontmatter.js +136 -2
- package/dist/frontmatter.js.map +1 -1
- package/dist/gc.d.ts +13 -0
- package/dist/gc.d.ts.map +1 -1
- package/dist/gc.js +16 -2
- package/dist/gc.js.map +1 -1
- package/dist/git.d.ts +18 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +70 -5
- package/dist/git.js.map +1 -1
- package/dist/harness.d.ts +44 -5
- package/dist/harness.d.ts.map +1 -1
- package/dist/harness.js +24 -1
- package/dist/harness.js.map +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/install-ci-action-pins.d.ts +123 -0
- package/dist/install-ci-action-pins.d.ts.map +1 -0
- package/dist/install-ci-action-pins.js +190 -0
- package/dist/install-ci-action-pins.js.map +1 -0
- package/dist/install-ci-actions-pr-creation.d.ts +70 -0
- package/dist/install-ci-actions-pr-creation.d.ts.map +1 -0
- package/dist/install-ci-actions-pr-creation.js +106 -0
- package/dist/install-ci-actions-pr-creation.js.map +1 -0
- package/dist/install-ci-capabilities/dorfl-item.d.ts +12 -0
- package/dist/install-ci-capabilities/dorfl-item.d.ts.map +1 -0
- package/dist/install-ci-capabilities/dorfl-item.js +26 -0
- package/dist/install-ci-capabilities/dorfl-item.js.map +1 -0
- package/dist/install-ci-core.d.ts +155 -37
- package/dist/install-ci-core.d.ts.map +1 -1
- package/dist/install-ci-core.js +304 -200
- package/dist/install-ci-core.js.map +1 -1
- package/dist/install-ci-github.d.ts +47 -2
- package/dist/install-ci-github.d.ts.map +1 -1
- package/dist/install-ci-github.js +113 -1
- package/dist/install-ci-github.js.map +1 -1
- package/dist/install-ci.d.ts +25 -6
- package/dist/install-ci.d.ts.map +1 -1
- package/dist/install-ci.js +72 -36
- package/dist/install-ci.js.map +1 -1
- package/dist/intake-marker.d.ts +6 -1
- package/dist/intake-marker.d.ts.map +1 -1
- package/dist/intake-marker.js.map +1 -1
- package/dist/intake-triage.d.ts.map +1 -1
- package/dist/intake-triage.js +6 -2
- package/dist/intake-triage.js.map +1 -1
- package/dist/intake-trigger-template.d.ts +31 -25
- package/dist/intake-trigger-template.d.ts.map +1 -1
- package/dist/intake-trigger-template.js +107 -292
- package/dist/intake-trigger-template.js.map +1 -1
- package/dist/intake.d.ts +186 -4
- package/dist/intake.d.ts.map +1 -1
- package/dist/intake.js +533 -416
- package/dist/intake.js.map +1 -1
- package/dist/integration-core.d.ts +154 -1
- package/dist/integration-core.d.ts.map +1 -1
- package/dist/integration-core.js +759 -486
- package/dist/integration-core.js.map +1 -1
- package/dist/integrator.d.ts +12 -0
- package/dist/integrator.d.ts.map +1 -1
- package/dist/integrator.js +21 -0
- package/dist/integrator.js.map +1 -1
- package/dist/isolation.d.ts +12 -2
- package/dist/isolation.d.ts.map +1 -1
- package/dist/isolation.js +19 -8
- package/dist/isolation.js.map +1 -1
- package/dist/item-lock.d.ts +93 -11
- package/dist/item-lock.d.ts.map +1 -1
- package/dist/item-lock.js +233 -65
- package/dist/item-lock.js.map +1 -1
- package/dist/land-question-residue.d.ts +58 -0
- package/dist/land-question-residue.d.ts.map +1 -0
- package/dist/land-question-residue.js +95 -0
- package/dist/land-question-residue.js.map +1 -0
- package/dist/lifecycle-gather.d.ts +16 -0
- package/dist/lifecycle-gather.d.ts.map +1 -1
- package/dist/lifecycle-gather.js +54 -6
- package/dist/lifecycle-gather.js.map +1 -1
- package/dist/merge-answer.d.ts +41 -0
- package/dist/merge-answer.d.ts.map +1 -0
- package/dist/merge-answer.js +60 -0
- package/dist/merge-answer.js.map +1 -0
- package/dist/merge-question-surfacer.d.ts +105 -30
- package/dist/merge-question-surfacer.d.ts.map +1 -1
- package/dist/merge-question-surfacer.js +202 -91
- package/dist/merge-question-surfacer.js.map +1 -1
- package/dist/merge-question-tick.d.ts +64 -0
- package/dist/merge-question-tick.d.ts.map +1 -0
- package/dist/merge-question-tick.js +96 -0
- package/dist/merge-question-tick.js.map +1 -0
- package/dist/mint-adr.d.ts.map +1 -1
- package/dist/mint-adr.js +20 -2
- package/dist/mint-adr.js.map +1 -1
- package/dist/needs-attention.d.ts +72 -2
- package/dist/needs-attention.d.ts.map +1 -1
- package/dist/needs-attention.js +275 -38
- package/dist/needs-attention.js.map +1 -1
- package/dist/phase-recorder.d.ts +168 -0
- package/dist/phase-recorder.d.ts.map +1 -0
- package/dist/phase-recorder.js +218 -0
- package/dist/phase-recorder.js.map +1 -0
- package/dist/phase.d.ts +94 -0
- package/dist/phase.d.ts.map +1 -0
- package/dist/phase.js +148 -0
- package/dist/phase.js.map +1 -0
- package/dist/pi-harness.d.ts.map +1 -1
- package/dist/pi-harness.js +19 -6
- package/dist/pi-harness.js.map +1 -1
- package/dist/prepare.d.ts.map +1 -1
- package/dist/prepare.js +4 -0
- package/dist/prepare.js.map +1 -1
- package/dist/prompt.d.ts +4 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +48 -1
- package/dist/prompt.js.map +1 -1
- package/dist/protocol/CLAIM-PROTOCOL.md +3 -1
- package/dist/protocol/WORK-CONTRACT.md +1 -1
- package/dist/reap-branches.d.ts +48 -0
- package/dist/reap-branches.d.ts.map +1 -1
- package/dist/reap-branches.js +64 -0
- package/dist/reap-branches.js.map +1 -1
- package/dist/ref-write.d.ts +198 -0
- package/dist/ref-write.d.ts.map +1 -0
- package/dist/ref-write.js +79 -0
- package/dist/ref-write.js.map +1 -0
- package/dist/repo-config.js +8 -8
- package/dist/repo-config.js.map +1 -1
- package/dist/repo-mirror.d.ts +36 -0
- package/dist/repo-mirror.d.ts.map +1 -1
- package/dist/repo-mirror.js +91 -1
- package/dist/repo-mirror.js.map +1 -1
- package/dist/review-verdict.d.ts +1 -1
- package/dist/review-verdict.js +1 -1
- package/dist/run.d.ts +7 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +28 -4
- package/dist/run.js.map +1 -1
- package/dist/scan.d.ts +19 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +21 -8
- package/dist/scan.js.map +1 -1
- package/dist/sidecar.d.ts +14 -0
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/sidecar.js +21 -0
- package/dist/sidecar.js.map +1 -1
- package/dist/single-job-warning.d.ts +63 -0
- package/dist/single-job-warning.d.ts.map +1 -0
- package/dist/single-job-warning.js +110 -0
- package/dist/single-job-warning.js.map +1 -0
- package/dist/skills/setup/protocol/CLAIM-PROTOCOL.md +3 -1
- package/dist/skills/setup/protocol/WORK-CONTRACT.md +1 -1
- package/dist/spec-complete.d.ts +13 -6
- package/dist/spec-complete.d.ts.map +1 -1
- package/dist/spec-complete.js +23 -11
- package/dist/spec-complete.js.map +1 -1
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +47 -14
- package/dist/start.js.map +1 -1
- package/dist/status.d.ts +12 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +11 -3
- package/dist/status.js.map +1 -1
- package/dist/tasking.d.ts +246 -1
- package/dist/tasking.d.ts.map +1 -1
- package/dist/tasking.js +452 -358
- package/dist/tasking.js.map +1 -1
- package/dist/triage-persist.d.ts.map +1 -1
- package/dist/triage-persist.js +21 -2
- package/dist/triage-persist.js.map +1 -1
- package/dist/verify-workflow-template.d.ts.map +1 -1
- package/dist/verify-workflow-template.js +8 -4
- package/dist/verify-workflow-template.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +4 -0
- package/dist/verify.js.map +1 -1
- package/dist/watch-session.d.ts +42 -13
- package/dist/watch-session.d.ts.map +1 -1
- package/dist/watch-session.js +52 -23
- package/dist/watch-session.js.map +1 -1
- package/dist/work-layout.d.ts +10 -4
- package/dist/work-layout.d.ts.map +1 -1
- package/dist/work-layout.js +11 -3
- package/dist/work-layout.js.map +1 -1
- package/dist/workspace.d.ts +65 -4
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +159 -11
- package/dist/workspace.js.map +1 -1
- package/package.json +1 -1
- package/src/advance-ci-template.ts +80 -81
- package/src/advance-drivers.ts +45 -6
- package/src/advance-isolated.ts +3 -5
- package/src/advance-lifecycle-template.ts +375 -538
- package/src/advance-loop-driver.ts +3 -5
- package/src/advance-treeless-publish.ts +61 -0
- package/src/advance.ts +145 -15
- package/src/advancing-lock.ts +36 -1
- package/src/agent-env.ts +174 -0
- package/src/agent-stop.ts +81 -0
- package/src/apply-merge-action.ts +401 -154
- package/src/apply-stuck-action.ts +2 -2
- package/src/ci-agent-result.ts +421 -0
- package/src/ci-handoff-apply.ts +737 -0
- package/src/ci-handoff-format.ts +926 -0
- package/src/ci-handoff-lfs.ts +519 -0
- package/src/ci-handoff.ts +392 -0
- package/src/ci-lock-outputs.ts +273 -0
- package/src/ci-phase-build.ts +1328 -0
- package/src/ci-phase-driver.ts +548 -0
- package/src/ci-phase-intake.ts +861 -0
- package/src/ci-phase-tasking.ts +1255 -0
- package/src/ci-phase-treeless.ts +2028 -0
- package/src/ci-read-token.ts +90 -0
- package/src/cli.ts +591 -29
- package/src/close-job-template.ts +52 -15
- package/src/close-job.ts +77 -12
- package/src/complete.ts +19 -1
- package/src/config.ts +43 -48
- package/src/continue-branch.ts +121 -16
- package/src/crashed-run-save.ts +162 -0
- package/src/cwd-section.ts +10 -0
- package/src/decision-engine.ts +10 -0
- package/src/do-config.ts +2 -57
- package/src/do.ts +170 -28
- package/src/dorfl-item-template.ts +556 -0
- package/src/env-config.ts +7 -5
- package/src/frontmatter.ts +155 -2
- package/src/gc.ts +27 -2
- package/src/git.ts +83 -5
- package/src/harness.ts +58 -6
- package/src/index.ts +50 -1
- package/src/install-ci-action-pins.ts +209 -0
- package/src/install-ci-actions-pr-creation.ts +186 -0
- package/src/install-ci-capabilities/dorfl-item.ts +33 -0
- package/src/install-ci-core.ts +388 -233
- package/src/install-ci-github.ts +149 -1
- package/src/install-ci.ts +83 -37
- package/src/intake-marker.ts +6 -1
- package/src/intake-triage.ts +7 -3
- package/src/intake-trigger-template.ts +126 -327
- package/src/intake.ts +725 -544
- package/src/integration-core.ts +975 -523
- package/src/integrator.ts +28 -0
- package/src/isolation.ts +29 -10
- package/src/item-lock.ts +308 -92
- package/src/land-question-residue.ts +122 -0
- package/src/lifecycle-gather.ts +82 -10
- package/src/merge-answer.ts +82 -0
- package/src/merge-question-surfacer.ts +320 -112
- package/src/merge-question-tick.ts +182 -0
- package/src/mint-adr.ts +28 -2
- package/src/needs-attention.ts +350 -42
- package/src/phase-recorder.ts +350 -0
- package/src/phase.ts +177 -0
- package/src/pi-harness.ts +25 -5
- package/src/prepare.ts +4 -0
- package/src/prompt.ts +68 -1
- package/src/reap-branches.ts +107 -0
- package/src/ref-write.ts +324 -0
- package/src/repo-config.ts +8 -8
- package/src/repo-mirror.ts +107 -1
- package/src/review-verdict.ts +1 -1
- package/src/run.ts +47 -6
- package/src/scan.ts +37 -7
- package/src/sidecar.ts +36 -0
- package/src/single-job-warning.ts +135 -0
- package/src/spec-complete.ts +36 -14
- package/src/start.ts +55 -16
- package/src/status.ts +29 -3
- package/src/tasking.ts +593 -378
- package/src/triage-persist.ts +24 -2
- package/src/verify-workflow-template.ts +8 -4
- package/src/verify.ts +4 -0
- package/src/watch-session.ts +86 -28
- package/src/work-layout.ts +11 -3
- package/src/workspace.ts +210 -14
package/src/integrator.ts
CHANGED
|
@@ -380,6 +380,34 @@ export interface IntegrateResult {
|
|
|
380
380
|
commit?: string;
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
/**
|
|
384
|
+
* The HONEST propose-land check (task `intake-reports-the-pr-it-actually-opened`):
|
|
385
|
+
* the reason NO review request exists for a propose land that pushed its branch,
|
|
386
|
+
* else `undefined`. A propose land whose provider did not open a request (a
|
|
387
|
+
* degraded `gh pr create`, e.g. a repository that refuses PR creation by Actions;
|
|
388
|
+
* the push-only `none` provider; the `noPR` intent) returns the provider's
|
|
389
|
+
* `instruction` (the real `gh` error + the manual `gh pr create` command), so every
|
|
390
|
+
* caller can say "pushed, but no PR" instead of implying one. `undefined` for a
|
|
391
|
+
* merge land, an opened request, a benign already-landed no-op (nothing was
|
|
392
|
+
* pushed, nothing to propose), or no integration at all.
|
|
393
|
+
*/
|
|
394
|
+
export function proposeRequestNotOpened(
|
|
395
|
+
integration: IntegrateResult | undefined,
|
|
396
|
+
): string | undefined {
|
|
397
|
+
if (
|
|
398
|
+
integration === undefined ||
|
|
399
|
+
integration.mode !== 'propose' ||
|
|
400
|
+
integration.requestOpened ||
|
|
401
|
+
integration.alreadyLanded === true
|
|
402
|
+
) {
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
return (
|
|
406
|
+
integration.instruction ??
|
|
407
|
+
`Pushed ${integration.pushedRef}; no review request was opened.`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
383
411
|
/** Outcome of the full rebase-then-integrate flow. */
|
|
384
412
|
export interface IntegrateWithRebaseResult {
|
|
385
413
|
outcome: 'integrated' | 'needs-attention';
|
package/src/isolation.ts
CHANGED
|
@@ -4,8 +4,10 @@ import {reapJob} from './gc.js';
|
|
|
4
4
|
import {
|
|
5
5
|
branchAheadOfArbiter,
|
|
6
6
|
rebaseContinuedBranchOntoMain,
|
|
7
|
-
|
|
7
|
+
type ContinueRebaseFailure,
|
|
8
8
|
} from './continue-branch.js';
|
|
9
|
+
import {refWrite} from './ref-write.js';
|
|
10
|
+
import {activePhase} from './phase.js';
|
|
9
11
|
import {workBranchRef, type SlugNamespace} from './slug-namespace.js';
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -69,13 +71,22 @@ export interface IsolatedTree {
|
|
|
69
71
|
*/
|
|
70
72
|
continued?: boolean;
|
|
71
73
|
/**
|
|
72
|
-
* True iff a CONTINUE rebase onto fresh main
|
|
74
|
+
* True iff a CONTINUE rebase onto fresh main did not apply at onboard-time
|
|
73
75
|
* (a requeue kept a `work/<slug>` whose commits did not replay onto the
|
|
74
|
-
* current main
|
|
76
|
+
* current main, by a genuine conflict or any other rebase failure; aborted,
|
|
77
|
+
* never auto-resolved). The pipeline routes the item
|
|
75
78
|
* to needs-attention (the §10 path) instead of running the agent. Absent /
|
|
76
79
|
* false on a fresh cut and on a clean continue.
|
|
77
80
|
*/
|
|
78
81
|
continueRebaseConflict?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Why the continue rebase did not apply (set exactly when
|
|
84
|
+
* {@link continueRebaseConflict} is): a genuine conflict with its paths, or
|
|
85
|
+
* any other rebase failure with git's report, plus the `main` sha rebased
|
|
86
|
+
* onto. The needs-attention reason renders it with
|
|
87
|
+
* `describeContinueRebaseFailure`.
|
|
88
|
+
*/
|
|
89
|
+
continueRebaseFailure?: ContinueRebaseFailure;
|
|
79
90
|
/**
|
|
80
91
|
* Set iff the CONTINUE reconcile push to the arbiter FAILED TERMINALLY at
|
|
81
92
|
* onboard-time (the stale-lease retry cap exhausted, or a non-stale-lease
|
|
@@ -199,6 +210,7 @@ export function jobWorktreeHandle(
|
|
|
199
210
|
arbiterUrl: job.mirror.url,
|
|
200
211
|
continued: job.continued,
|
|
201
212
|
continueRebaseConflict: job.continueRebaseConflict,
|
|
213
|
+
continueRebaseFailure: job.continueRebaseFailure,
|
|
202
214
|
continuePushFailure: job.continuePushFailure,
|
|
203
215
|
teardown(opts?: {reachableOnly?: boolean}): void {
|
|
204
216
|
// Auto-reap at end-of-job (ADR §4): re-apply the provably-safe deletion
|
|
@@ -261,7 +273,7 @@ export function inPlaceStrategy(options: {
|
|
|
261
273
|
// it)? In a normal clone the refs are the remote-tracking
|
|
262
274
|
// `<arbiter>/work/<slug>` and `<arbiter>/main`.
|
|
263
275
|
let continued = false;
|
|
264
|
-
let
|
|
276
|
+
let continueRebaseFailure: ContinueRebaseFailure | undefined;
|
|
265
277
|
let continuePushFailure: string | undefined;
|
|
266
278
|
// ARBITER-AUTHORITATIVE continue-detection: `ls-remote` the arbiter so a
|
|
267
279
|
// STALE local remote-tracking ref (a plain `git fetch` does NOT prune
|
|
@@ -310,8 +322,14 @@ export function inPlaceStrategy(options: {
|
|
|
310
322
|
`${arbiter}/main`,
|
|
311
323
|
env,
|
|
312
324
|
);
|
|
313
|
-
if (rebase.kind
|
|
314
|
-
|
|
325
|
+
if (rebase.kind !== 'clean') {
|
|
326
|
+
continueRebaseFailure = rebase;
|
|
327
|
+
} else if (activePhase() === 'agent') {
|
|
328
|
+
// CI AGENT PHASE (ADR `ci-agent-job-holds-no-write-token` decision 7):
|
|
329
|
+
// the agent job holds no write token, so the rebase stays LOCAL. The
|
|
330
|
+
// apply job pushes the rebased branch (in the handoff bundle) with
|
|
331
|
+
// `--force-with-lease` against the tip the lock job observed
|
|
332
|
+
// (`continueTip`), before any other write.
|
|
315
333
|
} else {
|
|
316
334
|
// The helper THROWS on a terminal failure (the stale-lease retry cap, or
|
|
317
335
|
// a non-stale-lease rejection / unreachable arbiter). CATCH it and flag
|
|
@@ -319,7 +337,7 @@ export function inPlaceStrategy(options: {
|
|
|
319
337
|
// — the kept work stays committed + recoverable — rather than crashing
|
|
320
338
|
// the run and stranding it silently in-progress (the stale-lease bug).
|
|
321
339
|
try {
|
|
322
|
-
const pushed =
|
|
340
|
+
const pushed = refWrite.pushContinuedBranch({
|
|
323
341
|
cwd: checkout,
|
|
324
342
|
branch,
|
|
325
343
|
arbiter,
|
|
@@ -327,8 +345,8 @@ export function inPlaceStrategy(options: {
|
|
|
327
345
|
expectedRemoteTip,
|
|
328
346
|
env,
|
|
329
347
|
});
|
|
330
|
-
if (pushed.kind
|
|
331
|
-
|
|
348
|
+
if (pushed.kind !== 'pushed') {
|
|
349
|
+
continueRebaseFailure = pushed;
|
|
332
350
|
}
|
|
333
351
|
} catch (err) {
|
|
334
352
|
continuePushFailure =
|
|
@@ -376,7 +394,8 @@ export function inPlaceStrategy(options: {
|
|
|
376
394
|
arbiterRemote: arbiter,
|
|
377
395
|
arbiterUrl,
|
|
378
396
|
continued,
|
|
379
|
-
continueRebaseConflict,
|
|
397
|
+
continueRebaseConflict: continueRebaseFailure !== undefined,
|
|
398
|
+
continueRebaseFailure,
|
|
380
399
|
continuePushFailure,
|
|
381
400
|
// NO-OP teardown: the checkout is the human's / CI's tree — left on
|
|
382
401
|
// `work/<slug>` in a defined state, NEVER reaped (ADR §2/§4). The
|
package/src/item-lock.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {randomUUID} from 'node:crypto';
|
|
2
2
|
import {runAsync, type RunResult} from './git.js';
|
|
3
|
+
import {refWrite} from './ref-write.js';
|
|
3
4
|
import {
|
|
4
5
|
resolveSidecarIdentity,
|
|
5
6
|
sidecarPathFor,
|
|
@@ -128,8 +129,27 @@ export interface LockEntry {
|
|
|
128
129
|
state: LockState;
|
|
129
130
|
holder: string;
|
|
130
131
|
since: string;
|
|
132
|
+
/**
|
|
133
|
+
* Why a finished build still holds the lock, when it does (task
|
|
134
|
+
* `wire-merge-questions-into-the-advance-tick`, decision 4). Absent on every
|
|
135
|
+
* live hold. See {@link LockKeptFor}.
|
|
136
|
+
*/
|
|
137
|
+
keptFor?: LockKeptFor;
|
|
131
138
|
}
|
|
132
139
|
|
|
140
|
+
/**
|
|
141
|
+
* The PROPOSE-KEPT marker a lock entry may carry. `propose-pr`: a propose land
|
|
142
|
+
* pushed the work branch (and opened a PR where the host allows it) but the
|
|
143
|
+
* work is not on `main` yet, so the build's `implement` lock is KEPT HELD
|
|
144
|
+
* "until the PR merges" (task `propose-keep-lock-until-pr-merge`). No build is
|
|
145
|
+
* running under it. It is what lets the merge-question surfacer tell a finished
|
|
146
|
+
* propose build (ask) from a live build (skip), and what the answered-merge
|
|
147
|
+
* lock phase may take over (for the same item) instead of backing off. A lock
|
|
148
|
+
* WITHOUT the marker is a live build, including the rebuild of a bounced task's
|
|
149
|
+
* kept branch, whose tip can already carry the done-move.
|
|
150
|
+
*/
|
|
151
|
+
export type LockKeptFor = 'propose-pr';
|
|
152
|
+
|
|
133
153
|
/** Outcome of an acquire attempt. `acquired` = we hold it; `lost` = someone else
|
|
134
154
|
* does (a genuine same-item conflict). `error` = environment/usage. */
|
|
135
155
|
export type AcquireOutcome = 'acquired' | 'lost' | 'error';
|
|
@@ -151,12 +171,12 @@ export interface ReleaseResult {
|
|
|
151
171
|
}
|
|
152
172
|
|
|
153
173
|
/**
|
|
154
|
-
* Outcome of an AMEND-style transition (resume-crash-orphan
|
|
174
|
+
* Outcome of an AMEND-style transition (resume-crash-orphan) — the
|
|
155
175
|
* lock-entry STATE MACHINE's interior moves. Post-CONTRACT step (task
|
|
156
176
|
* `retire-stuck-lock-state`) the `mark-stuck` transition + the `wrong-state`
|
|
157
177
|
* verdict are retired with the `stuck` state itself.
|
|
158
178
|
* - `transitioned` — we won the CAS; the entry is now at the target state
|
|
159
|
-
* (or removed, for
|
|
179
|
+
* (or removed, for the crash-orphan clear).
|
|
160
180
|
* - `not-held` — there is no entry to transition (the move's precondition
|
|
161
181
|
* is a held entry; absent ⇒ illegal here).
|
|
162
182
|
* - `lost` — the leased CAS was rejected because a CONCURRENT writer
|
|
@@ -172,7 +192,7 @@ export interface TransitionResult {
|
|
|
172
192
|
entry: string;
|
|
173
193
|
ref: string;
|
|
174
194
|
message: string;
|
|
175
|
-
/** The lock entry AFTER a successful `transitioned` (absent for
|
|
195
|
+
/** The lock entry AFTER a successful `transitioned` (absent for a removal). */
|
|
176
196
|
lock?: LockEntry;
|
|
177
197
|
}
|
|
178
198
|
|
|
@@ -213,9 +233,12 @@ export function serialiseLockEntry(e: LockEntry): string {
|
|
|
213
233
|
`state: ${e.state}`,
|
|
214
234
|
`holder: ${e.holder}`,
|
|
215
235
|
`since: ${e.since}`,
|
|
236
|
+
...(e.keptFor === undefined ? [] : [`keptFor: ${e.keptFor}`]),
|
|
216
237
|
'---',
|
|
217
238
|
'',
|
|
218
|
-
|
|
239
|
+
e.keptFor === undefined
|
|
240
|
+
? `Lock held for \`${e.entry}\` (${e.action}/${e.state}).`
|
|
241
|
+
: `Lock kept for \`${e.entry}\` (${e.action}/${e.state}, ${e.keptFor}): the build finished; its work is not on main yet.`,
|
|
219
242
|
'',
|
|
220
243
|
].join('\n');
|
|
221
244
|
}
|
|
@@ -326,11 +349,7 @@ export async function acquireItemLock(
|
|
|
326
349
|
env,
|
|
327
350
|
);
|
|
328
351
|
// CREATE-ONLY push: --force-with-lease=<ref>: (empty) succeeds iff ref absent.
|
|
329
|
-
const push = await
|
|
330
|
-
['push', arbiter, `${commit}:${ref}`, `--force-with-lease=${ref}:`],
|
|
331
|
-
cwd,
|
|
332
|
-
env,
|
|
333
|
-
);
|
|
352
|
+
const push = await refWrite.createLockRef({arbiter, ref, commit, cwd, env});
|
|
334
353
|
if (push.status === 0) {
|
|
335
354
|
return {outcome: 'acquired', entry, ref, message: `locked ${entry}`};
|
|
336
355
|
}
|
|
@@ -379,6 +398,42 @@ export async function acquireItemLock(
|
|
|
379
398
|
}
|
|
380
399
|
}
|
|
381
400
|
|
|
401
|
+
/**
|
|
402
|
+
* Lock refs whose release is LEASED on a known sha: `<ref>` → the sha this
|
|
403
|
+
* process last observed for it (spec `ci-agent-job-without-write-token` §8, the
|
|
404
|
+
* lock-ownership rule; task `ci-split-build-path-non-integrate-intents`).
|
|
405
|
+
*
|
|
406
|
+
* The CI apply phase holds the lock the lock JOB took, and every CI run's lock
|
|
407
|
+
* `holder` is the same bot, so only the sha tells runs apart. Its releases are
|
|
408
|
+
* reached from deep inside the reused write halves (the surface, the
|
|
409
|
+
* return-to-backlog, the land's needs-attention routes), which do not carry the
|
|
410
|
+
* sha, so the apply phase registers it here for its whole run
|
|
411
|
+
* ({@link leaseLockReleases}) and {@link releaseLockEntry} refuses to delete a
|
|
412
|
+
* lock that no longer points at it. Process-global for the same reason the
|
|
413
|
+
* phase is (`phase.ts`): a phase run is a one-shot process. Empty outside the
|
|
414
|
+
* apply phase, where every release behaves exactly as before.
|
|
415
|
+
*/
|
|
416
|
+
const releaseLeases = new Map<string, string>();
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Lease every release of `item`'s lock on `expectedSha` until the returned
|
|
420
|
+
* function is called: a release then deletes the ref only while it still points
|
|
421
|
+
* at `expectedSha`, and otherwise reports an `error` and leaves it untouched (the
|
|
422
|
+
* lock was re-taken by another run).
|
|
423
|
+
*/
|
|
424
|
+
export function leaseLockReleases(
|
|
425
|
+
item: string,
|
|
426
|
+
expectedSha: string,
|
|
427
|
+
): () => void {
|
|
428
|
+
const ref = itemLockRef(lockEntryFor(item));
|
|
429
|
+
const previous = releaseLeases.get(ref);
|
|
430
|
+
releaseLeases.set(ref, expectedSha);
|
|
431
|
+
return () => {
|
|
432
|
+
if (previous === undefined) releaseLeases.delete(ref);
|
|
433
|
+
else releaseLeases.set(ref, previous);
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
382
437
|
export interface ReleaseOptions {
|
|
383
438
|
/** The NAMESPACED item identity (same forms as {@link AcquireOptions.item}). */
|
|
384
439
|
item: string;
|
|
@@ -453,11 +508,24 @@ async function releaseLockEntry(
|
|
|
453
508
|
// Delete the ref on the arbiter. Lease on the current value guards against a
|
|
454
509
|
// concurrent change between our fetch and the delete.
|
|
455
510
|
const cur = (await gitHard(['rev-parse', ref], cwd, env)).stdout.trim();
|
|
456
|
-
const
|
|
457
|
-
|
|
511
|
+
const lease = releaseLeases.get(ref);
|
|
512
|
+
if (lease !== undefined && lease !== cur) {
|
|
513
|
+
return {
|
|
514
|
+
outcome: 'error',
|
|
515
|
+
entry,
|
|
516
|
+
ref,
|
|
517
|
+
message:
|
|
518
|
+
`not released: ${ref} now points at ${cur}, not the leased ` +
|
|
519
|
+
`${lease} (another run holds the lock now)`,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
const del = await refWrite.deleteLockRef({
|
|
523
|
+
arbiter,
|
|
524
|
+
ref,
|
|
525
|
+
expectedSha: cur,
|
|
458
526
|
cwd,
|
|
459
527
|
env,
|
|
460
|
-
);
|
|
528
|
+
});
|
|
461
529
|
if (del.status === 0) {
|
|
462
530
|
// Drop our local copy of the ref too (best-effort).
|
|
463
531
|
await gitSoft(['update-ref', '-d', ref], cwd, env);
|
|
@@ -583,7 +651,7 @@ export async function releaseHeldItemLock(
|
|
|
583
651
|
/**
|
|
584
652
|
* Fetch the lock refs and return the held entry + its current ref sha, or
|
|
585
653
|
* `undefined` when the item is at REST (no ref). Shared read-before-CAS step for
|
|
586
|
-
* the amend-style transitions (
|
|
654
|
+
* the amend-style transitions (resume): they need BOTH
|
|
587
655
|
* the current entry (to check the state precondition + carry forward `action` /
|
|
588
656
|
* `holder` / `since`) and the current sha (to lease the CAS on it).
|
|
589
657
|
*/
|
|
@@ -626,52 +694,6 @@ async function fetchHeldEntry(
|
|
|
626
694
|
return {lock, sha};
|
|
627
695
|
}
|
|
628
696
|
|
|
629
|
-
/**
|
|
630
|
-
* AMEND the held entry in place via a leased CAS: build a NEW parentless commit
|
|
631
|
-
* carrying `next` and push it to the SAME ref with `--force-with-lease=<ref>:<sha>`
|
|
632
|
-
* (the sha we just read). The arbiter accepts ONLY if the ref is unchanged since
|
|
633
|
-
* our read — a concurrent writer who moved it makes our lease fail (`lost`). No
|
|
634
|
-
* retry loop: a rejection is a genuine same-item race the caller should lose.
|
|
635
|
-
*/
|
|
636
|
-
async function amendHeldEntry(
|
|
637
|
-
next: LockEntry,
|
|
638
|
-
ref: string,
|
|
639
|
-
expectedSha: string,
|
|
640
|
-
cwd: string,
|
|
641
|
-
arbiter: string,
|
|
642
|
-
env: NodeJS.ProcessEnv | undefined,
|
|
643
|
-
): Promise<TransitionResult> {
|
|
644
|
-
const commit = await buildLockCommit(next, cwd, env);
|
|
645
|
-
const push = await gitSoft(
|
|
646
|
-
[
|
|
647
|
-
'push',
|
|
648
|
-
arbiter,
|
|
649
|
-
`${commit}:${ref}`,
|
|
650
|
-
`--force-with-lease=${ref}:${expectedSha}`,
|
|
651
|
-
],
|
|
652
|
-
cwd,
|
|
653
|
-
env,
|
|
654
|
-
);
|
|
655
|
-
if (push.status === 0) {
|
|
656
|
-
// Move our local copy to the new commit too (best-effort) so a subsequent
|
|
657
|
-
// read in the same clone sees the amended entry without a refetch.
|
|
658
|
-
await gitSoft(['update-ref', ref, commit], cwd, env);
|
|
659
|
-
return {
|
|
660
|
-
outcome: 'transitioned',
|
|
661
|
-
entry: next.entry,
|
|
662
|
-
ref,
|
|
663
|
-
message: `${next.entry} → ${next.action}/${next.state}`,
|
|
664
|
-
lock: next,
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
return {
|
|
668
|
-
outcome: 'lost',
|
|
669
|
-
entry: next.entry,
|
|
670
|
-
ref,
|
|
671
|
-
message: `'${next.entry}' lock changed concurrently (CAS lost). Back off.`,
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
|
|
675
697
|
/** Verdict of the SHARED leased delete {@link leasedDeleteLockRef}: `deleted` (the
|
|
676
698
|
* ref was removed), `lost` (a concurrent change moved the ref between the read and
|
|
677
699
|
* the delete — the lease was REJECTED, never `--force`d), or `error`. */
|
|
@@ -695,17 +717,13 @@ async function leasedDeleteLockRef(
|
|
|
695
717
|
arbiter: string,
|
|
696
718
|
env: NodeJS.ProcessEnv | undefined,
|
|
697
719
|
): Promise<LeasedDeleteOutcome> {
|
|
698
|
-
const del = await
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
'--delete',
|
|
703
|
-
ref,
|
|
704
|
-
`--force-with-lease=${ref}:${expectedSha}`,
|
|
705
|
-
],
|
|
720
|
+
const del = await refWrite.deleteLockRef({
|
|
721
|
+
arbiter,
|
|
722
|
+
ref,
|
|
723
|
+
expectedSha,
|
|
706
724
|
cwd,
|
|
707
725
|
env,
|
|
708
|
-
);
|
|
726
|
+
});
|
|
709
727
|
if (del.status !== 0) {
|
|
710
728
|
return 'lost';
|
|
711
729
|
}
|
|
@@ -871,21 +889,35 @@ export async function resumeItemLock(
|
|
|
871
889
|
}
|
|
872
890
|
}
|
|
873
891
|
|
|
892
|
+
export interface MarkProposeKeptOptions {
|
|
893
|
+
/** The NAMESPACED item identity (same forms as {@link AcquireOptions.item}). */
|
|
894
|
+
item: string;
|
|
895
|
+
cwd: string;
|
|
896
|
+
arbiter?: string;
|
|
897
|
+
/**
|
|
898
|
+
* The lock sha the caller holds (the CI apply phase's `lockSha`). When set,
|
|
899
|
+
* the mark is refused (`lost`) unless the ref still points at it. Unset: the
|
|
900
|
+
* current sha is read and leased on (the laptop `complete`, which holds the
|
|
901
|
+
* lock it claimed).
|
|
902
|
+
*/
|
|
903
|
+
expectedSha?: string;
|
|
904
|
+
env?: NodeJS.ProcessEnv;
|
|
905
|
+
}
|
|
906
|
+
|
|
874
907
|
/**
|
|
875
|
-
*
|
|
876
|
-
* the
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
*
|
|
908
|
+
* STAMP the {@link LockKeptFor} `propose-pr` marker on a held lock (task
|
|
909
|
+
* `wire-merge-questions-into-the-advance-tick`, decision 4): a propose land
|
|
910
|
+
* that keeps the build's lock held "until the PR merges" calls this, so the lock
|
|
911
|
+
* says it is kept for a propose PR and no longer covers a live build. The entry
|
|
912
|
+
* keeps its `action`, `holder` and `since`; the ref is replaced by a leased CAS
|
|
913
|
+
* (never forced), so a concurrent change is `lost`. Idempotent: a lock that
|
|
914
|
+
* already carries the marker is `transitioned` without a write.
|
|
882
915
|
*/
|
|
883
|
-
export async function
|
|
884
|
-
opts:
|
|
916
|
+
export async function markLockKeptForProposePr(
|
|
917
|
+
opts: MarkProposeKeptOptions,
|
|
885
918
|
): Promise<TransitionResult> {
|
|
886
919
|
const arbiter = opts.arbiter ?? 'origin';
|
|
887
|
-
const env = opts
|
|
888
|
-
const cwd = opts.cwd;
|
|
920
|
+
const {cwd, env} = opts;
|
|
889
921
|
if (!opts.item) {
|
|
890
922
|
return {outcome: 'error', entry: '', ref: '', message: 'missing item'};
|
|
891
923
|
}
|
|
@@ -898,34 +930,155 @@ export async function requeueItemLock(
|
|
|
898
930
|
outcome: 'not-held',
|
|
899
931
|
entry,
|
|
900
932
|
ref,
|
|
901
|
-
message: `'${entry}' not locked`,
|
|
933
|
+
message: `'${entry}' not locked; nothing to mark`,
|
|
902
934
|
};
|
|
903
935
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
'
|
|
936
|
+
if (opts.expectedSha !== undefined && held.sha !== opts.expectedSha) {
|
|
937
|
+
return {
|
|
938
|
+
outcome: 'lost',
|
|
939
|
+
entry,
|
|
940
|
+
ref,
|
|
941
|
+
message: `'${entry}' now points at ${held.sha}, not ${opts.expectedSha}; not marked`,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
if (held.lock.keptFor === 'propose-pr') {
|
|
945
|
+
return {
|
|
946
|
+
outcome: 'transitioned',
|
|
947
|
+
entry,
|
|
948
|
+
ref,
|
|
949
|
+
message: `'${entry}' is already kept for a propose PR`,
|
|
950
|
+
lock: held.lock,
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
const lock: LockEntry = {...held.lock, keptFor: 'propose-pr'};
|
|
954
|
+
const commit = await buildLockCommit(lock, cwd, env);
|
|
955
|
+
const push = await refWrite.replaceLockRef({
|
|
956
|
+
arbiter,
|
|
957
|
+
ref,
|
|
958
|
+
commit,
|
|
959
|
+
expectedSha: held.sha,
|
|
960
|
+
cwd,
|
|
961
|
+
env,
|
|
962
|
+
});
|
|
963
|
+
if (push.status !== 0) {
|
|
964
|
+
return {
|
|
965
|
+
outcome: 'lost',
|
|
966
|
+
entry,
|
|
967
|
+
ref,
|
|
968
|
+
message: `marking '${entry}' kept for a propose PR was rejected: ${push.stderr.trim()}`,
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
return {
|
|
972
|
+
outcome: 'transitioned',
|
|
973
|
+
entry,
|
|
974
|
+
ref,
|
|
975
|
+
message: `marked '${entry}' kept for a propose PR`,
|
|
976
|
+
lock,
|
|
977
|
+
};
|
|
978
|
+
} catch (err) {
|
|
979
|
+
return {
|
|
980
|
+
outcome: 'error',
|
|
981
|
+
entry,
|
|
982
|
+
ref,
|
|
983
|
+
message: err instanceof Error ? err.message : String(err),
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
export interface TakeOverProposeKeptOptions {
|
|
989
|
+
/** The NAMESPACED item identity (same forms as {@link AcquireOptions.item}). */
|
|
990
|
+
item: string;
|
|
991
|
+
/** The action the taken-over lock is held for (the answered merge: `advance`). */
|
|
992
|
+
action: LockAction;
|
|
993
|
+
cwd: string;
|
|
994
|
+
arbiter?: string;
|
|
995
|
+
holder?: string;
|
|
996
|
+
env?: NodeJS.ProcessEnv;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* TAKE OVER a lock that a finished propose build of the SAME item keeps held
|
|
1001
|
+
* (task `wire-merge-questions-into-the-advance-tick`, decisions 2 and 4): the
|
|
1002
|
+
* answered-merge lock phase calls this when the create-only acquire found the
|
|
1003
|
+
* item locked. It succeeds ONLY when the held entry carries the
|
|
1004
|
+
* {@link LockKeptFor} `propose-pr` marker and was taken for `implement` (the
|
|
1005
|
+
* propose build's own claim); the ref is then replaced, leased on the sha read,
|
|
1006
|
+
* by a fresh `action` hold without the marker, so the rest of the run owns it
|
|
1007
|
+
* exactly like a lock it created. Any other held lock (a live build, a tasking,
|
|
1008
|
+
* another advance) is `lost`, as the plain acquire reports it. A lock released
|
|
1009
|
+
* meanwhile falls back to the plain create-only acquire.
|
|
1010
|
+
*/
|
|
1011
|
+
export async function takeOverProposeKeptLock(
|
|
1012
|
+
opts: TakeOverProposeKeptOptions,
|
|
1013
|
+
): Promise<AcquireResult> {
|
|
1014
|
+
const arbiter = opts.arbiter ?? 'origin';
|
|
1015
|
+
const {cwd, env} = opts;
|
|
1016
|
+
if (!opts.item) {
|
|
1017
|
+
return {outcome: 'error', entry: '', ref: '', message: 'missing item'};
|
|
1018
|
+
}
|
|
1019
|
+
const entry = lockEntryFor(opts.item);
|
|
1020
|
+
const ref = itemLockRef(entry);
|
|
1021
|
+
try {
|
|
1022
|
+
const held = await fetchHeldEntry(entry, ref, cwd, arbiter, env);
|
|
1023
|
+
if (!held) {
|
|
1024
|
+
return acquireItemLock({
|
|
1025
|
+
item: opts.item,
|
|
1026
|
+
action: opts.action,
|
|
1027
|
+
cwd,
|
|
907
1028
|
arbiter,
|
|
908
|
-
|
|
1029
|
+
holder: opts.holder,
|
|
1030
|
+
env,
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
if (
|
|
1034
|
+
held.lock.keptFor !== 'propose-pr' ||
|
|
1035
|
+
held.lock.action !== 'implement' ||
|
|
1036
|
+
held.lock.entry !== entry
|
|
1037
|
+
) {
|
|
1038
|
+
return {
|
|
1039
|
+
outcome: 'lost',
|
|
1040
|
+
entry,
|
|
909
1041
|
ref,
|
|
910
|
-
|
|
911
|
-
|
|
1042
|
+
message:
|
|
1043
|
+
`'${entry}' is already locked (${held.lock.action}, not kept for a ` +
|
|
1044
|
+
'propose PR): a live hold is never taken over. Back off.',
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
const holder = opts.holder ?? (await resolveHolder(cwd, env));
|
|
1048
|
+
const commit = await buildLockCommit(
|
|
1049
|
+
{
|
|
1050
|
+
entry,
|
|
1051
|
+
action: opts.action,
|
|
1052
|
+
state: 'active',
|
|
1053
|
+
holder,
|
|
1054
|
+
since: new Date().toISOString(),
|
|
1055
|
+
},
|
|
912
1056
|
cwd,
|
|
913
1057
|
env,
|
|
914
1058
|
);
|
|
915
|
-
|
|
916
|
-
|
|
1059
|
+
const push = await refWrite.replaceLockRef({
|
|
1060
|
+
arbiter,
|
|
1061
|
+
ref,
|
|
1062
|
+
commit,
|
|
1063
|
+
expectedSha: held.sha,
|
|
1064
|
+
cwd,
|
|
1065
|
+
env,
|
|
1066
|
+
});
|
|
1067
|
+
if (push.status !== 0) {
|
|
917
1068
|
return {
|
|
918
|
-
outcome: '
|
|
1069
|
+
outcome: 'lost',
|
|
919
1070
|
entry,
|
|
920
1071
|
ref,
|
|
921
|
-
message: `
|
|
1072
|
+
message: `taking over '${entry}' lost the race: ${push.stderr.trim()}`,
|
|
922
1073
|
};
|
|
923
1074
|
}
|
|
1075
|
+
// Keep the local copy in step, as the create-only acquire's fetch would.
|
|
1076
|
+
await gitSoft(['update-ref', ref, commit], cwd, env);
|
|
924
1077
|
return {
|
|
925
|
-
outcome: '
|
|
1078
|
+
outcome: 'acquired',
|
|
926
1079
|
entry,
|
|
927
1080
|
ref,
|
|
928
|
-
message: `'${entry}'
|
|
1081
|
+
message: `took over '${entry}' from its finished propose build (kept for a propose PR)`,
|
|
929
1082
|
};
|
|
930
1083
|
} catch (err) {
|
|
931
1084
|
return {
|
|
@@ -2410,6 +2563,65 @@ export async function heldTaskSlugsStrict(
|
|
|
2410
2563
|
);
|
|
2411
2564
|
}
|
|
2412
2565
|
|
|
2566
|
+
/**
|
|
2567
|
+
* STRICT (fail-CLOSED) twin of {@link listItemLockEntries}: THROWS when the
|
|
2568
|
+
* lock refs cannot be read (the same fetch {@link listItemLocks} does), so a
|
|
2569
|
+
* caller that ACTS on the absence of a lock (the merge-question surfacer asks
|
|
2570
|
+
* about a branch only when no live build holds its item) never mistakes an
|
|
2571
|
+
* unreadable arbiter for "no locks". A held ref whose body does not parse is
|
|
2572
|
+
* returned as a plain `implement` hold without the marker (a live build: the
|
|
2573
|
+
* safe reading).
|
|
2574
|
+
*/
|
|
2575
|
+
export async function listItemLockEntriesStrict(
|
|
2576
|
+
cwd: string,
|
|
2577
|
+
arbiter = 'origin',
|
|
2578
|
+
env?: NodeJS.ProcessEnv,
|
|
2579
|
+
): Promise<LockEntry[]> {
|
|
2580
|
+
const names = await listItemLocks(cwd, arbiter, env);
|
|
2581
|
+
const entries: LockEntry[] = [];
|
|
2582
|
+
for (const entry of names) {
|
|
2583
|
+
const show = await gitSoft(
|
|
2584
|
+
['show', `${itemLockRef(entry)}:lock.md`],
|
|
2585
|
+
cwd,
|
|
2586
|
+
env,
|
|
2587
|
+
);
|
|
2588
|
+
const lock = show.status === 0 ? parseLockEntry(show.stdout) : undefined;
|
|
2589
|
+
entries.push(
|
|
2590
|
+
lock ?? {
|
|
2591
|
+
entry,
|
|
2592
|
+
action: 'implement',
|
|
2593
|
+
state: 'active',
|
|
2594
|
+
holder: '',
|
|
2595
|
+
since: '',
|
|
2596
|
+
},
|
|
2597
|
+
);
|
|
2598
|
+
}
|
|
2599
|
+
return entries;
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
/**
|
|
2603
|
+
* The TASK slugs whose lock a finished propose build keeps held, i.e. whose
|
|
2604
|
+
* entry carries the {@link LockKeptFor} `propose-pr` marker (task
|
|
2605
|
+
* `wire-merge-questions-into-the-advance-tick`, decision 6). Selection uses it
|
|
2606
|
+
* to let an answered `merge` through the held-slug subtraction; the
|
|
2607
|
+
* merge-question surfacer to tell a finished propose build from a live one.
|
|
2608
|
+
* GRACEFUL: a read fault yields an empty set, so a held lock keeps excluding
|
|
2609
|
+
* its item (the safe direction).
|
|
2610
|
+
*/
|
|
2611
|
+
export async function proposeKeptTaskSlugs(
|
|
2612
|
+
cwd: string,
|
|
2613
|
+
arbiter = 'origin',
|
|
2614
|
+
env?: NodeJS.ProcessEnv,
|
|
2615
|
+
): Promise<Set<string>> {
|
|
2616
|
+
const entries = await listItemLockEntries(cwd, arbiter, env);
|
|
2617
|
+
const prefix = 'task-';
|
|
2618
|
+
return new Set(
|
|
2619
|
+
entries
|
|
2620
|
+
.filter((e) => e.keptFor === 'propose-pr' && e.entry.startsWith(prefix))
|
|
2621
|
+
.map((e) => e.entry.slice(prefix.length)),
|
|
2622
|
+
);
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2413
2625
|
/**
|
|
2414
2626
|
* List the SPEC slugs currently lock-held on the arbiter — the held-SPEC set the
|
|
2415
2627
|
* TASKABLE-spec pool readers SUBTRACT (fix
|
|
@@ -2492,6 +2704,10 @@ export function parseLockEntry(body: string): LockEntry | undefined {
|
|
|
2492
2704
|
state: 'active',
|
|
2493
2705
|
holder: fields.holder ?? '',
|
|
2494
2706
|
since: fields.since ?? '',
|
|
2707
|
+
// Only the one known marker value is read; anything else is a live hold.
|
|
2708
|
+
...(fields.keptFor === 'propose-pr'
|
|
2709
|
+
? {keptFor: 'propose-pr' as const}
|
|
2710
|
+
: {}),
|
|
2495
2711
|
};
|
|
2496
2712
|
}
|
|
2497
2713
|
|