@backburner/cli 0.1.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.
Files changed (202) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +329 -0
  3. package/dist/src/agents/antigravity-launch.js +162 -0
  4. package/dist/src/agents/antigravity.js +251 -0
  5. package/dist/src/agents/claude-launch.js +117 -0
  6. package/dist/src/agents/claude.js +271 -0
  7. package/dist/src/agents/codex-launch.js +82 -0
  8. package/dist/src/agents/codex.js +261 -0
  9. package/dist/src/agents/composite-runner.js +13 -0
  10. package/dist/src/agents/extraction.js +442 -0
  11. package/dist/src/agents/failure-feedback.js +132 -0
  12. package/dist/src/agents/gemini-launch.js +96 -0
  13. package/dist/src/agents/gemini.js +392 -0
  14. package/dist/src/agents/job-agent-assignments.js +180 -0
  15. package/dist/src/agents/lifecycle-feedback.js +147 -0
  16. package/dist/src/agents/limit-detector.js +83 -0
  17. package/dist/src/agents/opencode-launch.js +138 -0
  18. package/dist/src/agents/opencode.js +156 -0
  19. package/dist/src/agents/plan-breakdown-feedback.js +97 -0
  20. package/dist/src/agents/pool.js +168 -0
  21. package/dist/src/agents/prompt.js +462 -0
  22. package/dist/src/agents/provider-sessions.js +105 -0
  23. package/dist/src/agents/review-feedback.js +255 -0
  24. package/dist/src/agents/selection.js +113 -0
  25. package/dist/src/agents/service.js +1087 -0
  26. package/dist/src/agents/types.js +1 -0
  27. package/dist/src/agents/wave-assessment-feedback.js +69 -0
  28. package/dist/src/attention/derive.js +337 -0
  29. package/dist/src/capabilities/index.js +2 -0
  30. package/dist/src/capabilities/projection.js +183 -0
  31. package/dist/src/capabilities/types.js +1 -0
  32. package/dist/src/capabilities/validator.js +87 -0
  33. package/dist/src/cli/commands/broker-smoke.js +96 -0
  34. package/dist/src/cli/commands/broker.js +56 -0
  35. package/dist/src/cli/commands/init.js +4 -0
  36. package/dist/src/cli/commands/journal.js +43 -0
  37. package/dist/src/cli/commands/run-loop.js +56 -0
  38. package/dist/src/cli/commands/run.js +1241 -0
  39. package/dist/src/cli/commands/tui.js +187 -0
  40. package/dist/src/cli/dispatcher.js +51 -0
  41. package/dist/src/cli/errors.js +2 -0
  42. package/dist/src/cli/init.js +92 -0
  43. package/dist/src/cli/options.js +86 -0
  44. package/dist/src/cli/output.js +231 -0
  45. package/dist/src/cli/run-journal.js +342 -0
  46. package/dist/src/cli/run-types.js +1 -0
  47. package/dist/src/cli/run.js +32 -0
  48. package/dist/src/cli/runtime/dispatch-lifecycle.js +772 -0
  49. package/dist/src/cli/runtime/provider-runtime-types.js +1 -0
  50. package/dist/src/cli/runtime/provider-runtime.js +152 -0
  51. package/dist/src/cli/runtime/provider-tools.js +73 -0
  52. package/dist/src/cli/runtime/providers/antigravity.js +26 -0
  53. package/dist/src/cli/runtime/providers/claude.js +20 -0
  54. package/dist/src/cli/runtime/providers/codex.js +20 -0
  55. package/dist/src/cli/runtime/providers/gemini.js +20 -0
  56. package/dist/src/cli/runtime/providers/opencode.js +20 -0
  57. package/dist/src/cli/runtime/run-context.js +131 -0
  58. package/dist/src/cli/tui/index.js +3 -0
  59. package/dist/src/cli/tui/screen.js +388 -0
  60. package/dist/src/cli/tui/tracker.js +558 -0
  61. package/dist/src/cli/tui/types.js +1 -0
  62. package/dist/src/cli/tui/views.js +1006 -0
  63. package/dist/src/config/loader.js +136 -0
  64. package/dist/src/config/schemas.js +403 -0
  65. package/dist/src/config/types.js +1 -0
  66. package/dist/src/context/types.js +331 -0
  67. package/dist/src/domain/entities.js +1 -0
  68. package/dist/src/git/backburner-git-tool-service.js +680 -0
  69. package/dist/src/git/gitops-lifecycle.js +56 -0
  70. package/dist/src/git/merge-prep-workspace.js +357 -0
  71. package/dist/src/git/types.js +484 -0
  72. package/dist/src/github/broker-model-content.js +215 -0
  73. package/dist/src/github/broker-server.js +237 -0
  74. package/dist/src/github/broker.js +638 -0
  75. package/dist/src/github/composed.js +53 -0
  76. package/dist/src/github/gateway.js +478 -0
  77. package/dist/src/github/normalize.js +222 -0
  78. package/dist/src/github/pr-classification.js +73 -0
  79. package/dist/src/github/scoped-broker.js +35 -0
  80. package/dist/src/github/security.js +126 -0
  81. package/dist/src/github/service.js +332 -0
  82. package/dist/src/github/types.js +1 -0
  83. package/dist/src/github/utils.js +16 -0
  84. package/dist/src/journal/index.js +4 -0
  85. package/dist/src/journal/reader.js +62 -0
  86. package/dist/src/journal/renderer.js +50 -0
  87. package/dist/src/journal/service.js +72 -0
  88. package/dist/src/journal/types.js +1 -0
  89. package/dist/src/journal/writer.js +19 -0
  90. package/dist/src/mcps/bridge.js +166 -0
  91. package/dist/src/mcps/index.js +5 -0
  92. package/dist/src/mcps/reconcile.js +113 -0
  93. package/dist/src/mcps/smithery-client.js +186 -0
  94. package/dist/src/mcps/smithery.js +164 -0
  95. package/dist/src/mcps/types.js +1 -0
  96. package/dist/src/memory/json-store.js +71 -0
  97. package/dist/src/memory/prompt.js +16 -0
  98. package/dist/src/memory/scope.js +31 -0
  99. package/dist/src/memory/store.js +1 -0
  100. package/dist/src/memory/tool-service.js +142 -0
  101. package/dist/src/memory/types.js +12 -0
  102. package/dist/src/onboarding/config-writer.js +96 -0
  103. package/dist/src/onboarding/engine.js +86 -0
  104. package/dist/src/onboarding/prompt.js +102 -0
  105. package/dist/src/onboarding/services/provider-discovery.js +58 -0
  106. package/dist/src/onboarding/services/repo-discovery.js +246 -0
  107. package/dist/src/onboarding/steps/check-github-auth.js +32 -0
  108. package/dist/src/onboarding/steps/check-requirements.js +34 -0
  109. package/dist/src/onboarding/steps/configure-models.js +105 -0
  110. package/dist/src/onboarding/steps/configure-paths.js +33 -0
  111. package/dist/src/onboarding/steps/discover-providers.js +38 -0
  112. package/dist/src/onboarding/steps/discover-repos.js +39 -0
  113. package/dist/src/onboarding/steps/generate-config.js +106 -0
  114. package/dist/src/onboarding/steps/readiness-validation.js +158 -0
  115. package/dist/src/onboarding/steps/select-repos.js +76 -0
  116. package/dist/src/onboarding/steps/show-summary.js +97 -0
  117. package/dist/src/onboarding/steps/welcome.js +27 -0
  118. package/dist/src/onboarding/types.js +1 -0
  119. package/dist/src/onboarding/validators/git-access.js +63 -0
  120. package/dist/src/onboarding/validators/github-auth.js +85 -0
  121. package/dist/src/onboarding/validators/tool-checker.js +153 -0
  122. package/dist/src/onboarding/validators/workspace.js +83 -0
  123. package/dist/src/prompts/resolver.js +106 -0
  124. package/dist/src/retrospectives/candidates.js +224 -0
  125. package/dist/src/retrospectives/derive.js +169 -0
  126. package/dist/src/retrospectives/ingest.js +321 -0
  127. package/dist/src/retrospectives/management-prs.js +380 -0
  128. package/dist/src/retrospectives/proposals.js +199 -0
  129. package/dist/src/retrospectives/scopes.js +272 -0
  130. package/dist/src/retrospectives/scoring.js +171 -0
  131. package/dist/src/retrospectives/types.js +1 -0
  132. package/dist/src/state/loader.js +473 -0
  133. package/dist/src/state/types.js +15 -0
  134. package/dist/src/tasks/derivation/builders.js +70 -0
  135. package/dist/src/tasks/derivation/handlers/comment-response.js +169 -0
  136. package/dist/src/tasks/derivation/handlers/implement-plan.js +103 -0
  137. package/dist/src/tasks/derivation/handlers/index.js +54 -0
  138. package/dist/src/tasks/derivation/handlers/packet-pr-control.js +92 -0
  139. package/dist/src/tasks/derivation/handlers/packet-worktree.js +29 -0
  140. package/dist/src/tasks/derivation/handlers/plan-breakdown.js +45 -0
  141. package/dist/src/tasks/derivation/handlers/prepare-for-merge.js +91 -0
  142. package/dist/src/tasks/derivation/handlers/product-discovery.js +36 -0
  143. package/dist/src/tasks/derivation/handlers/retry-failed-task.js +125 -0
  144. package/dist/src/tasks/derivation/handlers/review-pr.js +28 -0
  145. package/dist/src/tasks/derivation/handlers/shared.js +24 -0
  146. package/dist/src/tasks/derivation/handlers/sync-parent-branch.js +100 -0
  147. package/dist/src/tasks/derivation/handlers/wave-assessment.js +223 -0
  148. package/dist/src/tasks/derivation/handlers/write-plan.js +101 -0
  149. package/dist/src/tasks/derivation/handlers/write-product-spec.js +49 -0
  150. package/dist/src/tasks/derivation/runner.js +18 -0
  151. package/dist/src/tasks/derivation/types.js +1 -0
  152. package/dist/src/tasks/derive.js +810 -0
  153. package/dist/src/tasks/mcp-skip.js +10 -0
  154. package/dist/src/tasks/retry.js +10 -0
  155. package/dist/src/tasks/task.js +184 -0
  156. package/dist/src/tasks/types.js +1 -0
  157. package/dist/src/utils/command-runner.js +192 -0
  158. package/dist/src/utils/json.js +38 -0
  159. package/dist/src/utils/logger.js +43 -0
  160. package/dist/src/utils/paths.js +38 -0
  161. package/dist/src/utils/slug.js +10 -0
  162. package/dist/src/workflows/registry.js +174 -0
  163. package/dist/src/workflows/triage.js +90 -0
  164. package/dist/src/workflows/workstream-machine/derive-events.js +121 -0
  165. package/dist/src/workflows/workstream-machine/evaluate.js +24 -0
  166. package/dist/src/workflows/workstream-machine/events.js +1 -0
  167. package/dist/src/workflows/workstream-machine/machine.js +355 -0
  168. package/dist/src/workflows/workstream-machine/selectors.js +19 -0
  169. package/dist/src/workflows/workstream-machine/types.js +1 -0
  170. package/dist/src/workstreams/branch-sync.js +87 -0
  171. package/dist/src/workstreams/derive.js +629 -0
  172. package/dist/src/workstreams/mcp-blockers.js +420 -0
  173. package/dist/src/workstreams/packet-candidates.js +42 -0
  174. package/dist/src/workstreams/packet-lifecycle-feedback.js +60 -0
  175. package/dist/src/workstreams/packet-lifecycle-report.js +199 -0
  176. package/dist/src/workstreams/packet-plan.js +597 -0
  177. package/dist/src/workstreams/packet-projections.js +190 -0
  178. package/dist/src/workstreams/parent-branch-sync-action.js +312 -0
  179. package/dist/src/workstreams/parent-branch-sync.js +148 -0
  180. package/dist/src/workstreams/phases.js +4 -0
  181. package/dist/src/workstreams/plan-breakdown-generator/export.js +19 -0
  182. package/dist/src/workstreams/plan-breakdown-generator/index.js +6 -0
  183. package/dist/src/workstreams/plan-breakdown-generator/inspect.js +50 -0
  184. package/dist/src/workstreams/plan-breakdown-generator/patch.js +139 -0
  185. package/dist/src/workstreams/plan-breakdown-generator/session.js +19 -0
  186. package/dist/src/workstreams/plan-breakdown-generator/store.js +27 -0
  187. package/dist/src/workstreams/plan-breakdown-generator/tools.js +407 -0
  188. package/dist/src/workstreams/plan-breakdown-generator/types.js +6 -0
  189. package/dist/src/workstreams/plan-breakdown-generator/validate.js +212 -0
  190. package/dist/src/workstreams/plan-breakdown-schema.js +175 -0
  191. package/dist/src/workstreams/utils.js +15 -0
  192. package/dist/src/workstreams/wave-assessment/checkpoint.js +172 -0
  193. package/dist/src/workstreams/wave-assessment/index.js +3 -0
  194. package/dist/src/workstreams/wave-assessment/session.js +84 -0
  195. package/dist/src/workstreams/wave-assessment/store.js +51 -0
  196. package/dist/src/workstreams/wave-assessment/tools.js +419 -0
  197. package/dist/src/workstreams/wave-assessment/types.js +1 -0
  198. package/dist/src/workstreams/wave-assessment/validate.js +78 -0
  199. package/dist/src/worktrees/service.js +839 -0
  200. package/dist/src/worktrees/workspace-ownership.js +34 -0
  201. package/dist/src/worktrees/workspace-recovery.js +177 -0
  202. package/package.json +61 -0
