@telora/daemon 0.21.134 → 0.21.146
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/build-info.json +2 -2
- package/dist/audit/audit-hooks.d.ts +15 -6
- package/dist/audit/audit-hooks.d.ts.map +1 -1
- package/dist/audit/audit-hooks.js +56 -28
- package/dist/audit/audit-hooks.js.map +1 -1
- package/dist/ci/ci-escalation.js +1 -1
- package/dist/ci/ci-escalation.js.map +1 -1
- package/dist/ci/ci-sensor-loop.d.ts +24 -0
- package/dist/ci/ci-sensor-loop.d.ts.map +1 -1
- package/dist/ci/ci-sensor-loop.js +35 -9
- package/dist/ci/ci-sensor-loop.js.map +1 -1
- package/dist/cli/connect/config-write.d.ts +87 -0
- package/dist/cli/connect/config-write.d.ts.map +1 -0
- package/dist/cli/connect/config-write.js +255 -0
- package/dist/cli/connect/config-write.js.map +1 -0
- package/dist/cli/connect/registration.d.ts +76 -0
- package/dist/cli/connect/registration.d.ts.map +1 -0
- package/dist/cli/connect/registration.js +207 -0
- package/dist/cli/connect/registration.js.map +1 -0
- package/dist/cli/connect.d.ts +3 -141
- package/dist/cli/connect.d.ts.map +1 -1
- package/dist/cli/connect.js +9 -441
- package/dist/cli/connect.js.map +1 -1
- package/dist/completion/coverage-stabilized.d.ts +1 -1
- package/dist/completion/coverage-stabilized.d.ts.map +1 -1
- package/dist/completion/coverage-stabilized.js +1 -1
- package/dist/completion/coverage-stabilized.js.map +1 -1
- package/dist/completion/escalation-provenance.d.ts +2 -1
- package/dist/completion/escalation-provenance.d.ts.map +1 -1
- package/dist/completion/escalation-provenance.js +2 -1
- package/dist/completion/escalation-provenance.js.map +1 -1
- package/dist/completion/escalations.js +1 -1
- package/dist/completion/escalations.js.map +1 -1
- package/dist/completion/event-escalations.d.ts +1 -1
- package/dist/completion/event-escalations.d.ts.map +1 -1
- package/dist/completion/event-escalations.js +1 -1
- package/dist/completion/event-escalations.js.map +1 -1
- package/dist/completion/event.d.ts +1 -1
- package/dist/completion/event.d.ts.map +1 -1
- package/dist/completion/event.js +2 -2
- package/dist/completion/event.js.map +1 -1
- package/dist/completion/machine-remediable-demotion.d.ts +1 -1
- package/dist/completion/machine-remediable-demotion.d.ts.map +1 -1
- package/dist/completion/machine-remediable-demotion.js +2 -1
- package/dist/completion/machine-remediable-demotion.js.map +1 -1
- package/dist/completion/read-only-pristine.d.ts +1 -1
- package/dist/completion/read-only-pristine.d.ts.map +1 -1
- package/dist/completion/read-only-pristine.js +1 -1
- package/dist/completion/read-only-pristine.js.map +1 -1
- package/dist/completion/review-exit-escalations.d.ts +1 -1
- package/dist/completion/review-exit-escalations.d.ts.map +1 -1
- package/dist/completion/review-exit-phase.d.ts +5 -2
- package/dist/completion/review-exit-phase.d.ts.map +1 -1
- package/dist/completion/review-exit-phase.js +5 -2
- package/dist/completion/review-exit-phase.js.map +1 -1
- package/dist/completion/settle-injection-sweep.d.ts +9 -7
- package/dist/completion/settle-injection-sweep.d.ts.map +1 -1
- package/dist/completion/settle-injection-sweep.js +17 -15
- package/dist/completion/settle-injection-sweep.js.map +1 -1
- package/dist/completion/state-cascade.js +2 -2
- package/dist/completion/state-cascade.js.map +1 -1
- package/dist/completion/status-advance-phase.d.ts.map +1 -1
- package/dist/completion/status-advance-phase.js +2 -1
- package/dist/completion/status-advance-phase.js.map +1 -1
- package/dist/completion/team-lifecycle.d.ts.map +1 -1
- package/dist/completion/team-lifecycle.js +2 -1
- package/dist/completion/team-lifecycle.js.map +1 -1
- package/dist/delivery/delivery-advance-block.d.ts +1 -1
- package/dist/delivery/delivery-advance-block.d.ts.map +1 -1
- package/dist/delivery/delivery-advance-block.js +1 -1
- package/dist/delivery/delivery-advance-block.js.map +1 -1
- package/dist/delivery/delivery-lifecycle.js +1 -1
- package/dist/delivery/delivery-lifecycle.js.map +1 -1
- package/dist/delivery/delivery-merge-ops.js +1 -1
- package/dist/delivery/delivery-merge-ops.js.map +1 -1
- package/dist/delivery/delivery-reconciler-shadow.d.ts +1 -1
- package/dist/delivery/delivery-reconciler-shadow.d.ts.map +1 -1
- package/dist/delivery/delivery-reconciler-shadow.js +1 -1
- package/dist/delivery/delivery-reconciler-shadow.js.map +1 -1
- package/dist/delivery/operator-hold.d.ts +1 -1
- package/dist/delivery/operator-hold.d.ts.map +1 -1
- package/dist/delivery/paused-revival.d.ts.map +1 -1
- package/dist/delivery/paused-revival.js +2 -1
- package/dist/delivery/paused-revival.js.map +1 -1
- package/dist/directive/close-loop-dispatcher.d.ts +35 -12
- package/dist/directive/close-loop-dispatcher.d.ts.map +1 -1
- package/dist/directive/close-loop-dispatcher.js +31 -11
- package/dist/directive/close-loop-dispatcher.js.map +1 -1
- package/dist/directive/directive-dispatch.d.ts +1 -1
- package/dist/directive/directive-dispatch.d.ts.map +1 -1
- package/dist/directive/directive-dispatch.js +1 -1
- package/dist/directive/directive-dispatch.js.map +1 -1
- package/dist/directive/directive-executor.js +1 -1
- package/dist/directive/directive-executor.js.map +1 -1
- package/dist/drift/staleness-sensor.js +1 -1
- package/dist/drift/staleness-sensor.js.map +1 -1
- package/dist/focus/focus-engine.js +3 -2
- package/dist/focus/focus-engine.js.map +1 -1
- package/dist/focus/focus-lifecycle.d.ts.map +1 -1
- package/dist/focus/focus-lifecycle.js +2 -1
- package/dist/focus/focus-lifecycle.js.map +1 -1
- package/dist/focus/focus-respawn-guard.d.ts +1 -1
- package/dist/focus/focus-respawn-guard.d.ts.map +1 -1
- package/dist/focus/focus-respawn-guard.js +1 -1
- package/dist/focus/focus-respawn-guard.js.map +1 -1
- package/dist/focus/listener-auto-advance.d.ts +4 -45
- package/dist/focus/listener-auto-advance.d.ts.map +1 -1
- package/dist/focus/listener-auto-advance.js +5 -238
- package/dist/focus/listener-auto-advance.js.map +1 -1
- package/dist/focus/listener-spawn.d.ts +62 -0
- package/dist/focus/listener-spawn.d.ts.map +1 -0
- package/dist/focus/listener-spawn.js +418 -0
- package/dist/focus/listener-spawn.js.map +1 -0
- package/dist/focus/listener-state.d.ts +57 -0
- package/dist/focus/listener-state.d.ts.map +1 -0
- package/dist/focus/listener-state.js +63 -0
- package/dist/focus/listener-state.js.map +1 -0
- package/dist/focus/listener.d.ts +0 -58
- package/dist/focus/listener.d.ts.map +1 -1
- package/dist/focus/listener.js +25 -453
- package/dist/focus/listener.js.map +1 -1
- package/dist/focus/verify-open-issues.d.ts +62 -0
- package/dist/focus/verify-open-issues.d.ts.map +1 -0
- package/dist/focus/verify-open-issues.js +265 -0
- package/dist/focus/verify-open-issues.js.map +1 -0
- package/dist/focus-stage-lifecycle/index.d.ts +1 -1
- package/dist/focus-stage-lifecycle/index.d.ts.map +1 -1
- package/dist/focus-stage-lifecycle/index.js +1 -1
- package/dist/focus-stage-lifecycle/index.js.map +1 -1
- package/dist/focus-stage-lifecycle/wedge-watchdog.d.ts +1 -1
- package/dist/focus-stage-lifecycle/wedge-watchdog.d.ts.map +1 -1
- package/dist/focus-stage-lifecycle/wedge-watchdog.js +1 -1
- package/dist/focus-stage-lifecycle/wedge-watchdog.js.map +1 -1
- package/dist/git/index.d.ts +2 -0
- package/dist/git/index.d.ts.map +1 -1
- package/dist/git/index.js +2 -0
- package/dist/git/index.js.map +1 -1
- package/dist/git/merge-strategies.d.ts +80 -0
- package/dist/git/merge-strategies.d.ts.map +1 -0
- package/dist/git/merge-strategies.js +218 -0
- package/dist/git/merge-strategies.js.map +1 -0
- package/dist/git/merge-to-main.d.ts +76 -0
- package/dist/git/merge-to-main.d.ts.map +1 -0
- package/dist/git/merge-to-main.js +256 -0
- package/dist/git/merge-to-main.js.map +1 -0
- package/dist/git/merge.d.ts +27 -81
- package/dist/git/merge.d.ts.map +1 -1
- package/dist/git/merge.js +4 -448
- package/dist/git/merge.js.map +1 -1
- package/dist/git/state-detector.js +1 -1
- package/dist/git/state-detector.js.map +1 -1
- package/dist/loops/engine.js +1 -1
- package/dist/loops/engine.js.map +1 -1
- package/dist/merge/focus-merge.d.ts +1 -1
- package/dist/merge/focus-merge.d.ts.map +1 -1
- package/dist/merge/focus-merge.js +1 -1
- package/dist/merge/focus-merge.js.map +1 -1
- package/dist/merge/merge-back-loop.d.ts +1 -1
- package/dist/merge/merge-back-loop.d.ts.map +1 -1
- package/dist/merge/merge-back-loop.js +2 -2
- package/dist/merge/merge-back-loop.js.map +1 -1
- package/dist/merge/merge-detector.js +1 -1
- package/dist/merge/merge-detector.js.map +1 -1
- package/dist/nexus/return.js +1 -1
- package/dist/nexus/return.js.map +1 -1
- package/dist/prd/controller-shared.d.ts +223 -0
- package/dist/prd/controller-shared.d.ts.map +1 -0
- package/dist/prd/controller-shared.js +77 -0
- package/dist/prd/controller-shared.js.map +1 -0
- package/dist/prd/controller.d.ts +5 -256
- package/dist/prd/controller.d.ts.map +1 -1
- package/dist/prd/controller.js +9 -166
- package/dist/prd/controller.js.map +1 -1
- package/dist/prd/done-sweep.d.ts +55 -0
- package/dist/prd/done-sweep.d.ts.map +1 -0
- package/dist/prd/done-sweep.js +112 -0
- package/dist/prd/done-sweep.js.map +1 -0
- package/dist/prompt-sections/execution-sections.d.ts +1 -1
- package/dist/prompt-sections/execution-sections.d.ts.map +1 -1
- package/dist/prompt-sections/execution-sections.js +12 -2
- package/dist/prompt-sections/execution-sections.js.map +1 -1
- package/dist/prompt-sections/shared.d.ts +1 -1
- package/dist/prompt-sections/shared.d.ts.map +1 -1
- package/dist/queries/agent-resolvable-promotion.d.ts +1 -1
- package/dist/queries/agent-resolvable-promotion.d.ts.map +1 -1
- package/dist/queries/agent-resolvable-promotion.js +2 -1
- package/dist/queries/agent-resolvable-promotion.js.map +1 -1
- package/dist/queries/deliveries.d.ts +0 -195
- package/dist/queries/deliveries.d.ts.map +1 -1
- package/dist/queries/deliveries.js +1 -83
- package/dist/queries/deliveries.js.map +1 -1
- package/dist/queries/escalation-consumption.d.ts +120 -0
- package/dist/queries/escalation-consumption.d.ts.map +1 -0
- package/dist/queries/escalation-consumption.js +143 -0
- package/dist/queries/escalation-consumption.js.map +1 -0
- package/dist/queries/escalation-dispositions.d.ts +19 -0
- package/dist/queries/escalation-dispositions.d.ts.map +1 -0
- package/dist/queries/escalation-dispositions.js +18 -0
- package/dist/queries/escalation-dispositions.js.map +1 -0
- package/dist/queries/escalation-suppression.d.ts +1 -1
- package/dist/queries/escalation-suppression.d.ts.map +1 -1
- package/dist/queries/escalation-suppression.js +1 -1
- package/dist/queries/escalation-suppression.js.map +1 -1
- package/dist/queries/escalations.d.ts +343 -0
- package/dist/queries/escalations.d.ts.map +1 -0
- package/dist/queries/escalations.js +493 -0
- package/dist/queries/escalations.js.map +1 -0
- package/dist/queries/focus-discovery.d.ts +204 -0
- package/dist/queries/focus-discovery.d.ts.map +1 -0
- package/dist/queries/focus-discovery.js +92 -0
- package/dist/queries/focus-discovery.js.map +1 -0
- package/dist/queries/index.d.ts +4 -2
- package/dist/queries/index.d.ts.map +1 -1
- package/dist/queries/index.js +4 -2
- package/dist/queries/index.js.map +1 -1
- package/dist/queries/issues.d.ts +8 -436
- package/dist/queries/issues.d.ts.map +1 -1
- package/dist/queries/issues.js +7 -615
- package/dist/queries/issues.js.map +1 -1
- package/dist/resolvers/agent-escalations.d.ts +20 -0
- package/dist/resolvers/agent-escalations.d.ts.map +1 -1
- package/dist/resolvers/agent-escalations.js +40 -10
- package/dist/resolvers/agent-escalations.js.map +1 -1
- package/dist/resolvers/index.d.ts +2 -2
- package/dist/resolvers/index.d.ts.map +1 -1
- package/dist/resolvers/index.js +1 -1
- package/dist/resolvers/index.js.map +1 -1
- package/dist/resolvers/shared/git-diff.d.ts +99 -18
- package/dist/resolvers/shared/git-diff.d.ts.map +1 -1
- package/dist/resolvers/shared/git-diff.js +98 -27
- package/dist/resolvers/shared/git-diff.js.map +1 -1
- package/dist/shell/control-state.d.ts +1 -1
- package/dist/shell/control-state.d.ts.map +1 -1
- package/dist/shell/control-state.js +1 -1
- package/dist/shell/control-state.js.map +1 -1
- package/dist/shell/crash-recovery.d.ts +1 -1
- package/dist/shell/crash-recovery.d.ts.map +1 -1
- package/dist/shell/crash-recovery.js +2 -2
- package/dist/shell/crash-recovery.js.map +1 -1
- package/dist/shell/install-probe-tick.d.ts +1 -1
- package/dist/shell/install-probe-tick.d.ts.map +1 -1
- package/dist/shell/install-probe-tick.js +1 -1
- package/dist/shell/install-probe-tick.js.map +1 -1
- package/dist/shell/unified-shell.js +1 -1
- package/dist/shell/unified-shell.js.map +1 -1
- package/dist/spawn/model-routing.d.ts +1 -1
- package/dist/spawn/model-routing.d.ts.map +1 -1
- package/dist/spawn/model-routing.js +1 -1
- package/dist/spawn/model-routing.js.map +1 -1
- package/dist/spawn/prompt-assembly.d.ts +1 -1
- package/dist/spawn/prompt-assembly.d.ts.map +1 -1
- package/dist/spawn/prompt-assembly.js +1 -1
- package/dist/spawn/prompt-assembly.js.map +1 -1
- package/dist/spawn/spawn-decision.js +1 -1
- package/dist/spawn/spawn-decision.js.map +1 -1
- package/dist/spawn/worktree-session.d.ts +1 -1
- package/dist/spawn/worktree-session.d.ts.map +1 -1
- package/dist/spawn/worktree-session.js +1 -1
- package/dist/spawn/worktree-session.js.map +1 -1
- package/dist/templates/claude-md.d.ts +1 -1
- package/dist/templates/claude-md.d.ts.map +1 -1
- package/dist/templates/claude-md.js +12 -0
- package/dist/templates/claude-md.js.map +1 -1
- package/dist/verify/verification-default-on.d.ts +1 -1
- package/dist/verify/verification-default-on.d.ts.map +1 -1
- package/dist/verify/verification-deps.d.ts +1 -1
- package/dist/verify/verification-deps.d.ts.map +1 -1
- package/dist/verify/verification-engine.d.ts.map +1 -1
- package/dist/verify/verification-engine.js +20 -12
- package/dist/verify/verification-engine.js.map +1 -1
- package/dist/worktree/provision.js +1 -1
- package/dist/worktree/provision.js.map +1 -1
- package/dist/worktree/scratch-guard.d.ts.map +1 -1
- package/dist/worktree/scratch-guard.js +23 -2
- package/dist/worktree/scratch-guard.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git merge strategy + worktree-recovery mechanics.
|
|
3
|
+
*
|
|
4
|
+
* The low-level merge dispatch (fast-forward / rebase / merge-commit / squash)
|
|
5
|
+
* and the failed-merge worktree recovery primitives. Split out of git/merge.ts
|
|
6
|
+
* (file-size limit); this is the LEAF of the git-merge module family -- it calls
|
|
7
|
+
* nothing in git/merge.ts, so merge.ts and git/merge-to-main.ts both import from
|
|
8
|
+
* here without a cycle. Focus-level merge ORCHESTRATION stays in merge/.
|
|
9
|
+
*/
|
|
10
|
+
import { type MergeResult, type MergeStrategy } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Extract the list of conflicted files from git status.
|
|
13
|
+
* Returns an empty array if the command fails or there are no conflicts.
|
|
14
|
+
*/
|
|
15
|
+
export declare function extractConflictedFiles(repoPath: string): string[];
|
|
16
|
+
/**
|
|
17
|
+
* Recover a merge worktree left dirty by an EARLIER failed merge, BEFORE the next
|
|
18
|
+
* merge runs, so an already-wedged worktree heals itself with no operator. The
|
|
19
|
+
* residue is detected by a non-empty `git status --porcelain` (staged / unstaged
|
|
20
|
+
* / untracked) -- deliberately NOT by MERGE_HEAD: the 2026-08-18 wedge held 85
|
|
21
|
+
* staged files with MERGE_HEAD already cleared and AUTO_MERGE present, so a
|
|
22
|
+
* MERGE_HEAD probe would miss it. Any residue is discarded via
|
|
23
|
+
* restoreWorktreeAfterFailedMerge, which moves no branch ref -- focus-branch
|
|
24
|
+
* commits are untouched. Merge-worktree only; idempotent (a clean worktree is a
|
|
25
|
+
* no-op).
|
|
26
|
+
*
|
|
27
|
+
* CONFIRMS the outcome rather than recovering-and-hoping: it re-reads
|
|
28
|
+
* `git status --porcelain` AFTER any recovery and reports whether the worktree
|
|
29
|
+
* is now clean. `clean` is true only when the worktree is empty at exit -- it
|
|
30
|
+
* was already clean, or the recovery restored it. It is false when residue
|
|
31
|
+
* survived the recovery attempt, or when status itself could not be read (so
|
|
32
|
+
* cleanliness cannot be vouched for). A merge must REFUSE to run when this
|
|
33
|
+
* returns clean:false: running against an unconfirmed worktree is exactly the
|
|
34
|
+
* wedge this guard exists to prevent.
|
|
35
|
+
*/
|
|
36
|
+
export declare function recoverDirtyMergeWorktree(repoPath: string): {
|
|
37
|
+
clean: boolean;
|
|
38
|
+
residue?: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Attempt a fast-forward-only merge.
|
|
42
|
+
* Returns a MergeResult; on failure, no abort is needed (ff-only is atomic).
|
|
43
|
+
*/
|
|
44
|
+
export declare function tryFastForwardMerge(repoPath: string, sourceBranch: string): MergeResult;
|
|
45
|
+
/**
|
|
46
|
+
* Restore the merge worktree to the clean commit + index it had before a failed
|
|
47
|
+
* merge attempt. The conflict branches abort explicitly; this covers EVERY OTHER
|
|
48
|
+
* failure -- a commit-msg hook rejection (leaves MERGE_HEAD set + staged), a
|
|
49
|
+
* squash that staged changes with no MERGE_HEAD, or an unrecognised error --
|
|
50
|
+
* which git otherwise leaves as a staged, uncommitted merge that NOTHING clears.
|
|
51
|
+
* Because the merge worktree is shared by every focus in the product, that
|
|
52
|
+
* residue would fail the next focus's merge too; clearing it here contains one
|
|
53
|
+
* focus's failure to that focus. It discards ONLY the failed-merge residue --
|
|
54
|
+
* `git merge --abort` (or, when no merge is in progress, `reset --hard HEAD`)
|
|
55
|
+
* moves no branch ref, so commits on any focus branch are untouched. Only ever
|
|
56
|
+
* called against the merge worktree (never a developer checkout).
|
|
57
|
+
*/
|
|
58
|
+
export declare function restoreWorktreeAfterFailedMerge(repoPath: string): void;
|
|
59
|
+
/**
|
|
60
|
+
* Attempt a merge-commit merge (--no-ff).
|
|
61
|
+
* On conflict, aborts the merge and returns conflicted file details.
|
|
62
|
+
*/
|
|
63
|
+
export declare function tryMergeCommit(repoPath: string, sourceBranch: string, commitMessage: string): MergeResult;
|
|
64
|
+
/**
|
|
65
|
+
* Attempt a squash merge: stages all changes from the source branch as a
|
|
66
|
+
* single commit on the current branch.
|
|
67
|
+
*
|
|
68
|
+
* 1. `git merge --squash <sourceBranch>` -- stages all diffs without committing
|
|
69
|
+
* 2. `git commit -m <commitMessage>` -- creates a single squashed commit
|
|
70
|
+
* 3. Captures the resulting commit SHA via `git rev-parse HEAD`
|
|
71
|
+
*
|
|
72
|
+
* On conflict, aborts the merge and returns conflicted file details.
|
|
73
|
+
*/
|
|
74
|
+
export declare function trySquashMerge(repoPath: string, sourceBranch: string, commitMessage: string): MergeResult;
|
|
75
|
+
/**
|
|
76
|
+
* Dispatch to the appropriate merge strategy helper.
|
|
77
|
+
* For 'merge-commit', tries fast-forward first, then falls back to --no-ff.
|
|
78
|
+
*/
|
|
79
|
+
export declare function applyMergeStrategy(repoPath: string, sourceBranch: string, targetBranch: string, strategy: MergeStrategy): MergeResult;
|
|
80
|
+
//# sourceMappingURL=merge-strategies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-strategies.d.ts","sourceRoot":"","sources":["../../src/git/merge-strategies.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAc,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAK9E;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAMjE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CA0BhG;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB,WAAW,CASb;AAwBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,+BAA+B,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAStE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,GACpB,WAAW,CAgCb;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,GACpB,WAAW,CAoDb;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,aAAa,GACtB,WAAW,CAmBb"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git merge strategy + worktree-recovery mechanics.
|
|
3
|
+
*
|
|
4
|
+
* The low-level merge dispatch (fast-forward / rebase / merge-commit / squash)
|
|
5
|
+
* and the failed-merge worktree recovery primitives. Split out of git/merge.ts
|
|
6
|
+
* (file-size limit); this is the LEAF of the git-merge module family -- it calls
|
|
7
|
+
* nothing in git/merge.ts, so merge.ts and git/merge-to-main.ts both import from
|
|
8
|
+
* here without a cycle. Focus-level merge ORCHESTRATION stays in merge/.
|
|
9
|
+
*/
|
|
10
|
+
import { runGitSync } from './types.js';
|
|
11
|
+
import { createLogger } from '../shell/log.js';
|
|
12
|
+
const log = createLogger({ module: 'CI' });
|
|
13
|
+
/**
|
|
14
|
+
* Extract the list of conflicted files from git status.
|
|
15
|
+
* Returns an empty array if the command fails or there are no conflicts.
|
|
16
|
+
*/
|
|
17
|
+
export function extractConflictedFiles(repoPath) {
|
|
18
|
+
const statusResult = runGitSync(['diff', '--name-only', '--diff-filter=U'], repoPath);
|
|
19
|
+
if (!statusResult.success) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
return statusResult.output.split('\n').filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Recover a merge worktree left dirty by an EARLIER failed merge, BEFORE the next
|
|
26
|
+
* merge runs, so an already-wedged worktree heals itself with no operator. The
|
|
27
|
+
* residue is detected by a non-empty `git status --porcelain` (staged / unstaged
|
|
28
|
+
* / untracked) -- deliberately NOT by MERGE_HEAD: the 2026-08-18 wedge held 85
|
|
29
|
+
* staged files with MERGE_HEAD already cleared and AUTO_MERGE present, so a
|
|
30
|
+
* MERGE_HEAD probe would miss it. Any residue is discarded via
|
|
31
|
+
* restoreWorktreeAfterFailedMerge, which moves no branch ref -- focus-branch
|
|
32
|
+
* commits are untouched. Merge-worktree only; idempotent (a clean worktree is a
|
|
33
|
+
* no-op).
|
|
34
|
+
*
|
|
35
|
+
* CONFIRMS the outcome rather than recovering-and-hoping: it re-reads
|
|
36
|
+
* `git status --porcelain` AFTER any recovery and reports whether the worktree
|
|
37
|
+
* is now clean. `clean` is true only when the worktree is empty at exit -- it
|
|
38
|
+
* was already clean, or the recovery restored it. It is false when residue
|
|
39
|
+
* survived the recovery attempt, or when status itself could not be read (so
|
|
40
|
+
* cleanliness cannot be vouched for). A merge must REFUSE to run when this
|
|
41
|
+
* returns clean:false: running against an unconfirmed worktree is exactly the
|
|
42
|
+
* wedge this guard exists to prevent.
|
|
43
|
+
*/
|
|
44
|
+
export function recoverDirtyMergeWorktree(repoPath) {
|
|
45
|
+
const status = runGitSync(['status', '--porcelain'], repoPath);
|
|
46
|
+
if (!status.success) {
|
|
47
|
+
// Can't read status -> can't confirm clean. Treat as unsafe to merge.
|
|
48
|
+
return { clean: false, residue: `git status failed: ${status.error}` };
|
|
49
|
+
}
|
|
50
|
+
if (status.output.trim().length === 0) {
|
|
51
|
+
return { clean: true };
|
|
52
|
+
}
|
|
53
|
+
log.warn(`[merge] Merge worktree was left dirty by an earlier failed merge ` +
|
|
54
|
+
`(git status --porcelain non-empty); recovering it before the next merge ` +
|
|
55
|
+
`so an already-wedged worktree does not require an operator.`);
|
|
56
|
+
restoreWorktreeAfterFailedMerge(repoPath);
|
|
57
|
+
// Confirm the recovery actually cleaned the worktree before we let a merge run.
|
|
58
|
+
const after = runGitSync(['status', '--porcelain'], repoPath);
|
|
59
|
+
if (after.success && after.output.trim().length === 0) {
|
|
60
|
+
return { clean: true };
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
clean: false,
|
|
64
|
+
residue: after.success ? after.output.trim() : `git status failed: ${after.error}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Attempt a fast-forward-only merge.
|
|
69
|
+
* Returns a MergeResult; on failure, no abort is needed (ff-only is atomic).
|
|
70
|
+
*/
|
|
71
|
+
export function tryFastForwardMerge(repoPath, sourceBranch) {
|
|
72
|
+
const ffResult = runGitSync(['merge', '--ff-only', sourceBranch], repoPath);
|
|
73
|
+
if (ffResult.success) {
|
|
74
|
+
return { success: true };
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: `Fast-forward merge not possible: ${ffResult.error}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Attempt a rebase merge.
|
|
83
|
+
* On conflict, aborts the rebase and returns conflicted file details.
|
|
84
|
+
*/
|
|
85
|
+
function tryRebaseMerge(repoPath, sourceBranch) {
|
|
86
|
+
const rebaseResult = runGitSync(['rebase', sourceBranch], repoPath);
|
|
87
|
+
if (rebaseResult.success) {
|
|
88
|
+
return { success: true };
|
|
89
|
+
}
|
|
90
|
+
const conflictedFiles = extractConflictedFiles(repoPath);
|
|
91
|
+
runGitSync(['rebase', '--abort'], repoPath);
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
error: `Rebase failed: ${conflictedFiles.length} file(s) conflicted`,
|
|
95
|
+
conflictedFiles,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Restore the merge worktree to the clean commit + index it had before a failed
|
|
100
|
+
* merge attempt. The conflict branches abort explicitly; this covers EVERY OTHER
|
|
101
|
+
* failure -- a commit-msg hook rejection (leaves MERGE_HEAD set + staged), a
|
|
102
|
+
* squash that staged changes with no MERGE_HEAD, or an unrecognised error --
|
|
103
|
+
* which git otherwise leaves as a staged, uncommitted merge that NOTHING clears.
|
|
104
|
+
* Because the merge worktree is shared by every focus in the product, that
|
|
105
|
+
* residue would fail the next focus's merge too; clearing it here contains one
|
|
106
|
+
* focus's failure to that focus. It discards ONLY the failed-merge residue --
|
|
107
|
+
* `git merge --abort` (or, when no merge is in progress, `reset --hard HEAD`)
|
|
108
|
+
* moves no branch ref, so commits on any focus branch are untouched. Only ever
|
|
109
|
+
* called against the merge worktree (never a developer checkout).
|
|
110
|
+
*/
|
|
111
|
+
export function restoreWorktreeAfterFailedMerge(repoPath) {
|
|
112
|
+
// MERGE_HEAD present (a --no-ff merge that stopped, e.g. hook-rejected) aborts
|
|
113
|
+
// cleanly. --squash stages changes WITHOUT a MERGE_HEAD, so abort is a no-op
|
|
114
|
+
// there; fall back to a hard reset + untracked clean to drop the staged residue.
|
|
115
|
+
const abort = runGitSync(['merge', '--abort'], repoPath);
|
|
116
|
+
if (!abort.success) {
|
|
117
|
+
runGitSync(['reset', '--hard', 'HEAD'], repoPath);
|
|
118
|
+
runGitSync(['clean', '-fd'], repoPath);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Attempt a merge-commit merge (--no-ff).
|
|
123
|
+
* On conflict, aborts the merge and returns conflicted file details.
|
|
124
|
+
*/
|
|
125
|
+
export function tryMergeCommit(repoPath, sourceBranch, commitMessage) {
|
|
126
|
+
const mergeResult = runGitSync(['merge', sourceBranch, '--no-ff', '-m', commitMessage], repoPath);
|
|
127
|
+
if (mergeResult.success) {
|
|
128
|
+
return { success: true };
|
|
129
|
+
}
|
|
130
|
+
const isConflict = mergeResult.error.includes('CONFLICT') ||
|
|
131
|
+
mergeResult.error.includes('Automatic merge failed') ||
|
|
132
|
+
mergeResult.output.includes('CONFLICT');
|
|
133
|
+
if (isConflict) {
|
|
134
|
+
const conflictedFiles = extractConflictedFiles(repoPath);
|
|
135
|
+
runGitSync(['merge', '--abort'], repoPath);
|
|
136
|
+
return {
|
|
137
|
+
success: false,
|
|
138
|
+
error: `Merge conflict: ${conflictedFiles.length} file(s) conflicted`,
|
|
139
|
+
conflictedFiles,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// Non-conflict failure (hook rejection / unrecognised error) still leaves a
|
|
143
|
+
// staged, uncommitted merge behind -- restore the worktree so it does not
|
|
144
|
+
// poison the next focus's merge.
|
|
145
|
+
restoreWorktreeAfterFailedMerge(repoPath);
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
error: `Merge failed: ${mergeResult.error}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Attempt a squash merge: stages all changes from the source branch as a
|
|
153
|
+
* single commit on the current branch.
|
|
154
|
+
*
|
|
155
|
+
* 1. `git merge --squash <sourceBranch>` -- stages all diffs without committing
|
|
156
|
+
* 2. `git commit -m <commitMessage>` -- creates a single squashed commit
|
|
157
|
+
* 3. Captures the resulting commit SHA via `git rev-parse HEAD`
|
|
158
|
+
*
|
|
159
|
+
* On conflict, aborts the merge and returns conflicted file details.
|
|
160
|
+
*/
|
|
161
|
+
export function trySquashMerge(repoPath, sourceBranch, commitMessage) {
|
|
162
|
+
const squashResult = runGitSync(['merge', '--squash', sourceBranch], repoPath);
|
|
163
|
+
if (!squashResult.success) {
|
|
164
|
+
const isConflict = squashResult.error.includes('CONFLICT') ||
|
|
165
|
+
squashResult.error.includes('Automatic merge failed') ||
|
|
166
|
+
squashResult.output.includes('CONFLICT');
|
|
167
|
+
if (isConflict) {
|
|
168
|
+
const conflictedFiles = extractConflictedFiles(repoPath);
|
|
169
|
+
runGitSync(['merge', '--abort'], repoPath);
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
error: `Squash merge conflict: ${conflictedFiles.length} file(s) conflicted`,
|
|
173
|
+
conflictedFiles,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// Non-conflict squash failure leaves the staged squash residue behind
|
|
177
|
+
// (no MERGE_HEAD) -- restore so it does not poison the next focus's merge.
|
|
178
|
+
restoreWorktreeAfterFailedMerge(repoPath);
|
|
179
|
+
return {
|
|
180
|
+
success: false,
|
|
181
|
+
error: `Squash merge failed: ${squashResult.error}`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
// Commit the squashed changes
|
|
185
|
+
const commitResult = runGitSync(['commit', '-m', commitMessage], repoPath);
|
|
186
|
+
if (!commitResult.success) {
|
|
187
|
+
// The squash staged everything; a failed commit (e.g. a rejecting hook)
|
|
188
|
+
// leaves it staged. Restore so the residue does not wedge the next merge.
|
|
189
|
+
restoreWorktreeAfterFailedMerge(repoPath);
|
|
190
|
+
return {
|
|
191
|
+
success: false,
|
|
192
|
+
error: `Squash merge commit failed: ${commitResult.error}`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// Capture the resulting commit SHA
|
|
196
|
+
const shaResult = runGitSync(['rev-parse', 'HEAD'], repoPath);
|
|
197
|
+
const commitSha = shaResult.success ? shaResult.output : undefined;
|
|
198
|
+
return { success: true, commitSha };
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Dispatch to the appropriate merge strategy helper.
|
|
202
|
+
* For 'merge-commit', tries fast-forward first, then falls back to --no-ff.
|
|
203
|
+
*/
|
|
204
|
+
export function applyMergeStrategy(repoPath, sourceBranch, targetBranch, strategy) {
|
|
205
|
+
if (strategy === 'fast-forward') {
|
|
206
|
+
return tryFastForwardMerge(repoPath, sourceBranch);
|
|
207
|
+
}
|
|
208
|
+
if (strategy === 'rebase') {
|
|
209
|
+
return tryRebaseMerge(repoPath, sourceBranch);
|
|
210
|
+
}
|
|
211
|
+
// 'merge-commit': try ff first, fall back to --no-ff
|
|
212
|
+
const ffResult = tryFastForwardMerge(repoPath, sourceBranch);
|
|
213
|
+
if (ffResult.success) {
|
|
214
|
+
return ffResult;
|
|
215
|
+
}
|
|
216
|
+
return tryMergeCommit(repoPath, sourceBranch, `Advance ${targetBranch} to ${sourceBranch}`);
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=merge-strategies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-strategies.js","sourceRoot":"","sources":["../../src/git/merge-strategies.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The integration -> main merge (the single write-to-main point).
|
|
3
|
+
*
|
|
4
|
+
* Split out of git/merge.ts (file-size limit). mergeIntegrationToMain is the one
|
|
5
|
+
* place refs/heads/main advances, so the fail-closed CI green-wait gate lives
|
|
6
|
+
* here. Pure CONSUMER of the git-merge family: it imports the strategy mechanics
|
|
7
|
+
* from git/merge-strategies.ts and the shared primitives from git/merge.ts, and
|
|
8
|
+
* nothing in either imports back -> no cycle.
|
|
9
|
+
*/
|
|
10
|
+
import type { DaemonConfig } from '../types/index.js';
|
|
11
|
+
import { type MergeResult, type MergeStrategy } from './types.js';
|
|
12
|
+
import { type CiStatusReader, type CiMergeGateDecision } from '../ci/ci-status.js';
|
|
13
|
+
export interface MergeIntegrationToMainParams {
|
|
14
|
+
config: DaemonConfig;
|
|
15
|
+
/** Focus ID for log attribution / push-failure escalations. */
|
|
16
|
+
focusId: string;
|
|
17
|
+
/** Focus name for logging/commit messages. */
|
|
18
|
+
focusName: string;
|
|
19
|
+
/** Whether to also push main to remote (CD). */
|
|
20
|
+
pushToRemote: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Merge strategy honored when integrating into main. Defaults to
|
|
23
|
+
* `merge-commit` (which itself attempts fast-forward first, then falls
|
|
24
|
+
* back to `--no-ff`) so legacy callers that omit the field get the
|
|
25
|
+
* pre-existing behavior.
|
|
26
|
+
*/
|
|
27
|
+
mergeStrategy?: MergeStrategy;
|
|
28
|
+
/**
|
|
29
|
+
* The green-wait CI status reader, evaluated at THIS write point -- the single
|
|
30
|
+
* place `refs/heads/main` is advanced -- so the gate governs every daemon route
|
|
31
|
+
* to main identically, not whichever call path performed the merge. REQUIRED so
|
|
32
|
+
* no caller can advance main without making the CI decision explicit:
|
|
33
|
+
* - a reader (`resolveCiStatusReader()`) => the merged SHA's status is read
|
|
34
|
+
* and `resolveCiMergeGate` decides; pending/red/unknown HOLD (fail-closed),
|
|
35
|
+
* green proceeds. On a hold the ref is NOT advanced and the result carries
|
|
36
|
+
* `ciHeld` (not an error -- nothing was written, nothing to roll back).
|
|
37
|
+
* - `undefined` => an explicit, visible opt-out (used by tests and legacy
|
|
38
|
+
* callers with no CI source); the gate is skipped and the merge proceeds
|
|
39
|
+
* exactly as before.
|
|
40
|
+
*/
|
|
41
|
+
getCiStatus: CiStatusReader | undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolved remote name + URL describing the push target for a CD push.
|
|
45
|
+
* Exported only so the focus-completion / focus-team-lifecycle log lines
|
|
46
|
+
* can surface the remote without re-querying git.
|
|
47
|
+
*/
|
|
48
|
+
export interface PushedRemoteInfo {
|
|
49
|
+
/** Remote name (always `origin` today). */
|
|
50
|
+
name: string;
|
|
51
|
+
/** Remote URL as configured in the repo. */
|
|
52
|
+
url: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Merge the integration branch into the default (main) branch.
|
|
56
|
+
*
|
|
57
|
+
* Called when a focus completes with CI enabled. Optionally pushes
|
|
58
|
+
* to remote when CD is also enabled. The push step is gated by
|
|
59
|
+
* `pushToRemote` and only runs when an `origin` remote is configured.
|
|
60
|
+
*
|
|
61
|
+
* Uses the merge worktree and merge lock (same as mergeToIntegration)
|
|
62
|
+
* to avoid concurrent merge conflicts. The merge worktree checks main out
|
|
63
|
+
* DETACHED and advances refs/heads/main via `git update-ref`, so the merge
|
|
64
|
+
* completes even when main is attached in the daemon's primary worktree
|
|
65
|
+
* (where `git checkout main` would fail with "'main' is already used by
|
|
66
|
+
* worktree"). Clean worktrees holding main are re-synced afterward.
|
|
67
|
+
*
|
|
68
|
+
* The result's `pushedTo` field is set only when CD push happened and
|
|
69
|
+
* succeeded. Callers can read it to emit a focus-attributed log line
|
|
70
|
+
* with the resolved remote name + URL.
|
|
71
|
+
*/
|
|
72
|
+
export declare function mergeIntegrationToMain(params: MergeIntegrationToMainParams): Promise<MergeResult & {
|
|
73
|
+
pushedTo?: PushedRemoteInfo;
|
|
74
|
+
ciHeld?: CiMergeGateDecision;
|
|
75
|
+
}>;
|
|
76
|
+
//# sourceMappingURL=merge-to-main.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-to-main.d.ts","sourceRoot":"","sources":["../../src/git/merge-to-main.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAc,KAAK,WAAW,EAAE,KAAK,aAAa,EAAwB,MAAM,YAAY,CAAC;AAIpG,OAAO,EAGL,KAAK,cAAc,EAEnB,KAAK,mBAAmB,EACzB,MAAM,oBAAoB,CAAC;AAU5B,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,YAAY,CAAC;IACrB,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,YAAY,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;;;;;;;;;OAYG;IACH,WAAW,EAAE,cAAc,GAAG,SAAS,CAAC;CACzC;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,4BAA4B,GACnC,OAAO,CAAC,WAAW,GAAG;IAAE,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAAC,MAAM,CAAC,EAAE,mBAAmB,CAAA;CAAE,CAAC,CAoPtF"}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The integration -> main merge (the single write-to-main point).
|
|
3
|
+
*
|
|
4
|
+
* Split out of git/merge.ts (file-size limit). mergeIntegrationToMain is the one
|
|
5
|
+
* place refs/heads/main advances, so the fail-closed CI green-wait gate lives
|
|
6
|
+
* here. Pure CONSUMER of the git-merge family: it imports the strategy mechanics
|
|
7
|
+
* from git/merge-strategies.ts and the shared primitives from git/merge.ts, and
|
|
8
|
+
* nothing in either imports back -> no cycle.
|
|
9
|
+
*/
|
|
10
|
+
import { runGitSync, getMergeWorktreePath } from './types.js';
|
|
11
|
+
import { withMergeLock } from './merge-lock.js';
|
|
12
|
+
import { getDefaultBranch } from './branch.js';
|
|
13
|
+
import { resolveRef } from './utils.js';
|
|
14
|
+
import { resolveCiMergeGate, classifyCiMergeWait, } from '../ci/ci-status.js';
|
|
15
|
+
import { describeWaitReachability } from '../delivery/wait-reachability.js';
|
|
16
|
+
import { createLogger } from '../shell/log.js';
|
|
17
|
+
import { applyMergeStrategy, recoverDirtyMergeWorktree } from './merge-strategies.js';
|
|
18
|
+
import { hasOriginRemote, worktreesWithBranchCheckedOut, hasTrackedChanges, handlePushFailureWithRollback } from './merge.js';
|
|
19
|
+
const log = createLogger({ module: 'CI' });
|
|
20
|
+
/**
|
|
21
|
+
* Merge the integration branch into the default (main) branch.
|
|
22
|
+
*
|
|
23
|
+
* Called when a focus completes with CI enabled. Optionally pushes
|
|
24
|
+
* to remote when CD is also enabled. The push step is gated by
|
|
25
|
+
* `pushToRemote` and only runs when an `origin` remote is configured.
|
|
26
|
+
*
|
|
27
|
+
* Uses the merge worktree and merge lock (same as mergeToIntegration)
|
|
28
|
+
* to avoid concurrent merge conflicts. The merge worktree checks main out
|
|
29
|
+
* DETACHED and advances refs/heads/main via `git update-ref`, so the merge
|
|
30
|
+
* completes even when main is attached in the daemon's primary worktree
|
|
31
|
+
* (where `git checkout main` would fail with "'main' is already used by
|
|
32
|
+
* worktree"). Clean worktrees holding main are re-synced afterward.
|
|
33
|
+
*
|
|
34
|
+
* The result's `pushedTo` field is set only when CD push happened and
|
|
35
|
+
* succeeded. Callers can read it to emit a focus-attributed log line
|
|
36
|
+
* with the resolved remote name + URL.
|
|
37
|
+
*/
|
|
38
|
+
export function mergeIntegrationToMain(params) {
|
|
39
|
+
const { config, focusName, pushToRemote, mergeStrategy = 'merge-commit', getCiStatus } = params;
|
|
40
|
+
return withMergeLock(async () => {
|
|
41
|
+
const mwt = getMergeWorktreePath(config);
|
|
42
|
+
const mainBranch = getDefaultBranch(config.repoPath);
|
|
43
|
+
// Same self-heal as mergeToIntegration: this route shares the merge worktree,
|
|
44
|
+
// so an earlier failed merge's residue would break the detached checkout
|
|
45
|
+
// below. REFUSE if the worktree cannot be CONFIRMED clean afterward.
|
|
46
|
+
const recovery = recoverDirtyMergeWorktree(mwt);
|
|
47
|
+
if (!recovery.clean) {
|
|
48
|
+
return {
|
|
49
|
+
success: false,
|
|
50
|
+
error: `Merge worktree could not be confirmed clean before merging ${config.integrationBranch} into ` +
|
|
51
|
+
`${mainBranch}; residue survived recovery: ${recovery.residue ?? 'unknown'}`,
|
|
52
|
+
sourceBranch: config.integrationBranch,
|
|
53
|
+
targetBranch: mainBranch,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const withRemote = hasOriginRemote(config.repoPath);
|
|
57
|
+
// Fail early when a CD push was requested but no remote is configured.
|
|
58
|
+
// Returning a push-failure result (rather than silently skipping) lets
|
|
59
|
+
// the caller's shouldFileCdPushEscalation check fire so the operator
|
|
60
|
+
// sees a visible escalation instead of a silent no-op.
|
|
61
|
+
if (pushToRemote && !withRemote) {
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
error: 'Push failed: no origin remote configured for this repository',
|
|
65
|
+
sourceBranch: config.integrationBranch,
|
|
66
|
+
targetBranch: mainBranch,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// Fetch latest refs
|
|
70
|
+
if (withRemote) {
|
|
71
|
+
runGitSync(['fetch', 'origin'], config.repoPath);
|
|
72
|
+
}
|
|
73
|
+
// ── Green-wait gate (the single write-to-main point) ────────────────
|
|
74
|
+
// This is the ONE place refs/heads/main advances, so the CI verdict is
|
|
75
|
+
// evaluated HERE -- not in whichever completion/eager code path called us.
|
|
76
|
+
// Every daemon route to main inherits the same fail-closed gate. With a
|
|
77
|
+
// reader wired, the SHA about to reach main (the integration tip) must be
|
|
78
|
+
// GREEN; pending/red/unknown HOLD without advancing the ref (nothing was
|
|
79
|
+
// written -- no rollback, and the caller reports a hold, never an error or
|
|
80
|
+
// an escalation). An explicit `undefined` reader skips the gate (opt-out).
|
|
81
|
+
if (getCiStatus) {
|
|
82
|
+
let mergedSha = '';
|
|
83
|
+
try {
|
|
84
|
+
// resolveRef checks the git exit status, so an unresolvable
|
|
85
|
+
// integrationBranch yields null rather than git's stdout echo of the
|
|
86
|
+
// branch NAME -- which would otherwise reach the CI gate as if it were
|
|
87
|
+
// a commit SHA (echo-into-SHA-field bug class).
|
|
88
|
+
mergedSha = resolveRef(config.repoPath, config.integrationBranch) ?? '';
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
log.warn(`Could not resolve integration tip SHA for the CI gate: ${err.message}`);
|
|
92
|
+
}
|
|
93
|
+
if (mergedSha) {
|
|
94
|
+
let status = 'unknown';
|
|
95
|
+
try {
|
|
96
|
+
status = await getCiStatus({ sha: mergedSha, integrationBranch: config.integrationBranch, config });
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
log.warn(`CI status read failed for ${mergedSha.slice(0, 12)}: ${err.message}`);
|
|
100
|
+
}
|
|
101
|
+
const gate = resolveCiMergeGate(status);
|
|
102
|
+
if (!gate.proceed) {
|
|
103
|
+
// Classify whether this hold can still fire: a settled RED result on
|
|
104
|
+
// this (unchanged) sha never re-evaluates without a NEW commit -- a
|
|
105
|
+
// stall that reads as patience -- whereas a pending run can still be
|
|
106
|
+
// finished by the CI runner. Record the distinction on the hold and in
|
|
107
|
+
// the log so a stall is legible from the daemon log alone.
|
|
108
|
+
const wait = classifyCiMergeWait(status, mergedSha);
|
|
109
|
+
const marker = describeWaitReachability(wait);
|
|
110
|
+
log.info(`Holding ${config.integrationBranch}->${mainBranch} for "${focusName}": ${gate.reason} ` +
|
|
111
|
+
`(sha ${mergedSha.slice(0, 12)}) [${marker}].`);
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
ciHeld: { ...gate, reachable: wait.reachable, reachabilityReason: marker },
|
|
115
|
+
sourceBranch: config.integrationBranch,
|
|
116
|
+
targetBranch: mainBranch,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
log.info(`CI gate passed for "${focusName}": ${gate.reason} (sha ${mergedSha.slice(0, 12)}).`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Record where main was BEFORE we touch anything, so push failure can roll
|
|
123
|
+
// the ref back, and classify any worktrees that currently have main attached
|
|
124
|
+
// (the daemon's primary repoPath usually keeps main checked out). Advancing
|
|
125
|
+
// the ref out-of-band leaves such a worktree with a stale tree, so after a
|
|
126
|
+
// successful merge we re-sync every worktree that has NO tracked uncommitted
|
|
127
|
+
// changes -- `git reset --hard` restores tracked files and preserves
|
|
128
|
+
// untracked ones, so a worktree dirty only with untracked scratch (the
|
|
129
|
+
// common dev-box state) is still safe to sync. Worktrees with tracked WIP are
|
|
130
|
+
// left untouched (never clobber a developer's edits) but flagged loudly
|
|
131
|
+
// afterward. Classification MUST happen before the advance: afterward the
|
|
132
|
+
// phantom diff masquerades as tracked changes and defeats the check.
|
|
133
|
+
const mainRefBefore = runGitSync(['rev-parse', mainBranch], config.repoPath);
|
|
134
|
+
const mainWorktrees = worktreesWithBranchCheckedOut(config.repoPath, mainBranch, mwt);
|
|
135
|
+
const resyncableMainWorktrees = mainWorktrees.filter(wt => !hasTrackedChanges(wt));
|
|
136
|
+
const trackedDirtyMainWorktrees = mainWorktrees.filter(wt => hasTrackedChanges(wt));
|
|
137
|
+
// Detached checkout of main's tip in the merge worktree. We deliberately
|
|
138
|
+
// do NOT attach the main branch (`git checkout main`): main is frequently
|
|
139
|
+
// checked out in the daemon's primary worktree, and git refuses to attach
|
|
140
|
+
// the same branch in two worktrees ("'main' is already used by worktree").
|
|
141
|
+
// Detaching lets us build the merge result and advance the ref via
|
|
142
|
+
// update-ref, which bypasses that guard.
|
|
143
|
+
const checkoutResult = runGitSync(['checkout', '--detach', mainBranch], mwt);
|
|
144
|
+
if (!checkoutResult.success) {
|
|
145
|
+
return {
|
|
146
|
+
success: false,
|
|
147
|
+
error: `Failed to checkout ${mainBranch}: ${checkoutResult.error}`,
|
|
148
|
+
sourceBranch: config.integrationBranch,
|
|
149
|
+
targetBranch: mainBranch,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// Bring the detached HEAD up to the latest remote main (best-effort ff).
|
|
153
|
+
// If remote main has diverged, a later push would be rejected and rolled
|
|
154
|
+
// back; we don't merge it here to keep the integration->main result clean.
|
|
155
|
+
if (withRemote) {
|
|
156
|
+
runGitSync(['merge', '--ff-only', `origin/${mainBranch}`], mwt);
|
|
157
|
+
}
|
|
158
|
+
// Save pre-merge state for rollback
|
|
159
|
+
const preMergeRef = runGitSync(['rev-parse', 'HEAD'], mwt);
|
|
160
|
+
// Dispatch to the requested strategy. The strategy helpers operate on the
|
|
161
|
+
// merge worktree's current HEAD (here a detached HEAD at main's tip), so
|
|
162
|
+
// they work identically whether or not main is attached.
|
|
163
|
+
let result = applyMergeStrategy(mwt, config.integrationBranch, mainBranch, mergeStrategy);
|
|
164
|
+
if (!result.success) {
|
|
165
|
+
return {
|
|
166
|
+
...result,
|
|
167
|
+
error: `${result.error ?? 'Unknown error'} (merging ${config.integrationBranch} into ${mainBranch} via ${mergeStrategy})`,
|
|
168
|
+
sourceBranch: config.integrationBranch,
|
|
169
|
+
targetBranch: mainBranch,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// Capture merge commit SHA
|
|
173
|
+
const shaResult = runGitSync(['rev-parse', 'HEAD'], mwt);
|
|
174
|
+
if (!result.commitSha && shaResult.success) {
|
|
175
|
+
result = { ...result, commitSha: shaResult.output };
|
|
176
|
+
}
|
|
177
|
+
const newMainSha = shaResult.success ? shaResult.output : result.commitSha;
|
|
178
|
+
// Advance refs/heads/main out-of-band to the merge result. update-ref is a
|
|
179
|
+
// plumbing command without the checked-out-branch guard that `git checkout`
|
|
180
|
+
// and `git branch -f` enforce, so this succeeds even when main is attached
|
|
181
|
+
// in the primary worktree.
|
|
182
|
+
if (newMainSha) {
|
|
183
|
+
const updateRef = runGitSync(['update-ref', `refs/heads/${mainBranch}`, newMainSha], mwt);
|
|
184
|
+
if (!updateRef.success) {
|
|
185
|
+
return {
|
|
186
|
+
success: false,
|
|
187
|
+
error: `Failed to advance ${mainBranch} ref to merge result: ${updateRef.error}`,
|
|
188
|
+
sourceBranch: config.integrationBranch,
|
|
189
|
+
targetBranch: mainBranch,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Push main to remote (gated on CD opt-in).
|
|
194
|
+
let pushedTo;
|
|
195
|
+
if (withRemote && pushToRemote) {
|
|
196
|
+
const pushResult = runGitSync(['push', 'origin', mainBranch], mwt);
|
|
197
|
+
if (!pushResult.success) {
|
|
198
|
+
// Roll the main ref back to its pre-merge value before the generic
|
|
199
|
+
// rollback (which resets the detached HEAD and marks corruption on a
|
|
200
|
+
// failed rollback) runs.
|
|
201
|
+
if (mainRefBefore.success) {
|
|
202
|
+
runGitSync(['update-ref', `refs/heads/${mainBranch}`, mainRefBefore.output], mwt);
|
|
203
|
+
}
|
|
204
|
+
if (preMergeRef.success) {
|
|
205
|
+
return handlePushFailureWithRollback({
|
|
206
|
+
repoPath: mwt,
|
|
207
|
+
branch: mainBranch,
|
|
208
|
+
preMergeRef: preMergeRef.output,
|
|
209
|
+
pushError: pushResult.error,
|
|
210
|
+
sourceBranch: config.integrationBranch,
|
|
211
|
+
targetBranch: mainBranch,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
success: false,
|
|
216
|
+
error: `Push failed (no pre-merge ref for rollback): ${pushResult.error}`,
|
|
217
|
+
sourceBranch: config.integrationBranch,
|
|
218
|
+
targetBranch: mainBranch,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const urlResult = runGitSync(['remote', 'get-url', 'origin'], mwt, { timeoutMs: 3_000 });
|
|
222
|
+
pushedTo = { name: 'origin', url: urlResult.success ? urlResult.output : '' };
|
|
223
|
+
}
|
|
224
|
+
// Re-sync worktrees that held main and had no tracked changes before the ref
|
|
225
|
+
// advance, so the out-of-band update leaves no phantom diff. `reset --hard`
|
|
226
|
+
// restores tracked files to the new main and leaves untracked files intact,
|
|
227
|
+
// so this is safe for both clean and untracked-only worktrees.
|
|
228
|
+
if (newMainSha) {
|
|
229
|
+
for (const wt of resyncableMainWorktrees) {
|
|
230
|
+
const reset = runGitSync(['reset', '--hard', newMainSha], wt);
|
|
231
|
+
if (!reset.success) {
|
|
232
|
+
log.warn(`[CI] Advanced ${mainBranch} but failed to re-sync worktree ${wt}: ${reset.error}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Worktrees with tracked WIP can't be reset without clobbering work, but
|
|
236
|
+
// leaving them silently stale is exactly what confused downstream tooling
|
|
237
|
+
// (the /sync that committed a stale tree). Flag each loudly with the
|
|
238
|
+
// recovery command so the staleness is never silent.
|
|
239
|
+
for (const wt of trackedDirtyMainWorktrees) {
|
|
240
|
+
log.warn(`[CI] Advanced ${mainBranch} to ${newMainSha} but worktree ${wt} has uncommitted ` +
|
|
241
|
+
`TRACKED changes -- left unsynced to avoid clobbering work. Its tree now lags ` +
|
|
242
|
+
`${mainBranch} and will show a phantom diff (e.g. to /sync). Commit or stash there, ` +
|
|
243
|
+
`then 'git -C ${wt} reset --hard ${mainBranch}' to reconcile.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const action = pushToRemote ? 'merged and pushed' : 'merged';
|
|
247
|
+
log.info(`Successfully ${action} ${config.integrationBranch} into ${mainBranch}`);
|
|
248
|
+
return {
|
|
249
|
+
success: true,
|
|
250
|
+
commitSha: result.commitSha,
|
|
251
|
+
preMergeRef: preMergeRef.success ? preMergeRef.output : undefined,
|
|
252
|
+
pushedTo,
|
|
253
|
+
};
|
|
254
|
+
}, `mergeIntegrationToMain:${focusName}`);
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=merge-to-main.js.map
|