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/dist/integration-core.js
CHANGED
|
@@ -5,14 +5,16 @@ import { LEDGER_STATUS_FOLDERS, workItemPath, workItemRel, workFolderPath, workF
|
|
|
5
5
|
import { runVerify } from './verify.js';
|
|
6
6
|
import { ensurePrepared } from './prepare.js';
|
|
7
7
|
import { ReviewParseError, formatBlockReason, reviewRoundsExhaustedReason, } from './review-gate.js';
|
|
8
|
+
import { proposeRequestNotOpened, } from './integrator.js';
|
|
8
9
|
import { extractDecisionsBlock } from './agent-stop.js';
|
|
9
10
|
import { ledgerWrite } from './ledger-write.js';
|
|
10
11
|
import { selectProvider } from './github.js';
|
|
11
|
-
import { parseFrontmatter } from './frontmatter.js';
|
|
12
|
+
import { parseFrontmatter, unquoteYamlScalar } from './frontmatter.js';
|
|
12
13
|
import { git, run, runAsync } from './git.js';
|
|
13
14
|
import { realSleep } from './retry-backoff.js';
|
|
14
15
|
import { workBranchRef } from './slug-namespace.js';
|
|
15
16
|
import { isAncestor } from './gc.js';
|
|
17
|
+
import { clearOwnQuestionResidueOnLand } from './land-question-residue.js';
|
|
16
18
|
import { detectColocatedSidecars, formatSidecarGuardReason, } from './sidecar-guard.js';
|
|
17
19
|
/**
|
|
18
20
|
* Build the human-facing CONTEXT tail appended to an `acceptance gate failed`
|
|
@@ -156,7 +158,6 @@ export async function performIntegration(input) {
|
|
|
156
158
|
const arbiter = input.arbiter;
|
|
157
159
|
const slug = input.slug;
|
|
158
160
|
const source = input.source;
|
|
159
|
-
const recovering = input.recovering;
|
|
160
161
|
const note = input.note ?? (() => { });
|
|
161
162
|
// The work branch: the caller is on it (it carries the namespaced identity).
|
|
162
163
|
// Prefer the explicit `branch`; else read the branch HEAD is on; only fall
|
|
@@ -574,6 +575,18 @@ export async function performIntegration(input) {
|
|
|
574
575
|
if (!lifecycle) {
|
|
575
576
|
transcribeDecisionsIntoDoneRecord({ cwd, slug, output: input.body, note });
|
|
576
577
|
}
|
|
578
|
+
// 2c. CLEAR THE TASK'S OWN STALE QUESTION STATE in the SAME done-move commit
|
|
579
|
+
// (task `a-recovered-task-lands-without-stale-question-state`). A task that
|
|
580
|
+
// was surfaced to needs-attention and then recovered carries
|
|
581
|
+
// `needsAnswers: true` + its `work/questions/task-<slug>.md` sidecar on
|
|
582
|
+
// this branch; the done-move makes both moot, so they leave in the commit
|
|
583
|
+
// that makes them moot rather than on some later claim's reconcile. A
|
|
584
|
+
// normal land is untouched. Residue that only ARRIVES with the rebase (the
|
|
585
|
+
// surface landed on `main` after this branch was cut) is folded in after
|
|
586
|
+
// the rebase instead ({@link foldQuestionResidueIntoTip}).
|
|
587
|
+
if (!lifecycle) {
|
|
588
|
+
clearOwnQuestionResidueOnLand({ cwd, slug, note });
|
|
589
|
+
}
|
|
577
590
|
// 3. Commit: git add -A (the agent's uncommitted work + the move) into ONE
|
|
578
591
|
// atomic commit. Nothing to commit is FATAL (no-op-is-fatal, like claim.sh).
|
|
579
592
|
await gitHard(['add', '-A'], cwd, env);
|
|
@@ -612,205 +625,35 @@ export async function performIntegration(input) {
|
|
|
612
625
|
// needs-attention). Single-job callers pass no lock ⇒ the tail runs directly,
|
|
613
626
|
// byte-for-byte unchanged. The lock is keyed per repo, so cross-repo
|
|
614
627
|
// integration stays fully concurrent.
|
|
628
|
+
const reconcileCtx = {
|
|
629
|
+
cwd,
|
|
630
|
+
arbiter,
|
|
631
|
+
slug,
|
|
632
|
+
branch,
|
|
633
|
+
lifecycle: lifecycle !== undefined,
|
|
634
|
+
source,
|
|
635
|
+
surfaceArbiter: input.surfaceArbiter,
|
|
636
|
+
commitMessage,
|
|
637
|
+
env,
|
|
638
|
+
note,
|
|
639
|
+
};
|
|
640
|
+
// The rebase-to-integrate TAIL (step 4 fetch+rebase → step 5 integrate) is the
|
|
641
|
+
// ONLY region serialised per repo under the `run` concurrency seam: it is the
|
|
642
|
+
// land-on-`main` band where two concurrent SAME-repo merge jobs would otherwise
|
|
643
|
+
// race (the loser pushing a non-fast-forward `${branch}:main`). Wrapping ONLY
|
|
644
|
+
// this tail keeps the front-of-band gate (`prepare`+`verify`) and the Gate-2
|
|
645
|
+
// review agent CONCURRENT across same-repo jobs (run's parallelism); inside the
|
|
646
|
+
// lock the loser re-fetches + rebases onto the winner's now-advanced main, so
|
|
647
|
+
// its push is a clean fast-forward (a genuine conflict routes ONE to
|
|
648
|
+
// needs-attention). Single-job callers pass no lock ⇒ the tail runs directly,
|
|
649
|
+
// byte-for-byte unchanged. The lock is keyed per repo, so cross-repo
|
|
650
|
+
// integration stays fully concurrent.
|
|
615
651
|
const runRebaseToIntegrateTail = async () => {
|
|
616
|
-
// The step-4 rebase-onto-`<arbiter>/main` (with BOTH reconciliation arms: the
|
|
617
|
-
// sibling-slug ledger arm and the divergent-done-move recovery), factored so it
|
|
618
|
-
// can run ONCE before the gate AND be RE-RUN in the Race-1 merge-push retry loop
|
|
619
|
-
// (a sibling advancing main mid-push needs the SAME reconcile, not a bare
|
|
620
|
-
// rebase). Returns `{}` on a clean rebase (fall through to gate/integrate) or
|
|
621
|
-
// `{route}` when a genuine conflict / invariant violation must stop the tail.
|
|
622
|
-
const rebaseOntoMainWithReconcile = async () => {
|
|
623
|
-
// 4. Rebase-before-integrate (ADR §10): rebase the work branch onto the
|
|
624
|
-
// latest <arbiter>/main. Clean → continue. Conflict → abort + stop.
|
|
625
|
-
//
|
|
626
|
-
// RECOVERY reconciliation: post the per-item-lock cut-over, a stuck item's
|
|
627
|
-
// body never moved into a `needs-attention/` folder (stuck is the lock
|
|
628
|
-
// `state: stuck`; the body rests in `backlog/`), so a recovery `complete`'s
|
|
629
|
-
// kept branch carries NO historical `in-progress → needs-attention`
|
|
630
|
-
// move-only commit to drop and `<arbiter>/main` holds no surface move to
|
|
631
|
-
// conflict with. The old recovery drop (`rebaseDroppingNeedsAttentionSurface`,
|
|
632
|
-
// drop-bookkeeping-rebase) is deleted; recovery is now the SAME plain replay
|
|
633
|
-
// onto `<arbiter>/main` as any other build (see the plain-rebase note below).
|
|
634
|
-
// Fetch the arbiter's `main` into the `<arbiter>/main` remote-tracking ref
|
|
635
|
-
// EXPLICITLY. A `run` JOB WORKTREE is cut from a bare hub mirror whose remote
|
|
636
|
-
// has no fetch refspec (so `<arbiter>/main` would not otherwise resolve / would
|
|
637
|
-
// be stale, causing a spurious rebase conflict); a regular clone (`do`/
|
|
638
|
-
// `complete`) already has it, where the explicit refspec is harmless (the same
|
|
639
|
-
// refspec `rebaseOntoArbiterMain` used before the convergence).
|
|
640
|
-
await gitHard([
|
|
641
|
-
'fetch',
|
|
642
|
-
'--quiet',
|
|
643
|
-
arbiter,
|
|
644
|
-
`+refs/heads/main:refs/remotes/${arbiter}/main`,
|
|
645
|
-
], cwd, env);
|
|
646
|
-
// ONE-SLUG-ONE-FOLDER guard + divergent-base PRE-CHECK, read from the
|
|
647
|
-
// freshly-fetched `<arbiter>/main` (a READ of the tracking ref the fetch above
|
|
648
|
-
// just populated — NO new fetch, so no shared-mirror race). It (a) FAILS LOUD if
|
|
649
|
-
// the arbiter already holds the slug in >1 status folder with differing content
|
|
650
|
-
// (a corrupt ledger; never publish over it), and (b) detects the DIVERGENT base
|
|
651
|
-
// — the arbiter holds the slug's source in a DIFFERENT folder than our local
|
|
652
|
-
// done-move removed — which is the case that turns the rebased "move" into a
|
|
653
|
-
// "copy" (PR #86). The tasking lifecycle is exempt (its move is not a task
|
|
654
|
-
// done-move).
|
|
655
|
-
// CONTINUE-BUILD EXEMPTION (task
|
|
656
|
-
// `complete-builds-on-already-done-moved-continue`): on `source: 'done'`
|
|
657
|
-
// there was no first-time move on this commit (the slug was already in
|
|
658
|
-
// `done/` on the kept branch + on the arbiter), so the divergent-done-move
|
|
659
|
-
// reconcile reasoning does not apply. Skip the arbiter ledger placement
|
|
660
|
-
// pre-check too: it is structurally an instrument FOR that reconcile, and a
|
|
661
|
-
// continue-build is by construction already in `done/` on both sides.
|
|
662
|
-
if (!lifecycle && source !== 'done') {
|
|
663
|
-
const arbiterPlacement = readArbiterLedgerPlacement(cwd, arbiter, slug, env);
|
|
664
|
-
if (arbiterPlacement.error) {
|
|
665
|
-
note(arbiterPlacement.error);
|
|
666
|
-
return {
|
|
667
|
-
route: {
|
|
668
|
-
outcome: 'invariant-violation',
|
|
669
|
-
routedToNeedsAttention: false,
|
|
670
|
-
branch,
|
|
671
|
-
reason: arbiterPlacement.error,
|
|
672
|
-
},
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
// PLAIN rebase. After the per-item-lock cut-over (spec
|
|
677
|
-
// `ledger-status-per-item-lock-refs`, tasks 9a–9d) no transient status
|
|
678
|
-
// lands on a work branch: needs-attention is the lock `state: stuck` (not a
|
|
679
|
-
// `git mv` to `needs-attention/`), the body rests in `backlog/` while
|
|
680
|
-
// claimed, and the tasking/advancing markers are gone. So a recovery
|
|
681
|
-
// complete's kept branch carries NO historical route-to-needs-attention
|
|
682
|
-
// move-only commit to drop — the old `rebaseDroppingNeedsAttentionSurface`
|
|
683
|
-
// (drop-bookkeeping-rebase) is deleted and BOTH recovering and lifecycle
|
|
684
|
-
// rebases are the same plain replay onto `<arbiter>/main`.
|
|
685
|
-
void recovering;
|
|
686
|
-
void lifecycle;
|
|
687
|
-
// RENAME-DETECTION-OFF (task
|
|
688
|
-
// `disable-rename-detection-on-continue-rebase`): scope
|
|
689
|
-
// `-c merge.directoryRenames=false` to THIS rebase invocation, so a single
|
|
690
|
-
// durable folder-transition `git mv` out of a SPARSE work/<from>/ folder is
|
|
691
|
-
// NOT misread by git's directory-rename heuristic as a whole-DIRECTORY
|
|
692
|
-
// rename `work/<from>/ → work/<to>/` (which would spuriously flag every
|
|
693
|
-
// sibling file `<arbiter>/main` added into that folder as `CONFLICT (file
|
|
694
|
-
// location)` and force a FALSE needs-attention). Content-rename detection
|
|
695
|
-
// (`-Xno-renames`/`merge.renames`/`diff.renames`) is the wrong knob and
|
|
696
|
-
// does NOT suppress this directory-rename conflict; only
|
|
697
|
-
// `merge.directoryRenames=false` does. NEVER a persistent `git config`
|
|
698
|
-
// write — the repo's config stays clean so a user's interactive
|
|
699
|
-
// `git rebase` is unaffected. A GENUINE content conflict still surfaces
|
|
700
|
-
// and still routes via `rebaseConflictRoute()` below.
|
|
701
|
-
const rebase = await gitSoft(['-c', 'merge.directoryRenames=false', 'rebase', `${arbiter}/main`], cwd, env);
|
|
702
|
-
if (rebase.status !== 0) {
|
|
703
|
-
// NEVER auto-resolve a genuine CODE conflict. But FIRST, a SIBLING-SLUG
|
|
704
|
-
// LEDGER conflict (the replay conflicts ONLY on OTHER slugs'
|
|
705
|
-
// `work/<status>/<otherslug>.md` ledger files — a sibling job landed its own
|
|
706
|
-
// status-folder move on `<arbiter>/main` between our base and this rebase) is
|
|
707
|
-
// a benign ledger-only divergence with NO semantic judgement: the reconcile
|
|
708
|
-
// ABORTS the rebase, then redoes OUR work as one clean commit on top of
|
|
709
|
-
// `<arbiter>/main` (taking the arbiter's version of every sibling ledger file
|
|
710
|
-
// automatically). It is scoped STRICTLY to other slugs' ledger files — a
|
|
711
|
-
// conflict touching ANY code file, or THIS slug's own ledger, returns `false`
|
|
712
|
-
// (and leaves the rebase in progress), so the divergent-done-move recovery /
|
|
713
|
-
// needs-attention route below handles it; it NEVER widens to code. The tasking
|
|
714
|
-
// lifecycle is exempt (its move is not a task done-move).
|
|
715
|
-
const siblingReconciled = lifecycle
|
|
716
|
-
? false
|
|
717
|
-
: await reconcileSiblingLedgerConflict({
|
|
718
|
-
cwd,
|
|
719
|
-
arbiter,
|
|
720
|
-
slug,
|
|
721
|
-
env,
|
|
722
|
-
note,
|
|
723
|
-
});
|
|
724
|
-
if (siblingReconciled) {
|
|
725
|
-
// The branch is now cleanly on top of `<arbiter>/main` with OUR work + the
|
|
726
|
-
// arbiter's sibling-ledger files — fall through to the fresh-gate + integrate
|
|
727
|
-
// band. THIS slug's own move is untouched.
|
|
728
|
-
note(`Reconciled a sibling-slug ledger conflict during the rebase onto ` +
|
|
729
|
-
`${arbiter}/main (took the arbiter's version of the other slugs' ` +
|
|
730
|
-
`work/<status>/<slug>.md ledger files; no code file was touched).`);
|
|
731
|
-
}
|
|
732
|
-
else if (!lifecycle && source !== 'done') {
|
|
733
|
-
// NEVER auto-resolve a genuine CODE conflict: abort the rebase. But FIRST, a
|
|
734
|
-
// DIVERGENT-LEDGER conflict (the arbiter holds the slug's source in a folder
|
|
735
|
-
// our local done-move did not remove — PR #86) is auto-RECONCILABLE without
|
|
736
|
-
// any semantic judgement: redo the done-move arbiter-resolved (remove the
|
|
737
|
-
// arbiter's actual source folder, add `done/`) on top of `<arbiter>/main`. We
|
|
738
|
-
// only do this when the post-abort tree's ONLY divergence is the slug's ledger
|
|
739
|
-
// file; a real code conflict still routes to needs-attention untouched.
|
|
740
|
-
await gitSoft(['rebase', '--abort'], cwd, env);
|
|
741
|
-
const recovered = await reconcileDivergentDoneMove({
|
|
742
|
-
cwd,
|
|
743
|
-
arbiter,
|
|
744
|
-
slug,
|
|
745
|
-
branch,
|
|
746
|
-
localSource: source,
|
|
747
|
-
env,
|
|
748
|
-
note,
|
|
749
|
-
});
|
|
750
|
-
if (recovered) {
|
|
751
|
-
// The branch is now cleanly on top of `<arbiter>/main` with the slug in
|
|
752
|
-
// `done/` ONLY — fall through to integrate (skip the needs-attention
|
|
753
|
-
// route below).
|
|
754
|
-
note(`Reconciled the done-move against ${arbiter}/main: '${slug}' is in ` +
|
|
755
|
-
'work/done/ ONLY (the divergent source folder was removed; the move ' +
|
|
756
|
-
'is a move, not a copy).');
|
|
757
|
-
}
|
|
758
|
-
else {
|
|
759
|
-
return { route: await rebaseConflictRoute() };
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
else {
|
|
763
|
-
await gitSoft(['rebase', '--abort'], cwd, env);
|
|
764
|
-
return { route: await rebaseConflictRoute() };
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
// Clean rebase (or a reconciled one): fall through to the gate + integrate.
|
|
768
|
-
return {};
|
|
769
|
-
};
|
|
770
652
|
// Run the step-4 rebase ONCE up front (before the slow fresh gate).
|
|
771
|
-
const firstRebase = await rebaseOntoMainWithReconcile();
|
|
653
|
+
const firstRebase = await rebaseOntoMainWithReconcile(reconcileCtx);
|
|
772
654
|
if (firstRebase.route) {
|
|
773
655
|
return firstRebase.route;
|
|
774
656
|
}
|
|
775
|
-
// The rebase-conflict needs-attention route, factored so the divergent-ledger
|
|
776
|
-
// recovery above can fall through to integrate while a genuine code conflict
|
|
777
|
-
// still routes here.
|
|
778
|
-
async function rebaseConflictRoute() {
|
|
779
|
-
// Then mark the item stuck on its per-item lock with the conflict reason
|
|
780
|
-
// (ADR §12) THROUGH the ledger write seam's needs-attention transition,
|
|
781
|
-
// rather than leaving it dangling. Post lock-cutover this is a lock amend
|
|
782
|
-
// (`state: stuck` + the reason on the lock entry, no `done/ →
|
|
783
|
-
// needs-attention/` folder move); the done-move already committed above
|
|
784
|
-
// stands. No partial state.
|
|
785
|
-
const reason = `rebase onto ${arbiter}/main conflicted (aborted, never auto-resolved)`;
|
|
786
|
-
const routed = await ledgerWrite.applyNeedsAttentionTransition({
|
|
787
|
-
cwd,
|
|
788
|
-
slug,
|
|
789
|
-
reason,
|
|
790
|
-
// Autonomous caller (`do`) passes the arbiter so the seam both surfaces the
|
|
791
|
-
// conflict on `main` (OBSERVABLE) AND pushes the `work/<slug>` branch
|
|
792
|
-
// (RECOVERABLE, cross-machine). The human `complete` leaves it unset →
|
|
793
|
-
// no surface, no push, local-only. The push lives in the seam (HEAD's
|
|
794
|
-
// branch is `work/<slug>` here) — no bolted-on push.
|
|
795
|
-
arbiter: input.surfaceArbiter,
|
|
796
|
-
env,
|
|
797
|
-
note,
|
|
798
|
-
});
|
|
799
|
-
return {
|
|
800
|
-
outcome: 'rebase-conflict',
|
|
801
|
-
routedToNeedsAttention: routed.moved,
|
|
802
|
-
branch,
|
|
803
|
-
commitMessage,
|
|
804
|
-
reason: routed.moved
|
|
805
|
-
? `Rebasing ${branch} onto ${arbiter}/main conflicted; the rebase was ` +
|
|
806
|
-
`aborted (never auto-resolved) and '${slug}' was marked stuck on ` +
|
|
807
|
-
'its per-item lock (surfaced by status). Resolve against the latest ' +
|
|
808
|
-
'main, then `requeue` to release the stuck lock and re-run.'
|
|
809
|
-
: `Rebasing ${branch} onto ${arbiter}/main conflicted; the rebase was ` +
|
|
810
|
-
'aborted (never auto-resolved). Resolve against the latest main, ' +
|
|
811
|
-
'then re-run complete.',
|
|
812
|
-
};
|
|
813
|
-
}
|
|
814
657
|
// 4c. FRESH-WORKTREE GATE (task `gate-on-rebased-tip-fresh-worktree`): when ON,
|
|
815
658
|
// the acceptance gate (`prepare` then `verify`) runs HERE — on the work
|
|
816
659
|
// branch tip the rebase above just produced (the would-be-integrated tip) —
|
|
@@ -934,209 +777,513 @@ export async function performIntegration(input) {
|
|
|
934
777
|
}
|
|
935
778
|
}
|
|
936
779
|
}
|
|
937
|
-
// 5.
|
|
938
|
-
//
|
|
939
|
-
//
|
|
940
|
-
//
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
780
|
+
// 5. The LAND half (`landIntegration`, reached through the `integrationLand`
|
|
781
|
+
// seam): integrate per mode with the merge-mode CAS loop, then the review
|
|
782
|
+
// comment. The CI agent phase records this call as its boundary; the apply
|
|
783
|
+
// phase resumes here (task `ci-split-build-path`).
|
|
784
|
+
return await integrationLand.land({
|
|
785
|
+
...reconcileCtx,
|
|
786
|
+
mode,
|
|
787
|
+
noPR: input.noPR,
|
|
788
|
+
providerInstance: input.providerInstance,
|
|
789
|
+
openPr: input.openPr,
|
|
790
|
+
title: prTitle,
|
|
791
|
+
body: composeProposeBody({
|
|
792
|
+
slug,
|
|
793
|
+
body: input.body,
|
|
794
|
+
header: lifecycle?.proposeHeader,
|
|
795
|
+
}),
|
|
796
|
+
reviewProse: approvedVerdict?.review ?? input.approvedReviewProse,
|
|
797
|
+
mergeRetries: input.mergeRetries,
|
|
798
|
+
mergeJitterMs: input.mergeJitterMs,
|
|
799
|
+
});
|
|
800
|
+
};
|
|
801
|
+
// Serialise ONLY this tail per repo when the `run` seam is wired; absent ⇒ run
|
|
802
|
+
// it directly (an un-contended no-op, single-job behaviour unchanged).
|
|
803
|
+
return input.integrateLock && input.integrateLockKey !== undefined
|
|
804
|
+
? await input.integrateLock(input.integrateLockKey, runRebaseToIntegrateTail)
|
|
805
|
+
: await runRebaseToIntegrateTail();
|
|
806
|
+
}
|
|
807
|
+
/** The process-wide {@link IntegrationLandSeam} (replaced only by the phase recorder). */
|
|
808
|
+
export const integrationLand = {
|
|
809
|
+
land: (input) => landIntegration(input),
|
|
810
|
+
};
|
|
811
|
+
/**
|
|
812
|
+
* The step-4 rebase-onto-`<arbiter>/main` (with BOTH reconciliation arms: the
|
|
813
|
+
* sibling-slug ledger arm and the divergent-done-move recovery), factored so it
|
|
814
|
+
* can run ONCE before the gate AND be RE-RUN in the Race-1 merge-push retry loop
|
|
815
|
+
* (a sibling advancing main mid-push needs the SAME reconcile, not a bare
|
|
816
|
+
* rebase). Returns `{}` on a clean rebase (fall through to gate/integrate) or
|
|
817
|
+
* `{route}` when a genuine conflict / invariant violation must stop the tail.
|
|
818
|
+
* Module-level (not a closure) so BOTH halves of the rebase-to-integrate tail
|
|
819
|
+
* reach it: the agent half runs it once before the fresh gate, and the land
|
|
820
|
+
* half ({@link landIntegration}) re-runs it in the merge-mode CAS loop.
|
|
821
|
+
*/
|
|
822
|
+
async function rebaseOntoMainWithReconcile(ctx) {
|
|
823
|
+
const { cwd, arbiter, slug, branch, lifecycle, source, env } = ctx;
|
|
824
|
+
const note = ctx.note;
|
|
825
|
+
// 4. Rebase-before-integrate (ADR §10): rebase the work branch onto the
|
|
826
|
+
// latest <arbiter>/main. Clean → continue. Conflict → abort + stop.
|
|
827
|
+
//
|
|
828
|
+
// RECOVERY reconciliation: post the per-item-lock cut-over, a stuck item's
|
|
829
|
+
// body never moved into a `needs-attention/` folder (stuck is the lock
|
|
830
|
+
// `state: stuck`; the body rests in `backlog/`), so a recovery `complete`'s
|
|
831
|
+
// kept branch carries NO historical `in-progress → needs-attention`
|
|
832
|
+
// move-only commit to drop and `<arbiter>/main` holds no surface move to
|
|
833
|
+
// conflict with. The old recovery drop (`rebaseDroppingNeedsAttentionSurface`,
|
|
834
|
+
// drop-bookkeeping-rebase) is deleted; recovery is now the SAME plain replay
|
|
835
|
+
// onto `<arbiter>/main` as any other build (see the plain-rebase note below).
|
|
836
|
+
// Fetch the arbiter's `main` into the `<arbiter>/main` remote-tracking ref
|
|
837
|
+
// EXPLICITLY. A `run` JOB WORKTREE is cut from a bare hub mirror whose remote
|
|
838
|
+
// has no fetch refspec (so `<arbiter>/main` would not otherwise resolve / would
|
|
839
|
+
// be stale, causing a spurious rebase conflict); a regular clone (`do`/
|
|
840
|
+
// `complete`) already has it, where the explicit refspec is harmless (the same
|
|
841
|
+
// refspec `rebaseOntoArbiterMain` used before the convergence).
|
|
842
|
+
await gitHard([
|
|
843
|
+
'fetch',
|
|
844
|
+
'--quiet',
|
|
845
|
+
arbiter,
|
|
846
|
+
`+refs/heads/main:refs/remotes/${arbiter}/main`,
|
|
847
|
+
], cwd, env);
|
|
848
|
+
// ONE-SLUG-ONE-FOLDER guard + divergent-base PRE-CHECK, read from the
|
|
849
|
+
// freshly-fetched `<arbiter>/main` (a READ of the tracking ref the fetch above
|
|
850
|
+
// just populated — NO new fetch, so no shared-mirror race). It (a) FAILS LOUD if
|
|
851
|
+
// the arbiter already holds the slug in >1 status folder with differing content
|
|
852
|
+
// (a corrupt ledger; never publish over it), and (b) detects the DIVERGENT base
|
|
853
|
+
// — the arbiter holds the slug's source in a DIFFERENT folder than our local
|
|
854
|
+
// done-move removed — which is the case that turns the rebased "move" into a
|
|
855
|
+
// "copy" (PR #86). The tasking lifecycle is exempt (its move is not a task
|
|
856
|
+
// done-move).
|
|
857
|
+
// CONTINUE-BUILD EXEMPTION (task
|
|
858
|
+
// `complete-builds-on-already-done-moved-continue`): on `source: 'done'`
|
|
859
|
+
// there was no first-time move on this commit (the slug was already in
|
|
860
|
+
// `done/` on the kept branch + on the arbiter), so the divergent-done-move
|
|
861
|
+
// reconcile reasoning does not apply. Skip the arbiter ledger placement
|
|
862
|
+
// pre-check too: it is structurally an instrument FOR that reconcile, and a
|
|
863
|
+
// continue-build is by construction already in `done/` on both sides.
|
|
864
|
+
if (!lifecycle && source !== 'done') {
|
|
865
|
+
const arbiterPlacement = readArbiterLedgerPlacement(cwd, arbiter, slug, env);
|
|
866
|
+
if (arbiterPlacement.error) {
|
|
867
|
+
note(arbiterPlacement.error);
|
|
868
|
+
return {
|
|
869
|
+
route: {
|
|
870
|
+
outcome: 'invariant-violation',
|
|
871
|
+
routedToNeedsAttention: false,
|
|
872
|
+
branch,
|
|
873
|
+
reason: arbiterPlacement.error,
|
|
874
|
+
},
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
// PLAIN rebase. After the per-item-lock cut-over (spec
|
|
879
|
+
// `ledger-status-per-item-lock-refs`, tasks 9a–9d) no transient status
|
|
880
|
+
// lands on a work branch: needs-attention is the lock `state: stuck` (not a
|
|
881
|
+
// `git mv` to `needs-attention/`), the body rests in `backlog/` while
|
|
882
|
+
// claimed, and the tasking/advancing markers are gone. So a recovery
|
|
883
|
+
// complete's kept branch carries NO historical route-to-needs-attention
|
|
884
|
+
// move-only commit to drop — the old `rebaseDroppingNeedsAttentionSurface`
|
|
885
|
+
// (drop-bookkeeping-rebase) is deleted and BOTH recovering and lifecycle
|
|
886
|
+
// rebases are the same plain replay onto `<arbiter>/main`.
|
|
887
|
+
// RENAME-DETECTION-OFF (task
|
|
888
|
+
// `disable-rename-detection-on-continue-rebase`): scope
|
|
889
|
+
// `-c merge.directoryRenames=false` to THIS rebase invocation, so a single
|
|
890
|
+
// durable folder-transition `git mv` out of a SPARSE work/<from>/ folder is
|
|
891
|
+
// NOT misread by git's directory-rename heuristic as a whole-DIRECTORY
|
|
892
|
+
// rename `work/<from>/ → work/<to>/` (which would spuriously flag every
|
|
893
|
+
// sibling file `<arbiter>/main` added into that folder as `CONFLICT (file
|
|
894
|
+
// location)` and force a FALSE needs-attention). Content-rename detection
|
|
895
|
+
// (`-Xno-renames`/`merge.renames`/`diff.renames`) is the wrong knob and
|
|
896
|
+
// does NOT suppress this directory-rename conflict; only
|
|
897
|
+
// `merge.directoryRenames=false` does. NEVER a persistent `git config`
|
|
898
|
+
// write — the repo's config stays clean so a user's interactive
|
|
899
|
+
// `git rebase` is unaffected. A GENUINE content conflict still surfaces
|
|
900
|
+
// and still routes via `rebaseConflictRoute(ctx)` below.
|
|
901
|
+
const rebase = await gitSoft(['-c', 'merge.directoryRenames=false', 'rebase', `${arbiter}/main`], cwd, env);
|
|
902
|
+
if (rebase.status !== 0) {
|
|
903
|
+
// NEVER auto-resolve a genuine CODE conflict. But FIRST, a SIBLING-SLUG
|
|
904
|
+
// LEDGER conflict (the replay conflicts ONLY on OTHER slugs'
|
|
905
|
+
// `work/<status>/<otherslug>.md` ledger files — a sibling job landed its own
|
|
906
|
+
// status-folder move on `<arbiter>/main` between our base and this rebase) is
|
|
907
|
+
// a benign ledger-only divergence with NO semantic judgement: the reconcile
|
|
908
|
+
// ABORTS the rebase, then redoes OUR work as one clean commit on top of
|
|
909
|
+
// `<arbiter>/main` (taking the arbiter's version of every sibling ledger file
|
|
910
|
+
// automatically). It is scoped STRICTLY to other slugs' ledger files — a
|
|
911
|
+
// conflict touching ANY code file, or THIS slug's own ledger, returns `false`
|
|
912
|
+
// (and leaves the rebase in progress), so the divergent-done-move recovery /
|
|
913
|
+
// needs-attention route below handles it; it NEVER widens to code. The tasking
|
|
914
|
+
// lifecycle is exempt (its move is not a task done-move).
|
|
915
|
+
const siblingReconciled = lifecycle
|
|
916
|
+
? false
|
|
917
|
+
: await reconcileSiblingLedgerConflict({
|
|
1013
918
|
cwd,
|
|
919
|
+
arbiter,
|
|
920
|
+
slug,
|
|
1014
921
|
env,
|
|
922
|
+
note,
|
|
1015
923
|
});
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
// false-contention budget). Route to needs-attention rather than looping
|
|
1024
|
-
// forever or force-pushing main. A RARE outcome under realistic load thanks
|
|
1025
|
-
// to the jitter below + the rebase-until-real-conflict semantics; tests
|
|
1026
|
-
// reach it deterministically by injecting a small `mergeRetries`.
|
|
1027
|
-
return await mergeNonFastForwardRoute(`integrating ${branch} onto ${arbiter}/main kept hitting a ` +
|
|
1028
|
-
`non-fast-forward push (a sibling advanced main ${mergeAttempt + 1} ` +
|
|
1029
|
-
`times); gave up cleanly without --force`);
|
|
1030
|
-
}
|
|
1031
|
-
// Modest jitter on the refetch (C2): an instant lockstep refetch→re-push loop
|
|
1032
|
-
// maximises mutual rejection under sustained parallel load (thundering herd).
|
|
1033
|
-
// A uniformly-random `[0, mergeJitterMs]` ms sleep desynchronises the herd.
|
|
1034
|
-
// Skipped when `mergeJitterMs === 0` (the test seam).
|
|
1035
|
-
if (mergeJitterMs > 0) {
|
|
1036
|
-
await sleepMs(Math.floor(Math.random() * (mergeJitterMs + 1)));
|
|
1037
|
-
}
|
|
1038
|
-
// A sibling advanced main: re-run the step-4 rebase (with the reconcile arms)
|
|
1039
|
-
// before retrying the push. A genuine conflict on the re-rebase routes via
|
|
1040
|
-
// `rebaseOntoMainWithReconcile`'s `route` (the existing source-folder /
|
|
1041
|
-
// one-slug placement recheck IS the genuine-conflict terminator — see the
|
|
1042
|
-
// `DEFAULT_MERGE_RETRIES` docstring for the C2 SCOPE box). A clean re-rebase
|
|
1043
|
-
// (no route) loops without counting against a small budget.
|
|
1044
|
-
const reRebase = await rebaseOntoMainWithReconcile();
|
|
1045
|
-
if (reRebase.route) {
|
|
1046
|
-
return reRebase.route;
|
|
1047
|
-
}
|
|
924
|
+
if (siblingReconciled) {
|
|
925
|
+
// The branch is now cleanly on top of `<arbiter>/main` with OUR work + the
|
|
926
|
+
// arbiter's sibling-ledger files — fall through to the fresh-gate + integrate
|
|
927
|
+
// band. THIS slug's own move is untouched.
|
|
928
|
+
note(`Reconciled a sibling-slug ledger conflict during the rebase onto ` +
|
|
929
|
+
`${arbiter}/main (took the arbiter's version of the other slugs' ` +
|
|
930
|
+
`work/<status>/<slug>.md ledger files; no code file was touched).`);
|
|
1048
931
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
932
|
+
else if (!lifecycle && source !== 'done') {
|
|
933
|
+
// NEVER auto-resolve a genuine CODE conflict: abort the rebase. But FIRST, a
|
|
934
|
+
// DIVERGENT-LEDGER conflict (the arbiter holds the slug's source in a folder
|
|
935
|
+
// our local done-move did not remove — PR #86) is auto-RECONCILABLE without
|
|
936
|
+
// any semantic judgement: redo the done-move arbiter-resolved (remove the
|
|
937
|
+
// arbiter's actual source folder, add `done/`) on top of `<arbiter>/main`. We
|
|
938
|
+
// only do this when the post-abort tree's ONLY divergence is the slug's ledger
|
|
939
|
+
// file; a real code conflict still routes to needs-attention untouched.
|
|
940
|
+
await gitSoft(['rebase', '--abort'], cwd, env);
|
|
941
|
+
const recovered = await reconcileDivergentDoneMove({
|
|
1056
942
|
cwd,
|
|
943
|
+
arbiter,
|
|
1057
944
|
slug,
|
|
1058
|
-
|
|
1059
|
-
|
|
945
|
+
branch,
|
|
946
|
+
localSource: source,
|
|
1060
947
|
env,
|
|
1061
948
|
note,
|
|
1062
949
|
});
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
`non-fast-forward push (a sibling advanced main); '${slug}' was ` +
|
|
1071
|
-
`marked stuck on its per-item lock (surfaced by status). Resolve ` +
|
|
1072
|
-
`against the latest main, then \`requeue\` to release the stuck ` +
|
|
1073
|
-
`lock and re-run.`
|
|
1074
|
-
: `Integrating ${branch} onto ${arbiter}/main kept hitting a ` +
|
|
1075
|
-
`non-fast-forward push (a sibling advanced main). Resolve against the ` +
|
|
1076
|
-
`latest main, then re-run complete.`,
|
|
1077
|
-
};
|
|
1078
|
-
}
|
|
1079
|
-
// 6. Make the Gate-2 review VISIBLE on the PR (task `review-comment-prose-field`,
|
|
1080
|
-
// refining `review-gate-pr-comment`): AFTER the propose integrate, where the
|
|
1081
|
-
// approved verdict (with its deliberately-authored `review` prose), the
|
|
1082
|
-
// resolved `provider`, AND the opened PR url (`integration.url`) are ALL in
|
|
1083
|
-
// scope, post `verdict.review` as a comment on that PR — INCLUDING on approve
|
|
1084
|
-
// (the audit trail; decided 2026-06-06). The `review` field is a first-class
|
|
1085
|
-
// AUTHORED review (the prompt requires it), NOT the residue around the JSON
|
|
1086
|
-
// — posting the residue was the bug
|
|
1087
|
-
// (`work/findings/review-comment-posts-agent-thinking-not-a-review.md`). It
|
|
1088
|
-
// reuses the SAME `provider` the integrate used (the core never imports `gh`).
|
|
1089
|
-
// The comment is ADVISORY: it changes no gate/verdict/merge/integration logic
|
|
1090
|
-
// — by here the verdict has ALREADY routed (block never reaches this point; it
|
|
1091
|
-
// routed to needs-attention above) and the integrate has ALREADY happened.
|
|
1092
|
-
// The PR identity is resolved in PRECEDENCE: a parsed `integration.url` wins
|
|
1093
|
-
// (the normal path — post on it directly); else, when a PR WAS opened but its
|
|
1094
|
-
// url was unparseable (`integration.requestOpened` true, `url` undefined — the
|
|
1095
|
-
// `gh pr create` exit-0-but-unparseable-stdout degradation), FALL BACK to the
|
|
1096
|
-
// BRANCH-resolved comment (task `review-comment-fallback-on-unparsed-pr-url`):
|
|
1097
|
-
// the provider resolves the branch's open PR and comments on it, instead of
|
|
1098
|
-
// silently dropping a review on a PR that genuinely exists. Only when NO PR was
|
|
1099
|
-
// opened at all (merge mode, or a degraded/push-only propose ⇒ `requestOpened`
|
|
1100
|
-
// false) is it the honest clean no-op — and the branch-resolved fallback's own
|
|
1101
|
-
// "no PR resolvable" path is a clean no-op too (it tries first). Either way
|
|
1102
|
-
// `postPRComment*` never throws; the review stays in the run output. Because
|
|
1103
|
-
// this lives in the shared core, BOTH `do`/`complete` AND `run` post the
|
|
1104
|
-
// comment — no per-caller wiring.
|
|
1105
|
-
if (approvedVerdict?.review !== undefined) {
|
|
1106
|
-
if (integration.url !== undefined) {
|
|
1107
|
-
const posted = provider.postPRComment({
|
|
1108
|
-
cwd,
|
|
1109
|
-
url: integration.url,
|
|
1110
|
-
body: approvedVerdict.review,
|
|
1111
|
-
env,
|
|
1112
|
-
});
|
|
1113
|
-
note(posted.instruction);
|
|
950
|
+
if (recovered) {
|
|
951
|
+
// The branch is now cleanly on top of `<arbiter>/main` with the slug in
|
|
952
|
+
// `done/` ONLY — fall through to integrate (skip the needs-attention
|
|
953
|
+
// route below).
|
|
954
|
+
note(`Reconciled the done-move against ${arbiter}/main: '${slug}' is in ` +
|
|
955
|
+
'work/done/ ONLY (the divergent source folder was removed; the move ' +
|
|
956
|
+
'is a move, not a copy).');
|
|
1114
957
|
}
|
|
1115
|
-
else
|
|
1116
|
-
|
|
1117
|
-
// rather than dropping the review (the audit-trail fallback).
|
|
1118
|
-
const posted = provider.postPRCommentOnBranch({
|
|
1119
|
-
cwd,
|
|
1120
|
-
branch,
|
|
1121
|
-
body: approvedVerdict.review,
|
|
1122
|
-
env,
|
|
1123
|
-
});
|
|
1124
|
-
note(posted.instruction);
|
|
958
|
+
else {
|
|
959
|
+
return { route: await rebaseConflictRoute(ctx) };
|
|
1125
960
|
}
|
|
1126
961
|
}
|
|
962
|
+
else {
|
|
963
|
+
await gitSoft(['rebase', '--abort'], cwd, env);
|
|
964
|
+
return { route: await rebaseConflictRoute(ctx) };
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
// Clean rebase (or a reconciled one): fold any question residue the rebase
|
|
968
|
+
// brought in into the tip, then fall through to the gate + integrate.
|
|
969
|
+
if (!lifecycle) {
|
|
970
|
+
await foldQuestionResidueIntoTip(ctx);
|
|
971
|
+
}
|
|
972
|
+
return {};
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* After a rebase onto `<arbiter>/main`, CLEAR the landing task's own stale
|
|
976
|
+
* question state that the rebase BROUGHT IN, and AMEND it into the tip commit
|
|
977
|
+
* (the completion commit), so the land still carries it in the done-move commit
|
|
978
|
+
* itself (task `a-recovered-task-lands-without-stale-question-state`).
|
|
979
|
+
*
|
|
980
|
+
* This is the half step 2c cannot see: a kept branch cut BEFORE the item was
|
|
981
|
+
* surfaced (a re-dispatch continuing from the kept branch, a stranded-branch
|
|
982
|
+
* finish) replays its `git mv ... → tasks/done/` over a `main` whose body now
|
|
983
|
+
* carries `needsAnswers: true` and a sidecar, and git carries the flag into
|
|
984
|
+
* `tasks/done/` and keeps the sidecar. Runs on every rebase of the land: the
|
|
985
|
+
* build path's first rebase, the merge CAS loop's re-rebase (so the CI apply
|
|
986
|
+
* phase is covered), and the stranded-recovery tail.
|
|
987
|
+
*
|
|
988
|
+
* A no-op (no amend) when there is nothing to clear, and when the tip is already
|
|
989
|
+
* on `main` (never rewrite a commit that is not ours). Best-effort: a fault is
|
|
990
|
+
* reported and the land proceeds; the claim-time reconcile remains the backstop.
|
|
991
|
+
*/
|
|
992
|
+
async function foldQuestionResidueIntoTip(params) {
|
|
993
|
+
const { cwd, arbiter, slug, env, note } = params;
|
|
994
|
+
try {
|
|
995
|
+
if (isAncestor(cwd, 'HEAD', `${arbiter}/main`, env)) {
|
|
996
|
+
return; // no commit of ours on top of main to fold into.
|
|
997
|
+
}
|
|
998
|
+
const cleared = clearOwnQuestionResidueOnLand({ cwd, slug, note });
|
|
999
|
+
if (cleared.changed.length === 0) {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
await gitHard(['add', '-A', '--', ...cleared.changed], cwd, env);
|
|
1003
|
+
await gitHard(['commit', '-q', '--amend', '--no-edit'], cwd, env);
|
|
1004
|
+
}
|
|
1005
|
+
catch (err) {
|
|
1006
|
+
note(`Could not fold the stale question state of '${slug}' into its land ` +
|
|
1007
|
+
`(${err instanceof Error ? err.message : String(err)}); the next ` +
|
|
1008
|
+
'claim reconcile will drain it.');
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
// The rebase-conflict needs-attention route, factored so the divergent-ledger
|
|
1012
|
+
// recovery above can fall through to integrate while a genuine code conflict
|
|
1013
|
+
// still routes here.
|
|
1014
|
+
async function rebaseConflictRoute(ctx) {
|
|
1015
|
+
const { cwd, arbiter, slug, branch, commitMessage, env, note } = ctx;
|
|
1016
|
+
// Then mark the item stuck on its per-item lock with the conflict reason
|
|
1017
|
+
// (ADR §12) THROUGH the ledger write seam's needs-attention transition,
|
|
1018
|
+
// rather than leaving it dangling. Post lock-cutover this is a lock amend
|
|
1019
|
+
// (`state: stuck` + the reason on the lock entry, no `done/ →
|
|
1020
|
+
// needs-attention/` folder move); the done-move already committed above
|
|
1021
|
+
// stands. No partial state.
|
|
1022
|
+
const reason = `rebase onto ${arbiter}/main conflicted (aborted, never auto-resolved)`;
|
|
1023
|
+
const routed = await ledgerWrite.applyNeedsAttentionTransition({
|
|
1024
|
+
cwd,
|
|
1025
|
+
slug,
|
|
1026
|
+
reason,
|
|
1027
|
+
// Autonomous caller (`do`) passes the arbiter so the seam both surfaces the
|
|
1028
|
+
// conflict on `main` (OBSERVABLE) AND pushes the `work/<slug>` branch
|
|
1029
|
+
// (RECOVERABLE, cross-machine). The human `complete` leaves it unset →
|
|
1030
|
+
// no surface, no push, local-only. The push lives in the seam (HEAD's
|
|
1031
|
+
// branch is `work/<slug>` here) — no bolted-on push.
|
|
1032
|
+
arbiter: ctx.surfaceArbiter,
|
|
1033
|
+
env,
|
|
1034
|
+
note,
|
|
1035
|
+
});
|
|
1036
|
+
return {
|
|
1037
|
+
outcome: 'rebase-conflict',
|
|
1038
|
+
routedToNeedsAttention: routed.moved,
|
|
1039
|
+
branch,
|
|
1040
|
+
commitMessage,
|
|
1041
|
+
reason: routed.moved
|
|
1042
|
+
? `Rebasing ${branch} onto ${arbiter}/main conflicted; the rebase was ` +
|
|
1043
|
+
`aborted (never auto-resolved) and '${slug}' was marked stuck on ` +
|
|
1044
|
+
'its per-item lock (surfaced by status). Resolve against the latest ' +
|
|
1045
|
+
'main, then `requeue` to release the stuck lock and re-run.'
|
|
1046
|
+
: `Rebasing ${branch} onto ${arbiter}/main conflicted; the rebase was ` +
|
|
1047
|
+
'aborted (never auto-resolved). Resolve against the latest main, ' +
|
|
1048
|
+
'then re-run complete.',
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* **The LAND half of the rebase-to-integrate tail** (task `ci-split-build-path`,
|
|
1053
|
+
* spec `ci-agent-job-without-write-token` §8): integrate per mode through
|
|
1054
|
+
* `ledgerWrite.applyCompleteTransition`, with the merge-mode compare-and-swap
|
|
1055
|
+
* loop (a non-fast-forward push re-runs {@link rebaseOntoMainWithReconcile} and
|
|
1056
|
+
* retries, never `--force`), then post the approved Gate-2 review prose on the
|
|
1057
|
+
* opened PR. It launches no agent and runs no `prepare` / `verify`: the fresh
|
|
1058
|
+
* gate ran once, before it, in the other half. `performIntegration` reaches it
|
|
1059
|
+
* through the {@link integrationLand} seam, so the CI agent phase RECORDS this
|
|
1060
|
+
* call (the first write of the success path) and the apply phase resumes here
|
|
1061
|
+
* with trusted inputs and the validated work-branch tip.
|
|
1062
|
+
*/
|
|
1063
|
+
export async function landIntegration(input) {
|
|
1064
|
+
const { cwd, arbiter, slug, branch, mode, commitMessage, env } = input;
|
|
1065
|
+
const note = input.note;
|
|
1066
|
+
// 5. Integrate per mode through the ledger write seam's COMPLETE transition
|
|
1067
|
+
// (ADR §6 + `docs/adr/claim-ledger-vs-protected-main.md`). The rebase above
|
|
1068
|
+
// already brought the branch up to date, so the seam's sole strategy uses
|
|
1069
|
+
// `integrate` (not `integrateWithRebase`) and never --forces. Provider
|
|
1070
|
+
// selection: an injected `openPr` wins (legacy bridge); otherwise pick by
|
|
1071
|
+
// the arbiter's remote URL (a GitHub remote ⇒ `gh pr create`, else push-only
|
|
1072
|
+
// `none`) — PURELY arbiter-derived, no override axis. A missing/unauthenticated
|
|
1073
|
+
// `gh` degrades to push-only at runtime — never a hard failure (and the
|
|
1074
|
+
// start-of-run unauthed case is caught UP FRONT by the pre-flight `gh` probe).
|
|
1075
|
+
// The seam is storage-agnostic: we hand it the work branch, the integration
|
|
1076
|
+
// mode, the provider, and the PR-INTENT (`noPR`) — `main` lives only in the
|
|
1077
|
+
// strategy.
|
|
1078
|
+
// Provider precedence: an injected fully-formed provider wins (the `run`
|
|
1079
|
+
// stubbed-provider seam, carrying title/body/url); else the legacy `openPr`
|
|
1080
|
+
// bridge; else select PURELY from the arbiter URL (no override). The orthogonal
|
|
1081
|
+
// `noPR` INTENT (suppress the PR) is threaded SEPARATELY — it does NOT pick a
|
|
1082
|
+
// provider, the integrator simply skips `openRequest` when it is set.
|
|
1083
|
+
const provider = input.providerInstance ??
|
|
1084
|
+
(input.openPr
|
|
1085
|
+
? bridgeProvider(input.openPr)
|
|
1086
|
+
: selectProvider({
|
|
1087
|
+
arbiterUrl: await arbiterUrl(cwd, arbiter, env),
|
|
1088
|
+
}));
|
|
1089
|
+
// Race-1 (claim-vs-integrate, task
|
|
1090
|
+
// `run-fleet-claim-integrate-and-sibling-rebase-concurrency-safe`): integrate,
|
|
1091
|
+
// and on a non-fast-forward `${branch}:main` push (a SIBLING same-repo CLAIM —
|
|
1092
|
+
// under the SEPARATE claim lock — or a sibling integrate advanced
|
|
1093
|
+
// `<arbiter>/main` during our push window) RE-RUN the step-4 rebase (which
|
|
1094
|
+
// carries the sibling-ledger + divergent-done-move reconcile arms — a bare
|
|
1095
|
+
// re-rebase would MISS them) and RETRY the push, up to a small cap. INSTANT
|
|
1096
|
+
// retry (contention, not an outage; see `claim-cas.ts`). We NEVER `--force`
|
|
1097
|
+
// main: each retry re-rebases to a clean fast-forward. A genuine code conflict
|
|
1098
|
+
// on a re-rebase routes to needs-attention via the SAME `route` the up-front
|
|
1099
|
+
// rebase uses. A persistent non-fast-forward past the cap also routes (never a
|
|
1100
|
+
// silent drop). `input.mergeRetries` overrides the cap (tests; `0` ⇒ no retry).
|
|
1101
|
+
//
|
|
1102
|
+
// **C2 rebase-until-real-conflict (task `c2-rebase-until-real-on-durable-main-
|
|
1103
|
+
// promotions`):** the loop's TERMINATION CHANGED. A CLEAN re-rebase no longer
|
|
1104
|
+
// counts against a tiny give-up budget — only a GENUINE conflict surfaced by
|
|
1105
|
+
// `rebaseOntoMainWithReconcile` (a `route` ⇒ `rebase-conflict` or
|
|
1106
|
+
// `invariant-violation`) stops the loop. The step-4 rebase IS the source-folder
|
|
1107
|
+
// precondition recheck reused verbatim: if the slug is GONE from its expected
|
|
1108
|
+
// source folder on the new `main` (a concurrent legitimate same-item winner
|
|
1109
|
+
// already moved it), the `git mv` replay fails and `rebaseConflictRoute` routes
|
|
1110
|
+
// definitively — never a silent re-push that would clobber the winner. The
|
|
1111
|
+
// `maxMergeRetries` cap survives ONLY as a large liveness ceiling on the
|
|
1112
|
+
// pathological livelock tail (default {@link DEFAULT_MERGE_RETRIES} = 1000);
|
|
1113
|
+
// modest jitter on the refetch desynchronises a herd so the tail is not
|
|
1114
|
+
// reached under sustained parallel load.
|
|
1115
|
+
const maxMergeRetries = input.mergeRetries ?? DEFAULT_MERGE_RETRIES;
|
|
1116
|
+
const mergeJitterMs = input.mergeJitterMs ?? DEFAULT_MERGE_JITTER_MS;
|
|
1117
|
+
// The landed-vs-gated report (ADR `ci-agent-job-holds-no-write-token`
|
|
1118
|
+
// decision 2, task `ci-split-landed-vs-gated-report`), scoped to the CI apply
|
|
1119
|
+
// phase: only it sets `gatedTip` (the validated bundle tip the agent job
|
|
1120
|
+
// gated). The CAS loop below never re-gates, so a re-rebase after a lost race
|
|
1121
|
+
// can land a tree the gate never saw: {@link stampLandedWithoutRegate}
|
|
1122
|
+
// records that on the landed commit. Without `gatedTip` (every non-phase
|
|
1123
|
+
// land) the loop is exactly today's: no tree compare, no message rewrite.
|
|
1124
|
+
const gatedTree = input.gatedTip === undefined
|
|
1125
|
+
? undefined
|
|
1126
|
+
: await treeOf(cwd, input.gatedTip, env);
|
|
1127
|
+
let regateReport;
|
|
1128
|
+
let integration;
|
|
1129
|
+
for (let mergeAttempt = 0;; mergeAttempt++) {
|
|
1130
|
+
integration = await ledgerWrite.applyCompleteTransition({
|
|
1131
|
+
arbiter,
|
|
1132
|
+
branch,
|
|
1133
|
+
mode,
|
|
1134
|
+
provider,
|
|
1135
|
+
// PR-INTENT: when set (propose mode), push the branch but skip the PR.
|
|
1136
|
+
noPR: input.noPR,
|
|
1137
|
+
// Half A: an explicit single-line PR title (propose mode), so `gh` no longer
|
|
1138
|
+
// derives a run-on title from the commit subject via `--fill`.
|
|
1139
|
+
title: input.title,
|
|
1140
|
+
// Half B: the propose-mode PR body — the agent's summary under a deterministic
|
|
1141
|
+
// runner header (task pointer). Undefined when no body was supplied (the
|
|
1142
|
+
// header is only scaffolded when there IS a body) ⇒ today's `--fill` (no
|
|
1143
|
+
// regression). Ignored in merge mode by the provider/integrator.
|
|
1144
|
+
body: input.body,
|
|
1145
|
+
// Part (b) of the merged-branch hygiene task: when WE perform the merge
|
|
1146
|
+
// (this resolved `merge` mode), reap the remote `work/<slug>` HEAD branch
|
|
1147
|
+
// INLINE right after the merge lands — the commits are now on `main`, so the
|
|
1148
|
+
// head is provably merged and safe to delete (ancestor-guarded inside the
|
|
1149
|
+
// integrator). Idempotent no-op when no remote head exists (the plain
|
|
1150
|
+
// `${branch}:main` push opened none); ignored in `propose` mode (its branch is
|
|
1151
|
+
// the review surface, reaped later by `gc --remote-branches`). NEVER `--force`.
|
|
1152
|
+
deleteMergedHead: true,
|
|
1153
|
+
cwd,
|
|
1154
|
+
env,
|
|
1155
|
+
});
|
|
1156
|
+
// Only the merge push can be non-fast-forward (propose pushes its own ref).
|
|
1157
|
+
if (integration.mergeNonFastForward !== true) {
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
if (mergeAttempt >= maxMergeRetries) {
|
|
1161
|
+
// LIVENESS CEILING hit (default 1000; previously the small Race-1 cap of 5,
|
|
1162
|
+
// now reinterpreted by C2 as the pathological-livelock-tail bound, NOT a
|
|
1163
|
+
// false-contention budget). Route to needs-attention rather than looping
|
|
1164
|
+
// forever or force-pushing main. A RARE outcome under realistic load thanks
|
|
1165
|
+
// to the jitter below + the rebase-until-real-conflict semantics; tests
|
|
1166
|
+
// reach it deterministically by injecting a small `mergeRetries`.
|
|
1167
|
+
return await mergeNonFastForwardRoute(`integrating ${branch} onto ${arbiter}/main kept hitting a ` +
|
|
1168
|
+
`non-fast-forward push (a sibling advanced main ${mergeAttempt + 1} ` +
|
|
1169
|
+
`times); gave up cleanly without --force`);
|
|
1170
|
+
}
|
|
1171
|
+
// Modest jitter on the refetch (C2): an instant lockstep refetch→re-push loop
|
|
1172
|
+
// maximises mutual rejection under sustained parallel load (thundering herd).
|
|
1173
|
+
// A uniformly-random `[0, mergeJitterMs]` ms sleep desynchronises the herd.
|
|
1174
|
+
// Skipped when `mergeJitterMs === 0` (the test seam).
|
|
1175
|
+
if (mergeJitterMs > 0) {
|
|
1176
|
+
await sleepMs(Math.floor(Math.random() * (mergeJitterMs + 1)));
|
|
1177
|
+
}
|
|
1178
|
+
// A sibling advanced main: re-run the step-4 rebase (with the reconcile arms)
|
|
1179
|
+
// before retrying the push. A genuine conflict on the re-rebase routes via
|
|
1180
|
+
// `rebaseOntoMainWithReconcile`'s `route` (the existing source-folder /
|
|
1181
|
+
// one-slug placement recheck IS the genuine-conflict terminator — see the
|
|
1182
|
+
// `DEFAULT_MERGE_RETRIES` docstring for the C2 SCOPE box). A clean re-rebase
|
|
1183
|
+
// (no route) loops without counting against a small budget.
|
|
1184
|
+
const reRebase = await rebaseOntoMainWithReconcile(input);
|
|
1185
|
+
if (reRebase.route) {
|
|
1186
|
+
return reRebase.route;
|
|
1187
|
+
}
|
|
1188
|
+
if (gatedTree !== undefined) {
|
|
1189
|
+
regateReport = await stampLandedWithoutRegate({
|
|
1190
|
+
cwd,
|
|
1191
|
+
branch,
|
|
1192
|
+
gatedTree,
|
|
1193
|
+
lostRaces: mergeAttempt + 1,
|
|
1194
|
+
env,
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if (regateReport !== undefined && integration.mergedToMain === true) {
|
|
1199
|
+
note(`${branch} ${regateReport}.`);
|
|
1200
|
+
}
|
|
1201
|
+
noteRequestNotOpened(integration, branch, note);
|
|
1202
|
+
// The Race-1 needs-attention route for a merge that could not land (a genuine
|
|
1203
|
+
// re-rebase conflict is handled by `rebaseOntoMainWithReconcile`'s `route`; this
|
|
1204
|
+
// covers the cap-exhausted persistent-contention case). Mirrors
|
|
1205
|
+
// `rebaseConflictRoute`: the done-move was already committed (steps 2–3), so the
|
|
1206
|
+
// slug sits in work/done/ and the seam bounces it from there.
|
|
1207
|
+
async function mergeNonFastForwardRoute(reason) {
|
|
1208
|
+
const routed = await ledgerWrite.applyNeedsAttentionTransition({
|
|
1209
|
+
cwd,
|
|
1210
|
+
slug,
|
|
1211
|
+
reason,
|
|
1212
|
+
arbiter: input.surfaceArbiter,
|
|
1213
|
+
env,
|
|
1214
|
+
note,
|
|
1215
|
+
});
|
|
1127
1216
|
return {
|
|
1128
|
-
outcome: '
|
|
1129
|
-
routedToNeedsAttention:
|
|
1217
|
+
outcome: 'rebase-conflict',
|
|
1218
|
+
routedToNeedsAttention: routed.moved,
|
|
1130
1219
|
branch,
|
|
1131
1220
|
commitMessage,
|
|
1132
|
-
|
|
1221
|
+
reason: routed.moved
|
|
1222
|
+
? `Integrating ${branch} onto ${arbiter}/main kept hitting a ` +
|
|
1223
|
+
`non-fast-forward push (a sibling advanced main); '${slug}' was ` +
|
|
1224
|
+
`marked stuck on its per-item lock (surfaced by status). Resolve ` +
|
|
1225
|
+
`against the latest main, then \`requeue\` to release the stuck ` +
|
|
1226
|
+
`lock and re-run.`
|
|
1227
|
+
: `Integrating ${branch} onto ${arbiter}/main kept hitting a ` +
|
|
1228
|
+
`non-fast-forward push (a sibling advanced main). Resolve against the ` +
|
|
1229
|
+
`latest main, then re-run complete.`,
|
|
1133
1230
|
};
|
|
1231
|
+
}
|
|
1232
|
+
// 6. Make the Gate-2 review VISIBLE on the PR (task `review-comment-prose-field`,
|
|
1233
|
+
// refining `review-gate-pr-comment`): AFTER the propose integrate, where the
|
|
1234
|
+
// approved verdict (with its deliberately-authored `review` prose), the
|
|
1235
|
+
// resolved `provider`, AND the opened PR url (`integration.url`) are ALL in
|
|
1236
|
+
// scope, post `verdict.review` as a comment on that PR — INCLUDING on approve
|
|
1237
|
+
// (the audit trail; decided 2026-06-06). The `review` field is a first-class
|
|
1238
|
+
// AUTHORED review (the prompt requires it), NOT the residue around the JSON
|
|
1239
|
+
// — posting the residue was the bug
|
|
1240
|
+
// (`work/findings/review-comment-posts-agent-thinking-not-a-review.md`). It
|
|
1241
|
+
// reuses the SAME `provider` the integrate used (the core never imports `gh`).
|
|
1242
|
+
// The comment is ADVISORY: it changes no gate/verdict/merge/integration logic
|
|
1243
|
+
// — by here the verdict has ALREADY routed (block never reaches this point; it
|
|
1244
|
+
// routed to needs-attention above) and the integrate has ALREADY happened.
|
|
1245
|
+
// The PR identity is resolved in PRECEDENCE: a parsed `integration.url` wins
|
|
1246
|
+
// (the normal path — post on it directly); else, when a PR WAS opened but its
|
|
1247
|
+
// url was unparseable (`integration.requestOpened` true, `url` undefined — the
|
|
1248
|
+
// `gh pr create` exit-0-but-unparseable-stdout degradation), FALL BACK to the
|
|
1249
|
+
// BRANCH-resolved comment (task `review-comment-fallback-on-unparsed-pr-url`):
|
|
1250
|
+
// the provider resolves the branch's open PR and comments on it, instead of
|
|
1251
|
+
// silently dropping a review on a PR that genuinely exists. Only when NO PR was
|
|
1252
|
+
// opened at all (merge mode, or a degraded/push-only propose ⇒ `requestOpened`
|
|
1253
|
+
// false) is it the honest clean no-op — and the branch-resolved fallback's own
|
|
1254
|
+
// "no PR resolvable" path is a clean no-op too (it tries first). Either way
|
|
1255
|
+
// `postPRComment*` never throws; the review stays in the run output. Because
|
|
1256
|
+
// this lives in the shared core, BOTH `do`/`complete` AND `run` post the
|
|
1257
|
+
// comment — no per-caller wiring.
|
|
1258
|
+
if (input.reviewProse !== undefined) {
|
|
1259
|
+
if (integration.url !== undefined) {
|
|
1260
|
+
const posted = provider.postPRComment({
|
|
1261
|
+
cwd,
|
|
1262
|
+
url: integration.url,
|
|
1263
|
+
body: input.reviewProse,
|
|
1264
|
+
env,
|
|
1265
|
+
});
|
|
1266
|
+
note(posted.instruction);
|
|
1267
|
+
}
|
|
1268
|
+
else if (integration.requestOpened) {
|
|
1269
|
+
// A PR opened but its url was unparseable — resolve it from the branch
|
|
1270
|
+
// rather than dropping the review (the audit-trail fallback).
|
|
1271
|
+
const posted = provider.postPRCommentOnBranch({
|
|
1272
|
+
cwd,
|
|
1273
|
+
branch,
|
|
1274
|
+
body: input.reviewProse,
|
|
1275
|
+
env,
|
|
1276
|
+
});
|
|
1277
|
+
note(posted.instruction);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return {
|
|
1281
|
+
outcome: 'completed',
|
|
1282
|
+
routedToNeedsAttention: false,
|
|
1283
|
+
branch,
|
|
1284
|
+
commitMessage,
|
|
1285
|
+
integration,
|
|
1134
1286
|
};
|
|
1135
|
-
// Serialise ONLY this tail per repo when the `run` seam is wired; absent ⇒ run
|
|
1136
|
-
// it directly (an un-contended no-op, single-job behaviour unchanged).
|
|
1137
|
-
return input.integrateLock && input.integrateLockKey !== undefined
|
|
1138
|
-
? await input.integrateLock(input.integrateLockKey, runRebaseToIntegrateTail)
|
|
1139
|
-
: await runRebaseToIntegrateTail();
|
|
1140
1287
|
}
|
|
1141
1288
|
/**
|
|
1142
1289
|
* RECOVER an already-committed, already-done-moved STRANDED branch (spec
|
|
@@ -1275,7 +1422,10 @@ async function recoverAlreadyCommitted(params) {
|
|
|
1275
1422
|
for (;;) {
|
|
1276
1423
|
const rebase = await gitSoft(rebaseArgs(), cwd, env);
|
|
1277
1424
|
if (rebase.status === 0) {
|
|
1278
|
-
|
|
1425
|
+
// Clean rebase ⇒ fold in any question residue it brought (the kept
|
|
1426
|
+
// branch predates a surface on `main`), then fall through to integrate.
|
|
1427
|
+
await foldQuestionResidueIntoTip({ cwd, arbiter, slug, env, note });
|
|
1428
|
+
break;
|
|
1279
1429
|
}
|
|
1280
1430
|
// ALWAYS abort on conflict — never leave mid-rebase between attempts.
|
|
1281
1431
|
await gitSoft(['rebase', '--abort'], cwd, env);
|
|
@@ -1451,6 +1601,7 @@ async function recoverAlreadyCommitted(params) {
|
|
|
1451
1601
|
: `Recovered '${slug}': integrated the kept commit from ${branch} ` +
|
|
1452
1602
|
`(absorbed a moving ${arbiter}/main across ${attempt} re-fetch+re-` +
|
|
1453
1603
|
`rebase attempt${attempt === 1 ? '' : 's'}).`);
|
|
1604
|
+
noteRequestNotOpened(integration, branch, note);
|
|
1454
1605
|
// Post the Gate-2 review as a PR comment (task `committed-recovery-always-
|
|
1455
1606
|
// reviews`): the SAME post-integrate audit trail the build path keeps — the
|
|
1456
1607
|
// approved verdict's authored `review` prose, on the opened PR. Advisory only
|
|
@@ -1485,6 +1636,21 @@ async function recoverAlreadyCommitted(params) {
|
|
|
1485
1636
|
integration,
|
|
1486
1637
|
};
|
|
1487
1638
|
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Say so when a propose land opened NO review request (task
|
|
1641
|
+
* `intake-reports-the-pr-it-actually-opened`): the branch is pushed, but the
|
|
1642
|
+
* provider could not (or was told not to) open a PR, so the log prints the
|
|
1643
|
+
* provider's instruction (the real `gh` error + the manual `gh pr create`
|
|
1644
|
+
* command) instead of leaving it unread. Every land (build, tasking, intake,
|
|
1645
|
+
* recovery) passes through here, so no caller has to remember to print it. A
|
|
1646
|
+
* merge land, an opened request and an already-landed no-op print nothing.
|
|
1647
|
+
*/
|
|
1648
|
+
function noteRequestNotOpened(integration, branch, note) {
|
|
1649
|
+
const reason = proposeRequestNotOpened(integration);
|
|
1650
|
+
if (reason !== undefined) {
|
|
1651
|
+
note(`No PR was opened for ${branch}: ${reason}`);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1488
1654
|
/**
|
|
1489
1655
|
* Resolve the work branch the integration runs on: the branch HEAD is currently
|
|
1490
1656
|
* on (the caller is ALWAYS on the work branch — the agent built there / the
|
|
@@ -1665,6 +1831,8 @@ export function synthesiseProposeTitle(input) {
|
|
|
1665
1831
|
* header that points a reviewer back to the task file. Returns `undefined` when
|
|
1666
1832
|
* no body was supplied — so the provider degrades to today's `gh ... --fill` (no
|
|
1667
1833
|
* regression); the header is ONLY scaffolded when there IS prose to carry.
|
|
1834
|
+
* `header` overrides the default task-build pointer (`Task: work/tasks/done/<slug>.md`)
|
|
1835
|
+
* for a non-task lifecycle (tasking, intake), whose item rests elsewhere.
|
|
1668
1836
|
* Exported for unit tests of the header + pointer.
|
|
1669
1837
|
*/
|
|
1670
1838
|
export function composeProposeBody(input) {
|
|
@@ -1672,9 +1840,18 @@ export function composeProposeBody(input) {
|
|
|
1672
1840
|
if (!prose) {
|
|
1673
1841
|
return undefined;
|
|
1674
1842
|
}
|
|
1675
|
-
const header =
|
|
1843
|
+
const header = input.header ??
|
|
1844
|
+
proposePointer('Task', workItemRel('done', `${input.slug}.md`));
|
|
1676
1845
|
return `${header}\n\n${prose}`;
|
|
1677
1846
|
}
|
|
1847
|
+
/**
|
|
1848
|
+
* The one-line reviewer pointer a propose PR body opens with: `<label>: `<path>``,
|
|
1849
|
+
* where `path` is the repo-relative file the PR carries (task
|
|
1850
|
+
* `propose-pr-bodies-point-at-the-real-item`).
|
|
1851
|
+
*/
|
|
1852
|
+
export function proposePointer(label, path) {
|
|
1853
|
+
return `${label}: \`${path}\``;
|
|
1854
|
+
}
|
|
1678
1855
|
/**
|
|
1679
1856
|
* On a review APPROVE that carries ≥1 NON-BLOCKING finding, write ONE per-run
|
|
1680
1857
|
* observation `work/notes/observations/review-nits-<slug>-<YYYY-MM-DD>.md` capturing all
|
|
@@ -1769,21 +1946,13 @@ function readTitle(content) {
|
|
|
1769
1946
|
const match = /^title\s*:\s*(.*)$/.exec(line);
|
|
1770
1947
|
if (match) {
|
|
1771
1948
|
const value = match[1].trim();
|
|
1772
|
-
|
|
1949
|
+
// The shared frontmatter unquoter, so a title written by
|
|
1950
|
+
// `quoteYamlScalar` (`''` for a literal `'`) reads back verbatim.
|
|
1951
|
+
return value === '' ? undefined : unquoteYamlScalar(value);
|
|
1773
1952
|
}
|
|
1774
1953
|
}
|
|
1775
1954
|
return undefined;
|
|
1776
1955
|
}
|
|
1777
|
-
function unquote(value) {
|
|
1778
|
-
if (value.length >= 2) {
|
|
1779
|
-
const first = value[0];
|
|
1780
|
-
const last = value[value.length - 1];
|
|
1781
|
-
if ((first === '"' || first === "'") && last === first) {
|
|
1782
|
-
return value.slice(1, -1);
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
return value;
|
|
1786
|
-
}
|
|
1787
1956
|
function escapeRegExp(value) {
|
|
1788
1957
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1789
1958
|
}
|
|
@@ -2216,39 +2385,15 @@ async function reconcileDivergentDoneMove(params) {
|
|
|
2216
2385
|
return true;
|
|
2217
2386
|
}
|
|
2218
2387
|
/**
|
|
2219
|
-
* Run
|
|
2220
|
-
*
|
|
2221
|
-
*
|
|
2222
|
-
*
|
|
2223
|
-
*
|
|
2224
|
-
*
|
|
2225
|
-
* `cwd`, right after the front `verify` (today's order, byte-for-byte);
|
|
2226
|
-
* - fresh-worktree gate ON: the caller invokes it on the REBASED TIP (the fresh
|
|
2227
|
-
* gate worktree), right AFTER the rebased-tip `verify` passes — so
|
|
2228
|
-
* verify-THEN-review holds on the SAME merged tree.
|
|
2229
|
-
*
|
|
2230
|
-
* The review AGENT inspects {@param reviewCwd} (the tree under review); the
|
|
2231
|
-
* needs-attention ROUTING always targets `params.cwd` (where the work branch +
|
|
2232
|
-
* ledger live), so the routing is identical whichever tree was reviewed — ONLY the
|
|
2233
|
-
* source of the reviewed tree moved, not the verdict handling.
|
|
2234
|
-
*
|
|
2235
|
-
* It NEVER mutates `mode` or writes the nits observation itself (those are the
|
|
2236
|
-
* caller's concern, because WHEN they happen differs by path — pre-commit on OFF,
|
|
2237
|
-
* post-commit-amend on ON); it returns the verdict and lets the caller place those
|
|
2238
|
-
* effects correctly for its band position.
|
|
2388
|
+
* Run a review gate for up to `maxRounds` rounds on ONE tree and return the
|
|
2389
|
+
* verdict as DATA, writing nothing (the WRITE-FREE core of {@link runGate2Review},
|
|
2390
|
+
* which adds the needs-attention routing). Factored out so the CI tasking agent
|
|
2391
|
+
* phase (task `ci-split-tasking`) runs the one-round task-set review the SAME
|
|
2392
|
+
* way `performIntegration` does, in the job that may launch agents, without the
|
|
2393
|
+
* routing writes (the apply phase routes). A non-parse error is re-thrown.
|
|
2239
2394
|
*/
|
|
2240
|
-
async function
|
|
2241
|
-
const {
|
|
2242
|
-
const reviewGate = input.reviewGate;
|
|
2243
|
-
if (!reviewGate) {
|
|
2244
|
-
// `review` on with no gate wired is a usage error — the floor must never be
|
|
2245
|
-
// silently skipped. (Production always wires `harnessReviewGate`.) The caller's
|
|
2246
|
-
// try/catch maps a thrown error to its usage-error outcome.
|
|
2247
|
-
throw new Error(`review is on but no review gate is configured — cannot run Gate 2 ` +
|
|
2248
|
-
`for '${slug}' (this is a wiring bug; the gate must not be skipped).`);
|
|
2249
|
-
}
|
|
2250
|
-
const maxRounds = Math.max(1, input.reviewMaxRounds ?? 2);
|
|
2251
|
-
note('Running the PR/code review gate (Gate 2)…');
|
|
2395
|
+
export async function runReviewRounds(params) {
|
|
2396
|
+
const { reviewGate, slug, reviewCwd, maxRounds } = params;
|
|
2252
2397
|
// CORROBORATED-APPROVAL semantics (NOT retry-until-pass): the gate runs the
|
|
2253
2398
|
// reviewer up to `reviewMaxRounds` times on the SAME tip and approves ONLY if
|
|
2254
2399
|
// EVERY round approves. A `block` is TERMINAL — it short-circuits the loop and
|
|
@@ -2267,72 +2412,28 @@ async function runGate2Review(params) {
|
|
|
2267
2412
|
verdict = await reviewGate({
|
|
2268
2413
|
slug,
|
|
2269
2414
|
cwd: reviewCwd,
|
|
2270
|
-
reviewModel:
|
|
2415
|
+
reviewModel: params.reviewModel,
|
|
2271
2416
|
round,
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
sessionsDir: input.sessionsDir,
|
|
2278
|
-
// The review AGENT launches with the AMBIENT env, never the identity-scoped
|
|
2279
|
-
// `env` (an agent must not act as the bot). Falls back to `env` when no
|
|
2280
|
-
// identity is configured (unchanged for non-identity callers).
|
|
2281
|
-
env: input.agentEnv ?? env,
|
|
2417
|
+
watch: params.watch,
|
|
2418
|
+
watchSink: params.watchSink,
|
|
2419
|
+
color: params.color,
|
|
2420
|
+
sessionsDir: params.sessionsDir,
|
|
2421
|
+
env: params.env,
|
|
2282
2422
|
});
|
|
2283
2423
|
}
|
|
2284
2424
|
catch (err) {
|
|
2285
2425
|
if (!(err instanceof ReviewParseError)) {
|
|
2286
2426
|
// Anything else (a harness/connection throw, a programmer bug) is NOT this
|
|
2287
|
-
// gate's concern
|
|
2427
|
+
// gate's concern: re-throw so the existing catch sites classify it.
|
|
2288
2428
|
throw err;
|
|
2289
2429
|
}
|
|
2290
|
-
//
|
|
2291
|
-
//
|
|
2292
|
-
//
|
|
2293
|
-
// direction-2 repair pass could not salvage it). WITHOUT this catch the throw
|
|
2294
|
-
// escapes the core and `performComplete` maps it to the generic `usage-error`
|
|
2295
|
-
// (verbatim, no push, no surface) AFTER the green build but BEFORE the
|
|
2296
|
-
// done-move/push — STRANDING the lock + work branch with no PR.
|
|
2297
|
-
//
|
|
2298
|
-
// A parse failure in ANY round is TERMINAL: route IMMEDIATELY, never re-roll
|
|
2299
|
-
// the remaining rounds (mirroring the block-is-terminal rule — re-reviewing
|
|
2300
|
-
// the same tip would just be the dice re-roll the corroboration loop forbids).
|
|
2301
|
-
// We route through the SAME work-preserving `applyNeedsAttentionTransition`
|
|
2302
|
-
// seam the block path uses (it PUSHES the work branch + surfaces the item on
|
|
2303
|
-
// `surfaceArbiter` for the autonomous path), targeting `cwd` (the work branch +
|
|
2304
|
-
// ledger), NOT the throwaway `reviewCwd` — so BOTH the direct `!freshWorktreeGate`
|
|
2305
|
-
// path AND the fresh-worktree `review:` callback are covered by this ONE catch.
|
|
2306
|
-
// The recorded reason carries the parse-failure phrase the `failure-cause.ts`
|
|
2307
|
-
// signature matches → the `do`/`run` tail classifies it `transient-infra`
|
|
2308
|
-
// (retry the SAME work: the gate output is STOCHASTIC, so a re-run CAN differ,
|
|
2309
|
-
// and the direction-2 repair makes a re-run far more likely to parse). NEVER a
|
|
2310
|
-
// silent approve.
|
|
2311
|
-
const reason = `PR/code review (Gate 2) ran but its verdict could not be parsed: ` +
|
|
2312
|
-
`${err.message}`;
|
|
2313
|
-
const routed = await ledgerWrite.applyNeedsAttentionTransition({
|
|
2314
|
-
cwd,
|
|
2315
|
-
slug,
|
|
2316
|
-
reason,
|
|
2317
|
-
arbiter: input.surfaceArbiter,
|
|
2318
|
-
env,
|
|
2319
|
-
note,
|
|
2320
|
-
});
|
|
2321
|
-
const message = routed.moved
|
|
2322
|
-
? `PR/code review (Gate 2) produced an UNPARSEABLE verdict for '${slug}'; ` +
|
|
2323
|
-
'marked it stuck on its per-item lock (work branch pushed + lock ' +
|
|
2324
|
-
'surfaced; transient-infra — re-run). NOT integrated.'
|
|
2325
|
-
: `PR/code review (Gate 2) produced an UNPARSEABLE verdict for '${slug}'; ` +
|
|
2326
|
-
'NOT integrating.';
|
|
2327
|
-
note(message);
|
|
2430
|
+
// A parse failure in ANY round is TERMINAL (never re-roll the remaining
|
|
2431
|
+
// rounds). The reason carries the parse-failure phrase the
|
|
2432
|
+
// `failure-cause.ts` signature matches (a transient class).
|
|
2328
2433
|
return {
|
|
2329
|
-
kind: '
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
routedToNeedsAttention: routed.moved,
|
|
2333
|
-
branch,
|
|
2334
|
-
reason,
|
|
2335
|
-
},
|
|
2434
|
+
kind: 'unparseable',
|
|
2435
|
+
reason: `PR/code review (Gate 2) ran but its verdict could not be parsed: ` +
|
|
2436
|
+
`${err.message}`,
|
|
2336
2437
|
};
|
|
2337
2438
|
}
|
|
2338
2439
|
lastVerdict = verdict;
|
|
@@ -2346,6 +2447,111 @@ async function runGate2Review(params) {
|
|
|
2346
2447
|
// must ALSO approve for the gate to pass (corroboration, not first-approve-wins).
|
|
2347
2448
|
approved = true;
|
|
2348
2449
|
}
|
|
2450
|
+
return approved
|
|
2451
|
+
? { kind: 'approved', verdict: lastVerdict }
|
|
2452
|
+
: { kind: 'blocked', verdict: lastVerdict };
|
|
2453
|
+
}
|
|
2454
|
+
/**
|
|
2455
|
+
* Run the Gate-2 PR/code REVIEW gate against a given tree ({@param reviewCwd}) and
|
|
2456
|
+
* route a BLOCK to needs-attention, returning DATA the caller acts on. Factored out
|
|
2457
|
+
* of {@link performIntegration} so the SAME gate can run in TWO places without
|
|
2458
|
+
* forking its logic (MAINTAINER DECISION 2, task `gate-on-rebased-tip-fresh-worktree`):
|
|
2459
|
+
*
|
|
2460
|
+
* - fresh-worktree gate OFF: the caller invokes it at the FRONT on the pre-rebase
|
|
2461
|
+
* `cwd`, right after the front `verify` (today's order, byte-for-byte);
|
|
2462
|
+
* - fresh-worktree gate ON: the caller invokes it on the REBASED TIP (the fresh
|
|
2463
|
+
* gate worktree), right AFTER the rebased-tip `verify` passes — so
|
|
2464
|
+
* verify-THEN-review holds on the SAME merged tree.
|
|
2465
|
+
*
|
|
2466
|
+
* The review AGENT inspects {@param reviewCwd} (the tree under review); the
|
|
2467
|
+
* needs-attention ROUTING always targets `params.cwd` (where the work branch +
|
|
2468
|
+
* ledger live), so the routing is identical whichever tree was reviewed — ONLY the
|
|
2469
|
+
* source of the reviewed tree moved, not the verdict handling.
|
|
2470
|
+
*
|
|
2471
|
+
* It NEVER mutates `mode` or writes the nits observation itself (those are the
|
|
2472
|
+
* caller's concern, because WHEN they happen differs by path — pre-commit on OFF,
|
|
2473
|
+
* post-commit-amend on ON); it returns the verdict and lets the caller place those
|
|
2474
|
+
* effects correctly for its band position.
|
|
2475
|
+
*/
|
|
2476
|
+
async function runGate2Review(params) {
|
|
2477
|
+
const { reviewCwd, input, slug, branch, cwd, env, note } = params;
|
|
2478
|
+
const reviewGate = input.reviewGate;
|
|
2479
|
+
if (!reviewGate) {
|
|
2480
|
+
// `review` on with no gate wired is a usage error — the floor must never be
|
|
2481
|
+
// silently skipped. (Production always wires `harnessReviewGate`.) The caller's
|
|
2482
|
+
// try/catch maps a thrown error to its usage-error outcome.
|
|
2483
|
+
throw new Error(`review is on but no review gate is configured — cannot run Gate 2 ` +
|
|
2484
|
+
`for '${slug}' (this is a wiring bug; the gate must not be skipped).`);
|
|
2485
|
+
}
|
|
2486
|
+
note('Running the PR/code review gate (Gate 2)…');
|
|
2487
|
+
const maxRounds = Math.max(1, input.reviewMaxRounds ?? 2);
|
|
2488
|
+
const rounds = await runReviewRounds({
|
|
2489
|
+
reviewGate,
|
|
2490
|
+
slug,
|
|
2491
|
+
reviewCwd,
|
|
2492
|
+
maxRounds,
|
|
2493
|
+
reviewModel: input.reviewModel,
|
|
2494
|
+
// `--watch` threading (task `watch-review-session`): when on, the production
|
|
2495
|
+
// gate tails the review session live. OFF ⇒ the plain sync launch, unchanged.
|
|
2496
|
+
watch: input.watch,
|
|
2497
|
+
watchSink: input.watchSink,
|
|
2498
|
+
color: input.color,
|
|
2499
|
+
sessionsDir: input.sessionsDir,
|
|
2500
|
+
// The review AGENT launches with the AMBIENT env, never the identity-scoped
|
|
2501
|
+
// `env` (an agent must not act as the bot). Falls back to `env` when no
|
|
2502
|
+
// identity is configured (unchanged for non-identity callers).
|
|
2503
|
+
env: input.agentEnv ?? env,
|
|
2504
|
+
});
|
|
2505
|
+
if (rounds.kind === 'unparseable') {
|
|
2506
|
+
// THE GATE RAN BUT ITS VERDICT WAS UNREADABLE (direction 1, the safety net):
|
|
2507
|
+
// the reviewer did NOT block — the gate's OUTPUT could not be parsed (a
|
|
2508
|
+
// malformed JSON verdict, common on large diffs + weaker models, AFTER the
|
|
2509
|
+
// direction-2 repair pass could not salvage it). WITHOUT this catch the throw
|
|
2510
|
+
// escapes the core and `performComplete` maps it to the generic `usage-error`
|
|
2511
|
+
// (verbatim, no push, no surface) AFTER the green build but BEFORE the
|
|
2512
|
+
// done-move/push — STRANDING the lock + work branch with no PR.
|
|
2513
|
+
//
|
|
2514
|
+
// A parse failure in ANY round is TERMINAL: route IMMEDIATELY, never re-roll
|
|
2515
|
+
// the remaining rounds (mirroring the block-is-terminal rule — re-reviewing
|
|
2516
|
+
// the same tip would just be the dice re-roll the corroboration loop forbids).
|
|
2517
|
+
// We route through the SAME work-preserving `applyNeedsAttentionTransition`
|
|
2518
|
+
// seam the block path uses (it PUSHES the work branch + surfaces the item on
|
|
2519
|
+
// `surfaceArbiter` for the autonomous path), targeting `cwd` (the work branch +
|
|
2520
|
+
// ledger), NOT the throwaway `reviewCwd` — so BOTH the direct `!freshWorktreeGate`
|
|
2521
|
+
// path AND the fresh-worktree `review:` callback are covered by this ONE catch.
|
|
2522
|
+
// The recorded reason carries the parse-failure phrase the `failure-cause.ts`
|
|
2523
|
+
// signature matches → the `do`/`run` tail classifies it `transient-infra`
|
|
2524
|
+
// (retry the SAME work: the gate output is STOCHASTIC, so a re-run CAN differ,
|
|
2525
|
+
// and the direction-2 repair makes a re-run far more likely to parse). NEVER a
|
|
2526
|
+
// silent approve.
|
|
2527
|
+
const reason = rounds.reason;
|
|
2528
|
+
const routed = await ledgerWrite.applyNeedsAttentionTransition({
|
|
2529
|
+
cwd,
|
|
2530
|
+
slug,
|
|
2531
|
+
reason,
|
|
2532
|
+
arbiter: input.surfaceArbiter,
|
|
2533
|
+
env,
|
|
2534
|
+
note,
|
|
2535
|
+
});
|
|
2536
|
+
const message = routed.moved
|
|
2537
|
+
? `PR/code review (Gate 2) produced an UNPARSEABLE verdict for '${slug}'; ` +
|
|
2538
|
+
'marked it stuck on its per-item lock (work branch pushed + lock ' +
|
|
2539
|
+
'surfaced; transient-infra — re-run). NOT integrated.'
|
|
2540
|
+
: `PR/code review (Gate 2) produced an UNPARSEABLE verdict for '${slug}'; ` +
|
|
2541
|
+
'NOT integrating.';
|
|
2542
|
+
note(message);
|
|
2543
|
+
return {
|
|
2544
|
+
kind: 'blocked',
|
|
2545
|
+
result: {
|
|
2546
|
+
outcome: 'review-unparseable',
|
|
2547
|
+
routedToNeedsAttention: routed.moved,
|
|
2548
|
+
branch,
|
|
2549
|
+
reason,
|
|
2550
|
+
},
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
const lastVerdict = rounds.verdict;
|
|
2554
|
+
const approved = rounds.kind === 'approved';
|
|
2349
2555
|
if (!approved) {
|
|
2350
2556
|
// NON-approve verdict: route to needs-attention via the SAME seam the red gate
|
|
2351
2557
|
// uses, NEVER integrate. We reach here EITHER because a round returned a
|
|
@@ -2488,13 +2694,80 @@ async function runFreshWorktreeGate(params) {
|
|
|
2488
2694
|
}
|
|
2489
2695
|
}
|
|
2490
2696
|
}
|
|
2697
|
+
/**
|
|
2698
|
+
* The git trailer key that marks a merge-mode land whose tree differs from the
|
|
2699
|
+
* gated tree (ADR `ci-agent-job-holds-no-write-token` decision 2). Its value is
|
|
2700
|
+
* the report, e.g. `landed without re-gate after 2 lost races`. A git trailer
|
|
2701
|
+
* like `CAS-Nonce` (`ledger-write.ts`), so `git log --format=%(trailers)`
|
|
2702
|
+
* reads it back.
|
|
2703
|
+
*/
|
|
2704
|
+
export const LANDED_WITHOUT_REGATE_TRAILER = 'Landed-Without-Regate';
|
|
2705
|
+
/** The landed-vs-gated report for `lostRaces` lost pushes. */
|
|
2706
|
+
export function landedWithoutRegateReport(lostRaces) {
|
|
2707
|
+
return (`landed without re-gate after ${lostRaces} lost ` +
|
|
2708
|
+
(lostRaces === 1 ? 'race' : 'races'));
|
|
2709
|
+
}
|
|
2710
|
+
/** The tree id of `rev` in `cwd`. */
|
|
2711
|
+
async function treeOf(cwd, rev, env) {
|
|
2712
|
+
return (await gitHard(['rev-parse', `${rev}^{tree}`], cwd, env)).stdout.trim();
|
|
2713
|
+
}
|
|
2714
|
+
/**
|
|
2715
|
+
* After the merge-mode CAS loop re-rebased `branch` for the `lostRaces`-th
|
|
2716
|
+
* time, rewrite its tip so its message carries exactly the right
|
|
2717
|
+
* {@link LANDED_WITHOUT_REGATE_TRAILER}: set (with this count) when the tip's
|
|
2718
|
+
* tree differs from `gatedTree`, removed when it is equal again. A previous
|
|
2719
|
+
* attempt's trailer rides the re-rebase as part of the message, which is why an
|
|
2720
|
+
* existing one is always replaced, never appended to. Only the message changes:
|
|
2721
|
+
* the tree, the parents and the author are the tip's own, and the branch ref
|
|
2722
|
+
* moves by a compare-and-swap `update-ref` (the working tree and the index
|
|
2723
|
+
* already match that tree). Returns the report when the trailer is set.
|
|
2724
|
+
*/
|
|
2725
|
+
async function stampLandedWithoutRegate(params) {
|
|
2726
|
+
const { cwd, branch, gatedTree, lostRaces, env } = params;
|
|
2727
|
+
const ref = `refs/heads/${branch}`;
|
|
2728
|
+
const FIELD_SEP = '\u0000';
|
|
2729
|
+
const raw = (await gitHard(['log', '-1', '--format=%H%x00%T%x00%P%x00%an%x00%ae%x00%aI%x00%B', ref], cwd, env)).stdout;
|
|
2730
|
+
const [head, tree, parentList, authorName, authorEmail, authorDate, body] = raw.split(FIELD_SEP);
|
|
2731
|
+
const report = tree.trim() === gatedTree
|
|
2732
|
+
? undefined
|
|
2733
|
+
: landedWithoutRegateReport(lostRaces);
|
|
2734
|
+
const original = (body ?? '').replace(/\s+$/, '');
|
|
2735
|
+
const prefix = `${LANDED_WITHOUT_REGATE_TRAILER}:`;
|
|
2736
|
+
const stripped = original
|
|
2737
|
+
.split('\n')
|
|
2738
|
+
.filter((line) => !line.startsWith(prefix))
|
|
2739
|
+
.join('\n')
|
|
2740
|
+
.replace(/\s+$/, '');
|
|
2741
|
+
const message = report === undefined
|
|
2742
|
+
? stripped
|
|
2743
|
+
: (await gitHard([
|
|
2744
|
+
'interpret-trailers',
|
|
2745
|
+
'--trailer',
|
|
2746
|
+
`${LANDED_WITHOUT_REGATE_TRAILER}: ${report}`,
|
|
2747
|
+
], cwd, env, `${stripped}\n`)).stdout.replace(/\s+$/, '');
|
|
2748
|
+
if (message === original)
|
|
2749
|
+
return report;
|
|
2750
|
+
const parents = parentList
|
|
2751
|
+
.trim()
|
|
2752
|
+
.split(/\s+/)
|
|
2753
|
+
.filter((p) => p !== '')
|
|
2754
|
+
.flatMap((p) => ['-p', p]);
|
|
2755
|
+
const rewritten = (await gitHard(['commit-tree', tree.trim(), ...parents, '-F', '-'], cwd, {
|
|
2756
|
+
...(env ?? process.env),
|
|
2757
|
+
GIT_AUTHOR_NAME: authorName,
|
|
2758
|
+
GIT_AUTHOR_EMAIL: authorEmail,
|
|
2759
|
+
GIT_AUTHOR_DATE: authorDate,
|
|
2760
|
+
}, `${message}\n`)).stdout.trim();
|
|
2761
|
+
await gitHard(['update-ref', ref, rewritten, head.trim()], cwd, env);
|
|
2762
|
+
return report;
|
|
2763
|
+
}
|
|
2491
2764
|
/** Run git, returning the raw result (no throw) — for soft checks. */
|
|
2492
2765
|
function gitSoft(args, cwd, env) {
|
|
2493
2766
|
return runAsync('git', args, cwd, { env });
|
|
2494
2767
|
}
|
|
2495
2768
|
/** Run git; throw on non-zero (genuinely unexpected plumbing failures). */
|
|
2496
|
-
async function gitHard(args, cwd, env) {
|
|
2497
|
-
const result = await runAsync('git', args, cwd, { env });
|
|
2769
|
+
async function gitHard(args, cwd, env, input) {
|
|
2770
|
+
const result = await runAsync('git', args, cwd, { env, input });
|
|
2498
2771
|
if (result.status !== 0) {
|
|
2499
2772
|
throw new Error(`git ${args.join(' ')} failed (exit ${result.status}): ${result.stderr.trim()}`);
|
|
2500
2773
|
}
|