@@ -0,0 +1,56 @@
1
+ export class GitOpsLifecycleService {
2
+ gitGateway;
3
+ constructor(gitGateway) {
4
+ this.gitGateway = gitGateway;
5
+ }
6
+ async prepareManagementRoot(managementDir) {
7
+ const isGit = await this.gitGateway.isInsideWorkingTree(managementDir);
8
+ if (!isGit) {
9
+ return { status: "skipped_not_git" };
10
+ }
11
+ const isClean = await this.gitGateway.isWorkingTreeClean(managementDir);
12
+ if (!isClean) {
13
+ throw new Error(`Management directory is dirty: ${managementDir}`);
14
+ }
15
+ try {
16
+ await this.gitGateway.pullFastForwardOnly(managementDir);
17
+ }
18
+ catch (error) {
19
+ throw new Error(`Failed to pull management directory (fast-forward only): ${error instanceof Error ? error.message : String(error)}`);
20
+ }
21
+ return { status: "pulled" };
22
+ }
23
+ async prepareOutputsRoot(outputsDir) {
24
+ const isGit = await this.gitGateway.isInsideWorkingTree(outputsDir);
25
+ if (!isGit) {
26
+ return { status: "skipped_not_git" };
27
+ }
28
+ const isClean = await this.gitGateway.isWorkingTreeClean(outputsDir);
29
+ if (!isClean) {
30
+ throw new Error(`Outputs directory is dirty: ${outputsDir}`);
31
+ }
32
+ return { status: "clean_before_run" };
33
+ }
34
+ async finalizeOutputsRoot(outputsDir, startedAt, prepared) {
35
+ if (prepared.status === "skipped_not_git") {
36
+ return prepared;
37
+ }
38
+ const branch = await this.gitGateway.getCurrentBranch(outputsDir);
39
+ if (!branch) {
40
+ throw new Error(`Outputs directory is in a detached HEAD state: ${outputsDir}`);
41
+ }
42
+ const message = `Record orchestrator run ${startedAt}`;
43
+ const committed = await this.gitGateway.commitAll(outputsDir, message);
44
+ if (!committed) {
45
+ return { status: "no_changes" };
46
+ }
47
+ const commitSha = await this.gitGateway.getHeadCommitSha(outputsDir);
48
+ try {
49
+ await this.gitGateway.pushBranch(outputsDir, branch);
50
+ }
51
+ catch (error) {
52
+ throw new Error(`Failed to push outputs branch "${branch}" (commit ${commitSha}): ${error instanceof Error ? error.message : String(error)}`);
53
+ }
54
+ return { status: "pushed", commitSha };
55
+ }
56
+ }
@@ -0,0 +1,357 @@
1
+ import { appendFile, mkdir } from "node:fs/promises";
2
+ import { randomUUID } from "node:crypto";
3
+ import * as path from "node:path";
4
+ export const MERGE_PREP_ROOT_DIR = "_merge-prep";
5
+ export class MergePrepWorkspaceService {
6
+ gitGateway;
7
+ constructor(gitGateway) {
8
+ this.gitGateway = gitGateway;
9
+ }
10
+ /**
11
+ * Generate a unique execution ID for a merge-prep clone directory.
12
+ * Uses randomUUID to guarantee uniqueness across retries/reruns.
13
+ */
14
+ generateExecutionId() {
15
+ return randomUUID().replace(/-/g, "").slice(0, 12);
16
+ }
17
+ /**
18
+ * Refresh a persistent worktree before dispatch.
19
+ *
20
+ * Rules:
21
+ * - If path is not a git working tree → blocked_not_git
22
+ * - If working tree is dirty → blocked_dirty (do NOT reset/force)
23
+ * - Fetch origin
24
+ * - If on wrong branch → blocked_wrong_branch
25
+ * - Compare local HEAD vs origin/<branch>:
26
+ * - behind → fast-forward → fast_forwarded
27
+ * - equal → already_up_to_date
28
+ * - ahead only → blocked_ahead
29
+ * - diverged → blocked_diverged
30
+ */
31
+ async refreshPersistentWorktreeBeforeDispatch(input) {
32
+ const { localPath, expectedBranch } = input;
33
+ const isGit = await this.gitGateway.isInsideWorkingTree(localPath);
34
+ if (!isGit) {
35
+ return {
36
+ status: "blocked_not_git",
37
+ localPath,
38
+ branch: expectedBranch,
39
+ message: `Path ${localPath} is not a git working tree.`
40
+ };
41
+ }
42
+ const isClean = await this.gitGateway.isWorkingTreeClean(localPath);
43
+ if (!isClean) {
44
+ return {
45
+ status: "blocked_dirty",
46
+ localPath,
47
+ branch: expectedBranch,
48
+ message: `Worktree at ${localPath} has uncommitted changes; refusing to fast-forward.`
49
+ };
50
+ }
51
+ await this.gitGateway.fetchRepository(localPath);
52
+ const currentBranch = await this.gitGateway.getCurrentBranch(localPath);
53
+ if (currentBranch !== expectedBranch) {
54
+ return {
55
+ status: "blocked_wrong_branch",
56
+ localPath,
57
+ branch: expectedBranch,
58
+ message: `Worktree at ${localPath} is on branch "${currentBranch}", expected "${expectedBranch}".`
59
+ };
60
+ }
61
+ const { ahead, behind } = await this.gitGateway.getAheadBehindVsRemote(localPath, expectedBranch);
62
+ if (behind === 0 && ahead === 0) {
63
+ return {
64
+ status: "already_up_to_date",
65
+ localPath,
66
+ branch: expectedBranch,
67
+ message: "Already up to date."
68
+ };
69
+ }
70
+ if (behind > 0 && ahead === 0) {
71
+ await this.gitGateway.fastForwardDefaultBranch(localPath, expectedBranch);
72
+ return {
73
+ status: "fast_forwarded",
74
+ localPath,
75
+ branch: expectedBranch,
76
+ message: `Fast-forwarded ${expectedBranch} by ${behind} commit(s).`
77
+ };
78
+ }
79
+ if (ahead > 0 && behind === 0) {
80
+ return {
81
+ status: "blocked_ahead",
82
+ localPath,
83
+ branch: expectedBranch,
84
+ message: `Worktree at ${localPath} is ${ahead} commit(s) ahead of origin/${expectedBranch}. May be an unpushed handoff.`
85
+ };
86
+ }
87
+ return {
88
+ status: "blocked_diverged",
89
+ localPath,
90
+ branch: expectedBranch,
91
+ message: `Worktree at ${localPath} has diverged from origin/${expectedBranch} (${ahead} ahead, ${behind} behind).`
92
+ };
93
+ }
94
+ /**
95
+ * Create a dedicated temporary clone for a prepare-for-merge agent task.
96
+ *
97
+ * Clone path: <codeRoot>/_merge-prep/<owner>/<repo>/pr-<number>-<executionId>/
98
+ *
99
+ * Steps:
100
+ * 1. Generate a unique executionId.
101
+ * 2. Create the clone directory structure.
102
+ * 3. Clone from SSH URL.
103
+ * 4. Checkout the PR head branch at origin/<headBranch>.
104
+ * 5. Verify HEAD matches expectedHeadSha.
105
+ * 6. If SHA mismatch: delete clone and throw.
106
+ */
107
+ async prepareMergePrepClone(input) {
108
+ const { codeRoot, repoSshUrl, owner, repoName, prNumber, headBranch, expectedHeadSha } = input;
109
+ const executionId = this.generateExecutionId();
110
+ const clonePath = path.join(codeRoot, MERGE_PREP_ROOT_DIR, owner, repoName, `pr-${prNumber}-${executionId}`);
111
+ const parentDir = path.dirname(clonePath);
112
+ await mkdir(parentDir, { recursive: true });
113
+ await this.gitGateway.cloneRepositoryFromUrl(repoSshUrl, clonePath);
114
+ await this.gitGateway.checkoutBranchAtRemote(clonePath, headBranch, { skipFetch: true });
115
+ const headSha = await this.gitGateway.getHeadCommitSha(clonePath);
116
+ if (headSha !== expectedHeadSha) {
117
+ await this.gitGateway.removeDirectory(clonePath);
118
+ throw new Error(`Merge-prep clone SHA mismatch for PR #${prNumber}: expected ${expectedHeadSha}, got ${headSha}. Branch moved before work began.`);
119
+ }
120
+ return { clonePath, verifiedHeadSha: headSha, executionId };
121
+ }
122
+ /**
123
+ * Inspect the clone result after the agent returns.
124
+ *
125
+ * Checks (in order):
126
+ * 1. Clone exists and is a git working tree.
127
+ * 2. On the expected branch.
128
+ * 3. No active merge/rebase/cherry-pick/revert state.
129
+ * 4. Working tree is clean (dirty clone always blocks push).
130
+ * 5. Local HEAD is a descendant of originalHeadSha.
131
+ * 6. At least one commit ahead of originalHeadSha.
132
+ * 7. All good → ready_to_push with commit log and diff stats from GitGateway methods.
133
+ *
134
+ * Note: commit log and diff stats are obtained via this.gitGateway.getCommitLogSince()
135
+ * and this.gitGateway.getDiffStatSince() — both first-class GitGateway interface methods.
136
+ * There are no private helpers or as-any access here.
137
+ */
138
+ async inspectMergePrepCloneResult(input) {
139
+ const { clonePath, expectedBranch, originalHeadSha } = input;
140
+ const isGit = await this.gitGateway.isInsideWorkingTree(clonePath);
141
+ if (!isGit) {
142
+ return { status: "clone_missing", message: `Clone at ${clonePath} is not a git working tree.` };
143
+ }
144
+ const currentBranch = await this.gitGateway.getCurrentBranch(clonePath);
145
+ if (currentBranch !== expectedBranch) {
146
+ return {
147
+ status: "wrong_branch",
148
+ message: `Clone is on branch "${currentBranch}", expected "${expectedBranch}".`
149
+ };
150
+ }
151
+ const mergeState = await this.gitGateway.getActiveMergeState(clonePath);
152
+ if (mergeState !== "none") {
153
+ return {
154
+ status: "active_merge_state",
155
+ message: `Clone has an active ${mergeState} operation in progress.`
156
+ };
157
+ }
158
+ const isClean = await this.gitGateway.isWorkingTreeClean(clonePath);
159
+ if (!isClean) {
160
+ return {
161
+ status: "dirty_uncommitted",
162
+ message: `Clone at ${clonePath} has uncommitted changes after agent returned. Dirty clone always blocks push.`
163
+ };
164
+ }
165
+ const localSha = await this.gitGateway.getHeadCommitSha(clonePath);
166
+ const isDescendant = await this.gitGateway.isAncestorOf(clonePath, originalHeadSha, localSha);
167
+ if (!isDescendant) {
168
+ return {
169
+ status: "sha_baseline_mismatch",
170
+ localSha,
171
+ message: `Local HEAD ${localSha} is not a descendant of original PR head ${originalHeadSha}. History was rewritten or baseline diverged.`
172
+ };
173
+ }
174
+ const aheadCount = await this.gitGateway.countCommitsAheadOf(clonePath, originalHeadSha);
175
+ if (aheadCount === 0) {
176
+ return {
177
+ status: "no_commits_ahead",
178
+ localSha,
179
+ aheadCount: 0,
180
+ message: `Clone HEAD matches original PR head SHA ${originalHeadSha}. No merge-prep commits were made.`
181
+ };
182
+ }
183
+ // Use first-class GitGateway methods — no as-any, no private helpers
184
+ const commitLog = await this.gitGateway.getCommitLogSince(clonePath, originalHeadSha);
185
+ const diffStats = await this.gitGateway.getDiffStatSince(clonePath, originalHeadSha);
186
+ return {
187
+ status: "ready_to_push",
188
+ localSha,
189
+ aheadCount,
190
+ commitLog,
191
+ diffStats,
192
+ message: `Clone is ${aheadCount} commit(s) ahead of original head. Ready to push.`
193
+ };
194
+ }
195
+ /**
196
+ * Push the merge-prep result with a normal non-force push.
197
+ *
198
+ * If the remote branch advanced while prepare-for-merge was running, Git will
199
+ * reject the push and the orchestrator reports a retryable lease-style failure.
200
+ */
201
+ async publishMergePrepResult(input) {
202
+ const { clonePath, headBranch } = input;
203
+ try {
204
+ await this.gitGateway.pushBranch(clonePath, headBranch);
205
+ const pushedSha = await this.gitGateway.getHeadCommitSha(clonePath);
206
+ return {
207
+ status: "pushed",
208
+ pushedSha,
209
+ message: `Pushed ${headBranch} to origin (SHA: ${pushedSha}).`
210
+ };
211
+ }
212
+ catch (error) {
213
+ const message = error instanceof Error ? error.message : String(error);
214
+ const isLeaseFailure = message.includes("stale info") ||
215
+ message.includes("remote rejected") ||
216
+ message.includes("fetch first") ||
217
+ message.includes("[rejected]") ||
218
+ message.includes("non-fast-forward");
219
+ if (isLeaseFailure) {
220
+ return {
221
+ status: "lease_failed",
222
+ message: `Push rejected: remote branch "${headBranch}" moved while agent was running. Lease expired. Remote state was preserved.`
223
+ };
224
+ }
225
+ return { status: "push_failed", message: `Push failed: ${message}` };
226
+ }
227
+ }
228
+ /**
229
+ * Write the audit entry to the agent log in the durable persistent worktree.
230
+ *
231
+ * The orchestrator (not the agent) calls this. The durable log is the canonical
232
+ * audit trail for future agents.
233
+ */
234
+ async writeAuditToLog(input) {
235
+ const { audit, persistentLogDocumentPath } = input;
236
+ const logEntry = buildMergePrepLogEntry(audit);
237
+ try {
238
+ await appendFile(persistentLogDocumentPath, `\n${logEntry}\n`, "utf-8");
239
+ }
240
+ catch {
241
+ // Non-fatal: failure to write audit log should not block cleanup
242
+ }
243
+ }
244
+ /**
245
+ * Refresh the durable persistent worktree after a successful merge-prep push.
246
+ * Returns the refresh status so the caller can include it in the audit.
247
+ *
248
+ * Ensures follow-up tasks (review, discussion) see the merge-prep commit.
249
+ * Steps: fetch origin, checkout branch, fast-forward only.
250
+ */
251
+ async refreshPersistentWorktreeAfterMergePrep(input) {
252
+ const { persistentWorktreePath, headBranch } = input;
253
+ try {
254
+ await this.gitGateway.fetchRepository(persistentWorktreePath);
255
+ await this.gitGateway.checkoutBranch(persistentWorktreePath, headBranch);
256
+ await this.gitGateway.fastForwardDefaultBranch(persistentWorktreePath, headBranch);
257
+ return { status: "succeeded" };
258
+ }
259
+ catch (err) {
260
+ return {
261
+ status: "failed",
262
+ message: err instanceof Error ? err.message : String(err)
263
+ };
264
+ }
265
+ }
266
+ /**
267
+ * Clean up the temporary clone directory.
268
+ *
269
+ * Always delete after success or failure. Never silently preserve.
270
+ */
271
+ async cleanupMergePrepClone(input) {
272
+ try {
273
+ await this.gitGateway.removeDirectory(input.clonePath);
274
+ }
275
+ catch {
276
+ // Non-fatal
277
+ }
278
+ }
279
+ /**
280
+ * Write the audit log entry into the temporary clone and commit it,
281
+ * so it is included in the branch push and later pulled cleanly
282
+ * by the persistent worktree.
283
+ *
284
+ * The relative log path is derived from the persistent worktree root
285
+ * and the absolute log document path, then re-applied inside the clone.
286
+ *
287
+ * Non-fatal: a failure here is logged as a warning but does not block push.
288
+ */
289
+ async commitAuditInClone(input) {
290
+ const { clonePath, persistentWorktreePath, persistentLogDocumentPath, audit } = input;
291
+ const relativeLogPath = path.relative(persistentWorktreePath, persistentLogDocumentPath);
292
+ // Guard: log path must not escape the worktree root.
293
+ if (relativeLogPath.startsWith("..")) {
294
+ // Non-fatal; just skip
295
+ return;
296
+ }
297
+ const absoluteLogPathInClone = path.join(clonePath, relativeLogPath);
298
+ try {
299
+ await mkdir(path.dirname(absoluteLogPathInClone), { recursive: true });
300
+ const logEntry = buildMergePrepLogEntry(audit);
301
+ await appendFile(absoluteLogPathInClone, `\n${logEntry}\n`, "utf-8");
302
+ await this.gitGateway.commitAll(clonePath, `chore(agent): log merge-prep result for PR #${audit.prNumber}`);
303
+ }
304
+ catch {
305
+ // Non-fatal: failure to write and commit audit log should not block push
306
+ }
307
+ }
308
+ }
309
+ export function buildMergePrepPrComment(audit) {
310
+ const lines = [
311
+ "🤖 **Merge preparation result**",
312
+ "",
313
+ `- **PR**: #${audit.prNumber}`,
314
+ `- **Head branch**: \`${audit.headBranch}\``,
315
+ `- **Base branch**: \`${audit.baseBranch}\``,
316
+ `- **Original head SHA**: \`${audit.originalHeadSha}\``,
317
+ `- **Final local SHA**: ${audit.finalLocalSha ? `\`${audit.finalLocalSha}\`` : "N/A"}`,
318
+ `- **Branch pushed**: ${audit.pushed ? `Yes (SHA: \`${audit.pushedSha}\`)` : "No"}`
319
+ ];
320
+ if (audit.persistentRefreshStatus) {
321
+ lines.push(`- **Persistent worktree refresh**: ${audit.persistentRefreshStatus}`);
322
+ if (audit.persistentRefreshStatus === "failed" && audit.persistentRefreshMessage) {
323
+ lines.push(` - Refresh error: ${audit.persistentRefreshMessage}`);
324
+ }
325
+ }
326
+ if (audit.commitLog) {
327
+ lines.push("", "**Commits applied:**", "```", audit.commitLog, "```");
328
+ }
329
+ if (audit.diffStats) {
330
+ lines.push("", "**Files changed:**", "```", audit.diffStats, "```");
331
+ }
332
+ if (audit.blockerReason) {
333
+ lines.push("", `**Blocker**: ${audit.blockerReason}`);
334
+ }
335
+ if (audit.safeNextAction) {
336
+ lines.push(`**Recommended next action**: ${audit.safeNextAction}`);
337
+ }
338
+ return lines.join("\n");
339
+ }
340
+ function buildMergePrepLogEntry(audit) {
341
+ const timestamp = new Date().toISOString();
342
+ const status = audit.pushed ? "SUCCESS" : "BLOCKED";
343
+ const lines = [
344
+ `- ${timestamp}: merge-prep for PR #${audit.prNumber} — ${status}`,
345
+ ` original-head: ${audit.originalHeadSha}`,
346
+ ` final-sha: ${audit.finalLocalSha ?? "none"}`,
347
+ ` pushed: ${audit.pushed}${audit.pushedSha ? ` (${audit.pushedSha})` : ""}`,
348
+ ` persistent-refresh: ${audit.persistentRefreshStatus ?? "skipped"}`
349
+ ];
350
+ if (audit.persistentRefreshStatus === "failed" && audit.persistentRefreshMessage) {
351
+ lines.push(` persistent-refresh-error: ${audit.persistentRefreshMessage}`);
352
+ }
353
+ if (audit.blockerReason) {
354
+ lines.push(` blocker: ${audit.blockerReason}`);
355
+ }
356
+ return lines.join("\n");
357
+ }