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
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
type SurfacePersistOptions,
|
|
14
14
|
type SurfacePersistResult,
|
|
15
15
|
} from './surface-persist.js';
|
|
16
|
+
import {parseWorkBranchRef} from './slug-namespace.js';
|
|
16
17
|
import {workItemRel, type WorkFolderKey} from './work-layout.js';
|
|
18
|
+
import type {LockEntry} from './item-lock.js';
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* The **MERGE-QUESTION SURFACER** (spec `land-time-reverify-and-parallel-merge-ceiling`,
|
|
@@ -37,17 +39,30 @@ import {workItemRel, type WorkFolderKey} from './work-layout.js';
|
|
|
37
39
|
*
|
|
38
40
|
* Layered like the spec asks: the FLOOR (git-alone reachability) is the
|
|
39
41
|
* authoritative enumerator — it works against a bare `--bare` arbiter with
|
|
40
|
-
* `NoneProvider`. The CEILING (`gh pr list`
|
|
41
|
-
*
|
|
42
|
-
*
|
|
42
|
+
* `NoneProvider`. The CEILING (`gh pr list`) runs when a GitHub host is
|
|
43
|
+
* configured, and there it FILTERS (task
|
|
44
|
+
* `wire-merge-questions-into-the-advance-tick`, decision 1): an open PR already
|
|
45
|
+
* IS the human's land decision (merge or close it), so a branch with an open PR
|
|
46
|
+
* gets no merge question. Merge questions are for branches with no PR: the
|
|
47
|
+
* git-alone floor, or a branch whose PR was closed unmerged.
|
|
43
48
|
*
|
|
44
|
-
*
|
|
49
|
+
* Which branches it asks about (decisions 1, 4 and 6 of that task), in order:
|
|
45
50
|
*
|
|
46
|
-
* -
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* -
|
|
50
|
-
*
|
|
51
|
+
* - the task body rests in `tasks/ready/` or `tasks/backlog/` (a body in
|
|
52
|
+
* `done/` or `cancelled/` is terminal, which also covers a squash-merged PR
|
|
53
|
+
* whose branch was not deleted);
|
|
54
|
+
* - the sidecar carries no merge question yet (pending or answered);
|
|
55
|
+
* - no open PR (GitHub only; an unreadable PR list asks nothing);
|
|
56
|
+
* - the item's lock is not held, or is held WITH the `propose-pr` marker (a
|
|
57
|
+
* finished propose build keeps it until its work lands); a lock held without
|
|
58
|
+
* it is a live build, including the rebuild of a bounced task's kept branch;
|
|
59
|
+
* - the branch tip carries the done-move (`work/tasks/done/<slug>.md`), the
|
|
60
|
+
* state the answered-merge land (the committed-recovery tail) lands. A
|
|
61
|
+
* bounced build's kept work stops before its done-move, so it is not asked
|
|
62
|
+
* about.
|
|
63
|
+
*
|
|
64
|
+
* WIRED by `merge-question-tick.ts` (the laptop bare `advance` and the CI
|
|
65
|
+
* `surface-merge-questions` job), behind the `mergeQuestions: off | ask` gate.
|
|
51
66
|
*
|
|
52
67
|
* Tests inject the two seams ({@link listUnmergedWorkBranches},
|
|
53
68
|
* {@link listOpenPullRequests}) so they NEVER hit real GitHub — the floor uses
|
|
@@ -55,11 +70,11 @@ import {workItemRel, type WorkFolderKey} from './work-layout.js';
|
|
|
55
70
|
* `gh pr list` stub.
|
|
56
71
|
*/
|
|
57
72
|
|
|
58
|
-
/** A `work
|
|
73
|
+
/** A task build branch (`work/task-<slug>`) whose tip is not reachable from `<base>`. */
|
|
59
74
|
export interface UnmergedWorkBranch {
|
|
60
|
-
/** The
|
|
75
|
+
/** The branch name as it is on the arbiter, e.g. `work/task-foo`. */
|
|
61
76
|
ref: string;
|
|
62
|
-
/** The bare slug
|
|
77
|
+
/** The bare task slug, parsed by `parseWorkBranchRef`, e.g. `foo`. */
|
|
63
78
|
slug: string;
|
|
64
79
|
/** Branch tip SHA (advisory). */
|
|
65
80
|
sha?: string;
|
|
@@ -77,6 +92,12 @@ export interface MergeQuestionPullRequest {
|
|
|
77
92
|
export interface ListUnmergedInput {
|
|
78
93
|
cwd: string;
|
|
79
94
|
base: string;
|
|
95
|
+
/**
|
|
96
|
+
* The arbiter's remote NAME in `cwd` (default `origin`). In a clone the
|
|
97
|
+
* arbiter's branches are read from `refs/remotes/<arbiter>/`; ignored in the
|
|
98
|
+
* hub mirror, whose local heads ARE the arbiter's branches.
|
|
99
|
+
*/
|
|
100
|
+
arbiter?: string;
|
|
80
101
|
env?: NodeJS.ProcessEnv;
|
|
81
102
|
}
|
|
82
103
|
|
|
@@ -99,6 +120,12 @@ export interface SurfaceMergeQuestionsOptions {
|
|
|
99
120
|
arbiterUrl?: string;
|
|
100
121
|
/** The base branch reachability is checked against. Default `main`. */
|
|
101
122
|
base?: string;
|
|
123
|
+
/**
|
|
124
|
+
* The arbiter's remote NAME in `cwd` (default `origin`), whose
|
|
125
|
+
* remote-tracking refs the FLOOR reads in a clone. See
|
|
126
|
+
* {@link listUnmergedWorkBranchesViaGit}.
|
|
127
|
+
*/
|
|
128
|
+
arbiter?: string;
|
|
102
129
|
/** The `gh` CLI binary to invoke for the ceiling. Default `gh` on PATH. */
|
|
103
130
|
ghBin?: string;
|
|
104
131
|
/** Environment for spawned git/gh processes. */
|
|
@@ -121,6 +148,12 @@ export interface SurfaceMergeQuestionsOptions {
|
|
|
121
148
|
) => Map<string, MergeQuestionPullRequest>;
|
|
122
149
|
/** Seam: persist a single merge-question. Defaults to {@link persistSurfacedQuestions}. */
|
|
123
150
|
persist?: (options: SurfacePersistOptions) => SurfacePersistResult;
|
|
151
|
+
/**
|
|
152
|
+
* The per-item locks held on the arbiter (`listItemLockEntries`), read by the
|
|
153
|
+
* caller. A task whose lock is held WITHOUT the `propose-pr` marker is a live
|
|
154
|
+
* build and is skipped (`lock-held`). Omitted ⇒ no lock is held.
|
|
155
|
+
*/
|
|
156
|
+
locks?: readonly LockEntry[];
|
|
124
157
|
}
|
|
125
158
|
|
|
126
159
|
/** One surfaced merge-question (a row in the result). */
|
|
@@ -129,14 +162,12 @@ export interface MergeQuestionSurfaced {
|
|
|
129
162
|
item: string;
|
|
130
163
|
/** The bare slug. */
|
|
131
164
|
slug: string;
|
|
132
|
-
/** The `work
|
|
165
|
+
/** The `work/task-<slug>` branch the question is about. */
|
|
133
166
|
ref: string;
|
|
134
167
|
/** The sidecar path the persist touched (repo-relative). */
|
|
135
168
|
sidecarPath: string;
|
|
136
169
|
/** The persist commit (`undefined` if the persist was a no-op). */
|
|
137
170
|
commit?: string;
|
|
138
|
-
/** PR url when a GitHub ceiling matched the branch (advisory). */
|
|
139
|
-
prUrl?: string;
|
|
140
171
|
}
|
|
141
172
|
|
|
142
173
|
/**
|
|
@@ -152,7 +183,22 @@ export interface MergeQuestionSkipped {
|
|
|
152
183
|
ref: string;
|
|
153
184
|
slug: string;
|
|
154
185
|
/** PROVISIONAL vocabulary — see {@link MergeQuestionSkipped}. */
|
|
155
|
-
reason:
|
|
186
|
+
reason:
|
|
187
|
+
| 'no-item-body'
|
|
188
|
+
/** The body rests in `tasks/done/` or `tasks/cancelled/`. */
|
|
189
|
+
| 'terminal'
|
|
190
|
+
| 'already-pending-merge-question'
|
|
191
|
+
/** An answered merge question awaits its apply. */
|
|
192
|
+
| 'merge-question-answered'
|
|
193
|
+
/** The branch has an open PR (GitHub): the PR is the land decision. */
|
|
194
|
+
| 'open-pr'
|
|
195
|
+
/** GitHub, but the open PRs could not be listed: nothing is asked. */
|
|
196
|
+
| 'pr-state-unknown'
|
|
197
|
+
/** The item's lock is held without the `propose-pr` marker (a live build). */
|
|
198
|
+
| 'lock-held'
|
|
199
|
+
/** The branch tip does not carry the task's done-move. */
|
|
200
|
+
| 'no-done-move'
|
|
201
|
+
| 'persist-nothing';
|
|
156
202
|
}
|
|
157
203
|
|
|
158
204
|
/** Aggregate result of one surfacer pass. */
|
|
@@ -163,6 +209,14 @@ export interface SurfaceMergeQuestionsResult {
|
|
|
163
209
|
surfaced: MergeQuestionSurfaced[];
|
|
164
210
|
/** The branches that were considered but skipped, with the reason. */
|
|
165
211
|
skipped: MergeQuestionSkipped[];
|
|
212
|
+
/**
|
|
213
|
+
* Set when the arbiter is GitHub-shaped but its open PRs could not be listed
|
|
214
|
+
* (`gh pr list` failed: no credential, no `gh`, an outage). The pass then
|
|
215
|
+
* asks about no branch (`pr-state-unknown`); a caller reports it as a FAILED
|
|
216
|
+
* pass rather than a quiet one (the `surface-merge-questions` command exits
|
|
217
|
+
* non-zero). Absent when the listing succeeded or was not needed.
|
|
218
|
+
*/
|
|
219
|
+
prListingError?: string;
|
|
166
220
|
}
|
|
167
221
|
|
|
168
222
|
/** Raised for fatal usage errors (a missing repo, etc.). */
|
|
@@ -174,21 +228,18 @@ export class MergeQuestionSurfacerError extends Error {
|
|
|
174
228
|
}
|
|
175
229
|
|
|
176
230
|
/**
|
|
177
|
-
* The lifecycle folders {@link findTaskItemPath}
|
|
178
|
-
* `
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* - OMITS `in-progress` (and `needs-attention`): an unmerged `work/<slug>`
|
|
182
|
-
* branch whose body is mid-build should NOT trigger a merge-question — the
|
|
183
|
-
* build is still active, and surfacing a land-decision now would race the
|
|
184
|
-
* builder. Such tasks fall through to the `no-item-body` skip.
|
|
185
|
-
* - ADDS `cancelled`: a cancelled task with a lingering unmerged `work/*`
|
|
186
|
-
* branch SHOULD surface a merge-question so the operator explicitly decides
|
|
187
|
-
* whether to merge the branch or drop it.
|
|
231
|
+
* The lifecycle folders {@link findTaskItemPath} asks about (task
|
|
232
|
+
* `wire-merge-questions-into-the-advance-tick`, decision 6): the pool and
|
|
233
|
+
* staging. `in-progress` / `needs-attention` no longer exist as folders (a
|
|
234
|
+
* build in flight is a held lock, filtered separately).
|
|
188
235
|
*/
|
|
189
236
|
const TASK_FOLDERS: readonly WorkFolderKey[] = [
|
|
190
237
|
'tasks-ready',
|
|
191
238
|
'tasks-backlog',
|
|
239
|
+
] as const;
|
|
240
|
+
|
|
241
|
+
/** A body in one of these is TERMINAL: its branch is never asked about. */
|
|
242
|
+
const TERMINAL_TASK_FOLDERS: readonly WorkFolderKey[] = [
|
|
192
243
|
'done',
|
|
193
244
|
'cancelled',
|
|
194
245
|
] as const;
|
|
@@ -197,9 +248,10 @@ const TASK_FOLDERS: readonly WorkFolderKey[] = [
|
|
|
197
248
|
* Surface merge-questions for every unmerged `work/*` branch in `cwd`.
|
|
198
249
|
*
|
|
199
250
|
* The FLOOR (git reachability) is authoritative; the CEILING (`gh pr list`)
|
|
200
|
-
* runs only when {@link isGitHubArbiterUrl} accepts the arbiter URL
|
|
201
|
-
*
|
|
202
|
-
*
|
|
251
|
+
* runs only when {@link isGitHubArbiterUrl} accepts the arbiter URL. On GitHub
|
|
252
|
+
* it is load-bearing (an open PR is the land decision), so a failed listing
|
|
253
|
+
* asks about NO branch and is reported on
|
|
254
|
+
* {@link SurfaceMergeQuestionsResult.prListingError}.
|
|
203
255
|
*
|
|
204
256
|
* Idempotency: a branch whose sidecar ALREADY carries a PENDING `kind: merge`
|
|
205
257
|
* entry is SKIPPED (with `already-pending-merge-question`). A branch with no
|
|
@@ -219,11 +271,15 @@ export function surfaceMergeQuestions(
|
|
|
219
271
|
|
|
220
272
|
const listBranches =
|
|
221
273
|
options.listUnmergedWorkBranches ?? listUnmergedWorkBranchesViaGit;
|
|
222
|
-
const branches = listBranches({cwd, base, env});
|
|
274
|
+
const branches = listBranches({cwd, base, arbiter: options.arbiter, env});
|
|
223
275
|
|
|
224
276
|
// CEILING: only consult `gh pr list` when the arbiter is GitHub-shaped. A
|
|
225
277
|
// bare / non-GitHub arbiter never spawns `gh` (the floor is sufficient).
|
|
226
|
-
|
|
278
|
+
// On GitHub an open PR is the land decision, so a branch with one is not
|
|
279
|
+
// asked about; a failed listing asks about NOTHING this pass (a question on
|
|
280
|
+
// a branch with an open PR would land it behind the PR's back).
|
|
281
|
+
let prs: Map<string, MergeQuestionPullRequest> | undefined = new Map();
|
|
282
|
+
let prListingError: string | undefined;
|
|
227
283
|
const ghEnabled =
|
|
228
284
|
options.arbiterUrl !== undefined && isGitHubArbiterUrl(options.arbiterUrl);
|
|
229
285
|
if (ghEnabled && branches.length > 0) {
|
|
@@ -236,52 +292,92 @@ export function surfaceMergeQuestions(
|
|
|
236
292
|
env,
|
|
237
293
|
});
|
|
238
294
|
} catch (err) {
|
|
239
|
-
// `gh` is the ENRICHMENT layer — a failure must NEVER suppress the
|
|
240
|
-
// floor's surfacing. Note it and proceed with an empty PR map.
|
|
241
295
|
const detail = err instanceof Error ? err.message : String(err);
|
|
242
296
|
note(
|
|
243
|
-
`merge-question surfacer: gh pr list failed (${detail})
|
|
297
|
+
`merge-question surfacer: gh pr list failed (${detail}); asking about no branch this pass.`,
|
|
244
298
|
);
|
|
245
|
-
prs =
|
|
299
|
+
prs = undefined;
|
|
300
|
+
prListingError = detail;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// The item locks by task slug (`task-<slug>` entries only).
|
|
305
|
+
const locks = new Map<string, LockEntry>();
|
|
306
|
+
for (const lock of options.locks ?? []) {
|
|
307
|
+
if (lock.entry.startsWith('task-')) {
|
|
308
|
+
locks.set(lock.entry.slice('task-'.length), lock);
|
|
246
309
|
}
|
|
247
310
|
}
|
|
248
311
|
|
|
249
312
|
const surfaced: MergeQuestionSurfaced[] = [];
|
|
250
313
|
const skipped: MergeQuestionSkipped[] = [];
|
|
314
|
+
const skip = (
|
|
315
|
+
branch: UnmergedWorkBranch,
|
|
316
|
+
reason: MergeQuestionSkipped['reason'],
|
|
317
|
+
): void => {
|
|
318
|
+
skipped.push({ref: branch.ref, slug: branch.slug, reason});
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// The `main` every question of this pass is asked against: recorded on the
|
|
322
|
+
// entry so a `strictMergeApproval` apply can tell whether `main` moved since
|
|
323
|
+
// (task `strict-merge-approval-restale-check-runs-before-the-continue-rebase`).
|
|
324
|
+
// Unresolvable (a test seam over a non-repo) ⇒ no base recorded.
|
|
325
|
+
const askedAtMain =
|
|
326
|
+
branches.length > 0 ? resolveCommit(cwd, base, env) : undefined;
|
|
251
327
|
|
|
252
328
|
for (const branch of branches) {
|
|
253
329
|
const item = `task:${branch.slug}`;
|
|
254
330
|
|
|
255
|
-
// Idempotency: if the sidecar already carries a PENDING `kind: merge`
|
|
256
|
-
// entry, this surfacer must not append a duplicate. (An ANSWERED merge
|
|
257
|
-
// entry is fine — apply will dispatch it; appending a new entry would
|
|
258
|
-
// re-pause the sidecar unnecessarily.)
|
|
259
|
-
if (alreadyHasPendingMergeQuestion(cwd, item)) {
|
|
260
|
-
skipped.push({
|
|
261
|
-
ref: branch.ref,
|
|
262
|
-
slug: branch.slug,
|
|
263
|
-
reason: 'already-pending-merge-question',
|
|
264
|
-
});
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
331
|
const itemPath = findTaskItemPath(cwd, branch.slug);
|
|
269
332
|
if (itemPath === undefined) {
|
|
333
|
+
// A terminal body (done / cancelled): its branch is finished business
|
|
334
|
+
// (a squash-merged PR whose branch was not deleted rests here too).
|
|
335
|
+
if (isTerminalTask(cwd, branch.slug)) {
|
|
336
|
+
skip(branch, 'terminal');
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
270
339
|
// The `branch:`/`ref:`-keyed sidecar identity (SPEC sidecar Q5-i, the
|
|
271
340
|
// cross-cutting open question SHARED with the stuck-lock surfacer) is
|
|
272
|
-
// OUT OF SCOPE
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
skipped.push({
|
|
276
|
-
ref: branch.ref,
|
|
277
|
-
slug: branch.slug,
|
|
278
|
-
reason: 'no-item-body',
|
|
279
|
-
});
|
|
341
|
+
// OUT OF SCOPE: without a body to flip `needsAnswers` on, persist would
|
|
342
|
+
// tear the invariant. Skip with the reason so the case is visible.
|
|
343
|
+
skip(branch, 'no-item-body');
|
|
280
344
|
continue;
|
|
281
345
|
}
|
|
282
346
|
|
|
283
|
-
|
|
284
|
-
|
|
347
|
+
// Idempotency: never append a second merge question. A PENDING one is
|
|
348
|
+
// awaiting its answer; an ANSWERED one awaits its apply (appending would
|
|
349
|
+
// re-pause the sidecar and strand the answer).
|
|
350
|
+
const existing = existingMergeQuestion(cwd, item);
|
|
351
|
+
if (existing === 'pending') {
|
|
352
|
+
skip(branch, 'already-pending-merge-question');
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (existing === 'answered') {
|
|
356
|
+
skip(branch, 'merge-question-answered');
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (prs === undefined) {
|
|
361
|
+
skip(branch, 'pr-state-unknown');
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (prs.has(branch.ref)) {
|
|
365
|
+
skip(branch, 'open-pr');
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const lock = locks.get(branch.slug);
|
|
370
|
+
if (lock !== undefined && lock.keptFor !== 'propose-pr') {
|
|
371
|
+
skip(branch, 'lock-held');
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!tipHasDoneMove(cwd, branch, env)) {
|
|
376
|
+
skip(branch, 'no-done-move');
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const question = buildMergeQuestion(branch, ghEnabled, askedAtMain);
|
|
285
381
|
|
|
286
382
|
const result = persist({
|
|
287
383
|
cwd,
|
|
@@ -292,11 +388,7 @@ export function surfaceMergeQuestions(
|
|
|
292
388
|
note,
|
|
293
389
|
});
|
|
294
390
|
if (result.outcome === 'nothing') {
|
|
295
|
-
|
|
296
|
-
ref: branch.ref,
|
|
297
|
-
slug: branch.slug,
|
|
298
|
-
reason: 'persist-nothing',
|
|
299
|
-
});
|
|
391
|
+
skip(branch, 'persist-nothing');
|
|
300
392
|
continue;
|
|
301
393
|
}
|
|
302
394
|
|
|
@@ -306,11 +398,15 @@ export function surfaceMergeQuestions(
|
|
|
306
398
|
ref: branch.ref,
|
|
307
399
|
sidecarPath: result.sidecarPath,
|
|
308
400
|
commit: result.commit,
|
|
309
|
-
prUrl: pr?.url,
|
|
310
401
|
});
|
|
311
402
|
}
|
|
312
403
|
|
|
313
|
-
return {
|
|
404
|
+
return {
|
|
405
|
+
considered: branches.length,
|
|
406
|
+
surfaced,
|
|
407
|
+
skipped,
|
|
408
|
+
...(prListingError === undefined ? {} : {prListingError}),
|
|
409
|
+
};
|
|
314
410
|
}
|
|
315
411
|
|
|
316
412
|
/**
|
|
@@ -328,23 +424,15 @@ export function surfaceMergeQuestions(
|
|
|
328
424
|
*/
|
|
329
425
|
function buildMergeQuestion(
|
|
330
426
|
branch: UnmergedWorkBranch,
|
|
331
|
-
|
|
427
|
+
ghEnabled: boolean,
|
|
428
|
+
askedAtMain: string | undefined,
|
|
332
429
|
): NewQuestion {
|
|
333
430
|
const contextLines: string[] = [
|
|
334
431
|
`The branch \`${branch.ref}\` is not reachable from \`main\` — it carries pushed work that has not yet landed.`,
|
|
432
|
+
ghEnabled
|
|
433
|
+
? 'It has no open PR (none was opened, or its PR was closed without merging).'
|
|
434
|
+
: 'No host PR metadata available (git-alone floor: the branch is the source of truth).',
|
|
335
435
|
];
|
|
336
|
-
if (pr !== undefined) {
|
|
337
|
-
const titleSuffix = pr.title ? `: ${pr.title}` : '';
|
|
338
|
-
const urlSuffix = pr.url ? ` (${pr.url})` : '';
|
|
339
|
-
const stateSuffix = pr.state ? ` [${pr.state}]` : '';
|
|
340
|
-
contextLines.push(
|
|
341
|
-
`Open PR #${pr.number}${stateSuffix}${titleSuffix}${urlSuffix}.`,
|
|
342
|
-
);
|
|
343
|
-
} else {
|
|
344
|
-
contextLines.push(
|
|
345
|
-
`No host PR metadata available (git-alone floor — the branch is the source of truth).`,
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
436
|
return {
|
|
349
437
|
question: `Land \`${branch.ref}\`? An unmerged \`work/*\` branch is awaiting an integration decision.`,
|
|
350
438
|
context: contextLines.join('\n'),
|
|
@@ -352,26 +440,83 @@ function buildMergeQuestion(
|
|
|
352
440
|
default: 'merge | hold | drop',
|
|
353
441
|
// MACHINE dispatch signal — the apply layer routes on this, not on `default`.
|
|
354
442
|
kind: 'merge',
|
|
443
|
+
...(askedAtMain === undefined ? {} : {askedAtMain}),
|
|
355
444
|
};
|
|
356
445
|
}
|
|
357
446
|
|
|
358
|
-
/**
|
|
359
|
-
function
|
|
447
|
+
/** The full sha `rev` names in `cwd`, or `undefined` when it does not resolve. */
|
|
448
|
+
function resolveCommit(
|
|
449
|
+
cwd: string,
|
|
450
|
+
rev: string,
|
|
451
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
452
|
+
): string | undefined {
|
|
453
|
+
let res: RunResult;
|
|
454
|
+
try {
|
|
455
|
+
res = gitSoft(
|
|
456
|
+
['rev-parse', '--verify', '--quiet', `${rev}^{commit}`],
|
|
457
|
+
cwd,
|
|
458
|
+
env,
|
|
459
|
+
);
|
|
460
|
+
} catch {
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
if (res.status !== 0) return undefined;
|
|
464
|
+
const sha = res.stdout.trim();
|
|
465
|
+
return sha === '' ? undefined : sha;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Probe the sidecar for an existing `kind: merge` entry: `pending` when one is
|
|
470
|
+
* unanswered, `answered` when every one is answered, `undefined` when there is
|
|
471
|
+
* none.
|
|
472
|
+
*/
|
|
473
|
+
function existingMergeQuestion(
|
|
474
|
+
cwd: string,
|
|
475
|
+
item: string,
|
|
476
|
+
): 'pending' | 'answered' | undefined {
|
|
360
477
|
const rel = sidecarPathFor(item);
|
|
361
478
|
const abs = join(cwd, rel);
|
|
362
479
|
if (!existsSync(abs)) {
|
|
363
|
-
return
|
|
480
|
+
return undefined;
|
|
364
481
|
}
|
|
365
482
|
try {
|
|
366
|
-
const
|
|
367
|
-
|
|
483
|
+
const merge = parseSidecar(readFileSync(abs, 'utf8')).entries.filter(
|
|
484
|
+
(e) => e.kind === 'merge',
|
|
485
|
+
);
|
|
486
|
+
if (merge.length === 0) return undefined;
|
|
487
|
+
return merge.some((e) => !isEntryAnswered(e)) ? 'pending' : 'answered';
|
|
368
488
|
} catch {
|
|
369
489
|
// A malformed sidecar is not THIS surfacer's problem — pretend there is
|
|
370
|
-
// no
|
|
371
|
-
return
|
|
490
|
+
// no merge-question and let the persist write surface the issue.
|
|
491
|
+
return undefined;
|
|
372
492
|
}
|
|
373
493
|
}
|
|
374
494
|
|
|
495
|
+
/** Does the body of `slug` rest in a terminal task folder? */
|
|
496
|
+
function isTerminalTask(cwd: string, slug: string): boolean {
|
|
497
|
+
return TERMINAL_TASK_FOLDERS.some((folder) =>
|
|
498
|
+
existsSync(join(cwd, workItemRel(folder, `${slug}.md`))),
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Does the branch tip carry the task's done-move (its body at
|
|
504
|
+
* `work/tasks/done/<slug>.md`)? The answered-merge land is the
|
|
505
|
+
* committed-recovery tail, which presupposes it. Read at the tip sha the
|
|
506
|
+
* listing gave; a branch listed without one is not asked about.
|
|
507
|
+
*/
|
|
508
|
+
function tipHasDoneMove(
|
|
509
|
+
cwd: string,
|
|
510
|
+
branch: UnmergedWorkBranch,
|
|
511
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
512
|
+
): boolean {
|
|
513
|
+
if (branch.sha === undefined) return false;
|
|
514
|
+
const done = workItemRel('done', `${branch.slug}.md`);
|
|
515
|
+
return (
|
|
516
|
+
gitSoft(['cat-file', '-e', `${branch.sha}:${done}`], cwd, env).status === 0
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
375
520
|
/** Locate the task body `work/<folder>/<slug>.md` across lifecycle folders. */
|
|
376
521
|
function findTaskItemPath(cwd: string, slug: string): string | undefined {
|
|
377
522
|
for (const folder of TASK_FOLDERS) {
|
|
@@ -394,19 +539,50 @@ function gitSoft(
|
|
|
394
539
|
}
|
|
395
540
|
|
|
396
541
|
/**
|
|
397
|
-
* Production FLOOR: enumerate
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
542
|
+
* Production FLOOR: enumerate the ARBITER's task build branches
|
|
543
|
+
* (`work/task-<slug>`, the {@link workBranchRef} form) whose tip is NOT
|
|
544
|
+
* reachable from the arbiter's `<base>`.
|
|
545
|
+
*
|
|
546
|
+
* WHERE the arbiter's refs live depends on the repo `cwd` is:
|
|
547
|
+
*
|
|
548
|
+
* - A **clone** (non-bare, with an `<arbiter>` remote): the arbiter's branches
|
|
549
|
+
* are the remote-tracking refs `refs/remotes/<arbiter>/work/*`, checked
|
|
550
|
+
* against `refs/remotes/<arbiter>/<base>`. Local `refs/heads/work/*` in a
|
|
551
|
+
* clone are this machine's own (possibly unpushed or stale) heads, not the
|
|
552
|
+
* arbiter's, so they are NOT listed.
|
|
553
|
+
* - The **hub mirror** (a `--bare` clone, or a worktree added from one, where
|
|
554
|
+
* `core.bare` reads `true`) or a repo with no `<arbiter>` remote (the repo IS
|
|
555
|
+
* the arbiter's ref store): the local heads `refs/heads/work/*`, checked
|
|
556
|
+
* against the local `<base>`.
|
|
557
|
+
*
|
|
558
|
+
* The item slug comes from {@link parseWorkBranchRef}, never from slicing the
|
|
559
|
+
* ref. Only the plain BUILD branch of a task (`namespace: 'task'`, no producer)
|
|
560
|
+
* is listed: an intake branch (`work/intake-*`) creates an item rather than
|
|
561
|
+
* building one, a spec branch (`work/spec-*`) carries tasking, and an
|
|
562
|
+
* un-namespaced `work/<slug>` is a pre-cutover ref `parseWorkBranchRef` refuses
|
|
563
|
+
* (the clean-break stance). None of those is what a `kind: merge` answer lands.
|
|
564
|
+
*
|
|
565
|
+
* Purely local: it reads refs already in `cwd` (no fetch, no `ls-remote`); the
|
|
566
|
+
* caller refreshes them. A repo whose `<base>` does not resolve (e.g. a fresh
|
|
567
|
+
* init before the first commit, or a clone that never fetched `<base>`) yields
|
|
568
|
+
* the empty list: without a base, "unmerged" is meaningless and every branch
|
|
569
|
+
* would over-surface.
|
|
401
570
|
*/
|
|
402
571
|
export function listUnmergedWorkBranchesViaGit(
|
|
403
572
|
input: ListUnmergedInput,
|
|
404
573
|
): UnmergedWorkBranch[] {
|
|
405
574
|
const {cwd, base, env} = input;
|
|
406
|
-
|
|
407
|
-
|
|
575
|
+
const arbiter = input.arbiter ?? 'origin';
|
|
576
|
+
const source = arbiterRefSource(cwd, arbiter, env);
|
|
577
|
+
const baseRef =
|
|
578
|
+
source === 'remote-tracking'
|
|
579
|
+
? `refs/remotes/${arbiter}/${base}`
|
|
580
|
+
: `refs/heads/${base}`;
|
|
581
|
+
const branchPrefix =
|
|
582
|
+
source === 'remote-tracking' ? `refs/remotes/${arbiter}/` : 'refs/heads/';
|
|
583
|
+
|
|
408
584
|
const haveBase = gitSoft(
|
|
409
|
-
['rev-parse', '--verify', '--quiet',
|
|
585
|
+
['rev-parse', '--verify', '--quiet', `${baseRef}^{commit}`],
|
|
410
586
|
cwd,
|
|
411
587
|
env,
|
|
412
588
|
);
|
|
@@ -416,8 +592,8 @@ export function listUnmergedWorkBranchesViaGit(
|
|
|
416
592
|
const refs = gitSoft(
|
|
417
593
|
[
|
|
418
594
|
'for-each-ref',
|
|
419
|
-
'--format=%(refname
|
|
420
|
-
|
|
595
|
+
'--format=%(refname) %(objectname)',
|
|
596
|
+
`${branchPrefix}work/`,
|
|
421
597
|
],
|
|
422
598
|
cwd,
|
|
423
599
|
env,
|
|
@@ -435,35 +611,65 @@ export function listUnmergedWorkBranchesViaGit(
|
|
|
435
611
|
if (space === -1) {
|
|
436
612
|
continue;
|
|
437
613
|
}
|
|
438
|
-
const
|
|
614
|
+
const fullRef = line.slice(0, space);
|
|
439
615
|
const sha = line.slice(space + 1).trim();
|
|
440
|
-
if (!
|
|
616
|
+
if (!fullRef.startsWith(branchPrefix)) {
|
|
441
617
|
continue;
|
|
442
618
|
}
|
|
443
|
-
|
|
444
|
-
|
|
619
|
+
// The branch name AS IT IS ON THE ARBITER (`work/task-<slug>`), whichever
|
|
620
|
+
// local namespace it was read from.
|
|
621
|
+
const ref = fullRef.slice(branchPrefix.length);
|
|
622
|
+
const parsed = parseWorkBranchRef(ref);
|
|
623
|
+
if (
|
|
624
|
+
parsed === undefined ||
|
|
625
|
+
parsed.namespace !== 'task' ||
|
|
626
|
+
parsed.producer !== undefined
|
|
627
|
+
) {
|
|
445
628
|
continue;
|
|
446
629
|
}
|
|
447
|
-
// `git merge-base --is-ancestor <sha> <base
|
|
630
|
+
// `git merge-base --is-ancestor <sha> <base>`: exit 0 ⇒ reachable
|
|
448
631
|
// (merged), exit 1 ⇒ unmerged.
|
|
449
|
-
const reach = gitSoft(
|
|
632
|
+
const reach = gitSoft(
|
|
633
|
+
['merge-base', '--is-ancestor', sha, baseRef],
|
|
634
|
+
cwd,
|
|
635
|
+
env,
|
|
636
|
+
);
|
|
450
637
|
if (reach.status !== 0) {
|
|
451
|
-
unmerged.push({ref, slug, sha});
|
|
638
|
+
unmerged.push({ref, slug: parsed.slug, sha});
|
|
452
639
|
}
|
|
453
640
|
}
|
|
454
641
|
return unmerged;
|
|
455
642
|
}
|
|
456
643
|
|
|
644
|
+
/**
|
|
645
|
+
* Which local ref namespace holds the arbiter's branches in `cwd`: the local
|
|
646
|
+
* heads for the bare hub mirror (and its worktrees, which share its
|
|
647
|
+
* `core.bare = true` config) or for a repo with no `<arbiter>` remote; the
|
|
648
|
+
* remote-tracking refs for an ordinary clone.
|
|
649
|
+
*/
|
|
650
|
+
function arbiterRefSource(
|
|
651
|
+
cwd: string,
|
|
652
|
+
arbiter: string,
|
|
653
|
+
env: NodeJS.ProcessEnv | undefined,
|
|
654
|
+
): 'local-heads' | 'remote-tracking' {
|
|
655
|
+
const bare = gitSoft(['config', '--bool', 'core.bare'], cwd, env);
|
|
656
|
+
if (bare.status === 0 && bare.stdout.trim() === 'true') {
|
|
657
|
+
return 'local-heads';
|
|
658
|
+
}
|
|
659
|
+
const remote = gitSoft(['remote', 'get-url', arbiter], cwd, env);
|
|
660
|
+
return remote.status === 0 ? 'remote-tracking' : 'local-heads';
|
|
661
|
+
}
|
|
662
|
+
|
|
457
663
|
/**
|
|
458
664
|
* Production CEILING: shell `gh pr list --state open --json …` and index the
|
|
459
|
-
* results by their `headRefName
|
|
460
|
-
*
|
|
461
|
-
*
|
|
665
|
+
* results by their `headRefName`, so the surfacer can skip a branch with an
|
|
666
|
+
* open PR. A non-zero `gh` (or output that is not a JSON array) THROWS: the
|
|
667
|
+
* surfacer then asks about no branch this pass, rather than asking about
|
|
668
|
+
* branches whose PR it cannot see.
|
|
462
669
|
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
* of >200 open PRs degrades to floor-only output, never corrupts it.
|
|
670
|
+
* The `--state open`, `--base <base>`, and `--limit 200` arguments are
|
|
671
|
+
* DELIBERATE ceilings: a PR targeting a non-`main` base (e.g. a stacked PR) or
|
|
672
|
+
* the case of >200 open PRs is not seen, so its branch may still be asked about.
|
|
467
673
|
*/
|
|
468
674
|
export function listOpenPullRequestsViaGh(
|
|
469
675
|
input: ListPullRequestsInput,
|
|
@@ -487,16 +693,18 @@ export function listOpenPullRequestsViaGh(
|
|
|
487
693
|
{env},
|
|
488
694
|
);
|
|
489
695
|
if (result.status !== 0) {
|
|
490
|
-
|
|
696
|
+
throw new Error(
|
|
697
|
+
`${ghBin} pr list exited ${result.status}: ${result.stderr.trim().slice(0, 300)}`,
|
|
698
|
+
);
|
|
491
699
|
}
|
|
492
700
|
let parsed: unknown;
|
|
493
701
|
try {
|
|
494
702
|
parsed = JSON.parse(result.stdout);
|
|
495
703
|
} catch {
|
|
496
|
-
|
|
704
|
+
throw new Error(`${ghBin} pr list printed no JSON`);
|
|
497
705
|
}
|
|
498
706
|
if (!Array.isArray(parsed)) {
|
|
499
|
-
|
|
707
|
+
throw new Error(`${ghBin} pr list printed no JSON array`);
|
|
500
708
|
}
|
|
501
709
|
const out = new Map<string, MergeQuestionPullRequest>();
|
|
502
710
|
for (const raw of parsed) {
|