@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,680 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { CommandExecutionError } from "../utils/command-runner.js";
4
+ const MAX_DIFF_BYTES = 64_000;
5
+ const MAX_FILE_BYTES = 256_000;
6
+ const CONFLICT_MARKER_RE = /^(<<<<<<<|=======|>>>>>>>)(?: .*)?$/m;
7
+ class BackburnerGitToolError extends Error {
8
+ code;
9
+ constructor(code, message) {
10
+ super(message);
11
+ this.code = code;
12
+ }
13
+ }
14
+ export class BackburnerGitToolService {
15
+ commandRunner;
16
+ logger;
17
+ sessions = new Map();
18
+ constructor(commandRunner, logger) {
19
+ this.commandRunner = commandRunner;
20
+ this.logger = logger;
21
+ }
22
+ listTools() {
23
+ return TOOL_DEFINITIONS;
24
+ }
25
+ registerPrepareForMergeWorkstream(input) {
26
+ if (this.sessions.has(input.backburnerGitSessionId)) {
27
+ throw new Error(`Backburner Git session "${input.backburnerGitSessionId}" is already registered. ` +
28
+ "Concurrent prepare-for-merge tasks must use distinct session IDs.");
29
+ }
30
+ this.sessions.set(input.backburnerGitSessionId, {
31
+ ...input,
32
+ repoRoot: path.resolve(input.repoRoot)
33
+ });
34
+ }
35
+ unregisterBackburnerGitSession(backburnerGitSessionId) {
36
+ this.sessions.delete(backburnerGitSessionId);
37
+ }
38
+ updateWorkstreamState(backburnerGitSessionId, state) {
39
+ const registered = this.sessions.get(backburnerGitSessionId);
40
+ if (!registered) {
41
+ throw new BackburnerGitToolError("unknown_workstream", `Unknown Backburner Git session "${backburnerGitSessionId}"`);
42
+ }
43
+ registered.state = state;
44
+ }
45
+ async callTool(name, rawArguments) {
46
+ const startedAt = new Date().toISOString();
47
+ try {
48
+ const result = await this.callToolInner(name, rawArguments);
49
+ this.logInvocation(startedAt, name, rawArguments, "ok");
50
+ return result;
51
+ }
52
+ catch (error) {
53
+ const failure = toFailure(name, error);
54
+ this.logInvocation(startedAt, name, rawArguments, "error", failure.ok ? undefined : failure.error.message);
55
+ return failure;
56
+ }
57
+ }
58
+ async callToolInner(name, rawArguments) {
59
+ switch (name) {
60
+ case "backburner_git_get_state": {
61
+ const workstream = this.requireWorkstream(expectBackburnerGitSessionId(rawArguments));
62
+ const result = await this.getState(workstream);
63
+ return success(name, "Backburner Git state loaded.", result);
64
+ }
65
+ case "backburner_git_get_conflicts": {
66
+ const workstream = this.requireWorkstream(expectBackburnerGitSessionId(rawArguments));
67
+ const result = await this.getConflicts(workstream);
68
+ return success(name, "Backburner Git conflicts loaded.", result);
69
+ }
70
+ case "backburner_git_get_conflict_versions": {
71
+ const args = expectObject(rawArguments, "tool arguments must be an object");
72
+ const workstream = this.requireWorkstream(expectString(args.backburnerGitSessionId, '"backburnerGitSessionId" is required'));
73
+ const safePath = await this.validateKnownPath(workstream, expectString(args.path, '"path" is required'));
74
+ const result = await this.getConflictVersions(workstream, safePath);
75
+ return success(name, `Backburner Git conflict versions loaded for ${safePath}.`, result);
76
+ }
77
+ case "backburner_git_get_diff": {
78
+ const args = expectObject(rawArguments ?? {}, "tool arguments must be an object");
79
+ const workstream = this.requireWorkstream(expectString(args.backburnerGitSessionId, '"backburnerGitSessionId" is required'));
80
+ const options = expectDiffOptions(args.options);
81
+ const safePath = options.path ? await this.validateKnownPath(workstream, options.path) : undefined;
82
+ const result = await this.getDiff(workstream, safePath ? { ...options, path: safePath } : options);
83
+ return success(name, "Backburner Git diff loaded.", result);
84
+ }
85
+ case "backburner_git_mark_resolved": {
86
+ const args = expectObject(rawArguments, "tool arguments must be an object");
87
+ const workstream = this.requireWorkstream(expectString(args.backburnerGitSessionId, '"backburnerGitSessionId" is required'));
88
+ this.requireMergeConflictState(workstream);
89
+ const paths = await this.validatePathList(workstream, args.paths);
90
+ for (const repoPath of paths) {
91
+ await this.rejectConflictMarkers(workstream, repoPath);
92
+ }
93
+ await this.runGit(workstream.repoRoot, ["add", "--", ...paths]);
94
+ return success(name, "Backburner Git paths marked resolved.", { paths, state: await this.getState(workstream) });
95
+ }
96
+ case "backburner_git_mark_deleted": {
97
+ const args = expectObject(rawArguments, "tool arguments must be an object");
98
+ const workstream = this.requireWorkstream(expectString(args.backburnerGitSessionId, '"backburnerGitSessionId" is required'));
99
+ this.requireMergeConflictState(workstream);
100
+ const paths = await this.validateDeletedPathList(workstream, args.paths);
101
+ await this.runGit(workstream.repoRoot, ["rm", "--force", "--", ...paths]);
102
+ return success(name, "Backburner Git deleted paths marked resolved.", { paths, state: await this.getState(workstream) });
103
+ }
104
+ case "backburner_git_unstage": {
105
+ const args = expectObject(rawArguments, "tool arguments must be an object");
106
+ const workstream = this.requireWorkstream(expectString(args.backburnerGitSessionId, '"backburnerGitSessionId" is required'));
107
+ this.requireMergeConflictState(workstream);
108
+ const paths = await this.validatePathList(workstream, args.paths);
109
+ await this.runGit(workstream.repoRoot, ["restore", "--staged", "--", ...paths]);
110
+ return success(name, "Backburner Git paths unstaged.", { paths, state: await this.getState(workstream) });
111
+ }
112
+ case "backburner_git_restore_worktree": {
113
+ const args = expectObject(rawArguments, "tool arguments must be an object");
114
+ const workstream = this.requireWorkstream(expectString(args.backburnerGitSessionId, '"backburnerGitSessionId" is required'));
115
+ this.requireMergeConflictState(workstream);
116
+ const paths = await this.validateRestorableWorktreePathList(workstream, args.paths);
117
+ await this.runGit(workstream.repoRoot, ["restore", "--worktree", "--", ...paths]);
118
+ return success(name, "Backburner Git worktree paths restored.", { paths, state: await this.getState(workstream) });
119
+ }
120
+ case "backburner_git_continue_merge": {
121
+ const workstream = this.requireWorkstream(expectBackburnerGitSessionId(rawArguments));
122
+ this.requireMergeConflictState(workstream);
123
+ const result = await this.continueMerge(workstream);
124
+ workstream.state = "merge_started";
125
+ return success(name, "Backburner Git merge continued.", result);
126
+ }
127
+ case "backburner_git_abort_merge": {
128
+ const workstream = this.requireWorkstream(expectBackburnerGitSessionId(rawArguments));
129
+ await this.requireActiveOperation(workstream, "merge");
130
+ await this.runGit(workstream.repoRoot, ["merge", "--abort"]);
131
+ workstream.state = "merge_aborted";
132
+ return success(name, "Backburner Git merge aborted.", { state: await this.getState(workstream) });
133
+ }
134
+ default:
135
+ throw new BackburnerGitToolError("tool_not_found", `Unknown Backburner Git tool "${name}"`);
136
+ }
137
+ }
138
+ requireWorkstream(backburnerGitSessionId) {
139
+ const workstream = this.sessions.get(backburnerGitSessionId);
140
+ if (!workstream) {
141
+ throw new BackburnerGitToolError("unknown_workstream", `Unknown Backburner Git session "${backburnerGitSessionId}"`);
142
+ }
143
+ return workstream;
144
+ }
145
+ requireMergeConflictState(workstream) {
146
+ if (workstream.state !== "merge_conflict") {
147
+ throw new BackburnerGitToolError("invalid_workstream_state", `Backburner Git session "${workstream.backburnerGitSessionId}" for workstream "${workstream.workstreamId}" ` +
148
+ `is in ${workstream.state} state, not merge_conflict`);
149
+ }
150
+ }
151
+ async requireActiveOperation(workstream, expected) {
152
+ const operation = await this.getActiveOperation(workstream.repoRoot);
153
+ if (operation !== expected) {
154
+ throw new BackburnerGitToolError("invalid_git_operation", `Expected active ${expected}, found ${operation}`);
155
+ }
156
+ }
157
+ async validatePathList(workstream, rawPaths) {
158
+ return (await this.validatePathListWithSnapshot(workstream, rawPaths)).paths;
159
+ }
160
+ async validatePathListWithSnapshot(workstream, rawPaths) {
161
+ if (!Array.isArray(rawPaths) || rawPaths.length === 0) {
162
+ throw new BackburnerGitToolError("invalid_arguments", '"paths" must be a non-empty string array');
163
+ }
164
+ const [tracked, entries] = await Promise.all([
165
+ this.getTrackedPaths(workstream.repoRoot),
166
+ this.getPorcelainEntryMap(workstream.repoRoot)
167
+ ]);
168
+ const known = new Set([...tracked, ...entries.keys()]);
169
+ const paths = [];
170
+ for (const entry of rawPaths) {
171
+ const repoPath = validateRepoRelativePath(workstream.repoRoot, expectString(entry, "each path must be a string"));
172
+ if (!known.has(repoPath)) {
173
+ throw new BackburnerGitToolError("path_not_changed", `Path "${repoPath}" is not conflicted, tracked, or changed for this Backburner Git session`);
174
+ }
175
+ paths.push(repoPath);
176
+ }
177
+ return { paths, tracked, entries };
178
+ }
179
+ async validateDeletedPathList(workstream, rawPaths) {
180
+ const { paths, entries } = await this.validatePathListWithSnapshot(workstream, rawPaths);
181
+ const invalid = paths.filter((repoPath) => {
182
+ const entry = entries.get(repoPath);
183
+ return !entry || !isCurrentDeletionEntry(entry);
184
+ });
185
+ if (invalid.length > 0) {
186
+ throw new BackburnerGitToolError("path_not_deleted", `Path is not currently deleted or unmerged for this Backburner Git session: ${invalid.join(", ")}`);
187
+ }
188
+ return paths;
189
+ }
190
+ async validateRestorableWorktreePathList(workstream, rawPaths) {
191
+ const { paths, tracked, entries } = await this.validatePathListWithSnapshot(workstream, rawPaths);
192
+ const invalid = paths.filter((repoPath) => {
193
+ const entry = entries.get(repoPath);
194
+ return !tracked.has(repoPath) || !entry || isUnmergedEntry(entry) || entry.y === " ";
195
+ });
196
+ if (invalid.length > 0) {
197
+ throw new BackburnerGitToolError("path_not_restorable", `Path has no restorable tracked worktree change for this Backburner Git session: ${invalid.join(", ")}. ` +
198
+ "To undo a staged change, use backburner_git_unstage instead.");
199
+ }
200
+ return paths;
201
+ }
202
+ async validateKnownPath(workstream, inputPath) {
203
+ const repoPath = validateRepoRelativePath(workstream.repoRoot, inputPath);
204
+ const known = await this.getKnownPaths(workstream);
205
+ if (!known.has(repoPath)) {
206
+ throw new BackburnerGitToolError("path_not_changed", `Path "${repoPath}" is not conflicted, tracked, or changed for this Backburner Git session`);
207
+ }
208
+ return repoPath;
209
+ }
210
+ async getKnownPaths(workstream) {
211
+ const entries = await this.getPorcelainEntries(workstream.repoRoot);
212
+ const known = new Set(entries.map((entry) => entry.path));
213
+ for (const repoPath of await this.getTrackedPaths(workstream.repoRoot)) {
214
+ known.add(repoPath);
215
+ }
216
+ return known;
217
+ }
218
+ async getTrackedPaths(repoRoot) {
219
+ const tracked = await this.runGit(repoRoot, ["ls-files"]);
220
+ return new Set(tracked.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
221
+ }
222
+ async getState(workstream) {
223
+ const [operation, currentBranch, currentHeadSha, entries, conflictedFiles] = await Promise.all([
224
+ this.getActiveOperation(workstream.repoRoot),
225
+ this.getCurrentBranch(workstream.repoRoot),
226
+ this.runGit(workstream.repoRoot, ["rev-parse", "HEAD"]).then((result) => result.stdout.trim()),
227
+ this.getPorcelainEntries(workstream.repoRoot),
228
+ this.getUnresolvedConflictPaths(workstream.repoRoot)
229
+ ]);
230
+ const files = classifyPorcelainEntries(entries);
231
+ return {
232
+ operation,
233
+ currentBranch,
234
+ baseRef: workstream.baseRef,
235
+ expectedHeadSha: workstream.expectedHeadSha,
236
+ currentHeadSha,
237
+ worktreeClean: entries.length === 0,
238
+ indexHasUnresolvedConflicts: conflictedFiles.length > 0,
239
+ stagedFiles: files.stagedFiles,
240
+ unstagedFiles: files.unstagedFiles,
241
+ untrackedFiles: files.untrackedFiles,
242
+ conflictedFiles,
243
+ statusSummary: await this.getStatusSummary(workstream.repoRoot)
244
+ };
245
+ }
246
+ async getConflicts(workstream) {
247
+ const entries = (await this.getPorcelainEntries(workstream.repoRoot)).filter(isUnmergedEntry);
248
+ const conflicts = [];
249
+ for (const entry of entries) {
250
+ const filePath = path.join(workstream.repoRoot, entry.path);
251
+ const exists = await pathExists(filePath);
252
+ conflicts.push({
253
+ path: entry.path,
254
+ type: conflictType(entry),
255
+ exists,
256
+ conflictMarkersPresent: exists ? await fileHasConflictMarkers(filePath) : false
257
+ });
258
+ }
259
+ return { conflicts };
260
+ }
261
+ async getConflictVersions(workstream, repoPath) {
262
+ const [base, ours, theirs, current] = await Promise.all([
263
+ this.readGitStage(workstream, 1, repoPath),
264
+ this.readGitStage(workstream, 2, repoPath),
265
+ this.readGitStage(workstream, 3, repoPath),
266
+ readBoundedWorkingTreeFile(path.join(workstream.repoRoot, repoPath))
267
+ ]);
268
+ return {
269
+ path: repoPath,
270
+ stages: {
271
+ base: base.exists,
272
+ ours: ours.exists,
273
+ theirs: theirs.exists
274
+ },
275
+ base: base.content,
276
+ ours: ours.content,
277
+ theirs: theirs.content,
278
+ current: current.content,
279
+ currentExists: current.exists,
280
+ truncated: base.truncated || ours.truncated || theirs.truncated || current.truncated
281
+ };
282
+ }
283
+ async getDiff(workstream, options) {
284
+ const args = ["diff"];
285
+ if (options.staged) {
286
+ args.push("--cached");
287
+ }
288
+ if (options.path) {
289
+ args.push("--", options.path);
290
+ }
291
+ const result = await this.runGit(workstream.repoRoot, args);
292
+ const bounded = boundText(result.stdout, MAX_DIFF_BYTES);
293
+ return {
294
+ staged: options.staged,
295
+ unstaged: options.unstaged,
296
+ path: options.path ?? null,
297
+ diff: bounded.text,
298
+ truncated: bounded.truncated,
299
+ originalLength: result.stdout.length
300
+ };
301
+ }
302
+ async continueMerge(workstream) {
303
+ await this.requireActiveOperation(workstream, "merge");
304
+ const unresolved = await this.getUnresolvedConflictPaths(workstream.repoRoot);
305
+ if (unresolved.length > 0) {
306
+ throw new BackburnerGitToolError("unresolved_conflicts", `Unresolved conflict entries remain: ${unresolved.join(", ")}`);
307
+ }
308
+ const markerFiles = await this.findConflictMarkersInChangedTrackedFiles(workstream);
309
+ if (markerFiles.length > 0) {
310
+ throw new BackburnerGitToolError("conflict_markers_present", `Conflict markers remain in: ${markerFiles.join(", ")}`);
311
+ }
312
+ const files = classifyPorcelainEntries(await this.getPorcelainEntries(workstream.repoRoot));
313
+ if (files.unstagedFiles.length > 0 || files.untrackedFiles.length > 0) {
314
+ throw new BackburnerGitToolError("dirty_worktree", "Cannot continue merge with a dirty worktree " +
315
+ `(unstaged tracked: ${files.unstagedFiles.join(", ") || "none"}; ` +
316
+ `untracked: ${files.untrackedFiles.join(", ") || "none"}). ` +
317
+ "Stage intended modified files with backburner_git_mark_resolved or mark intended deleted files with " +
318
+ "backburner_git_mark_deleted. If a staged resolution was accidentally changed or deleted in the worktree, " +
319
+ "restore it with backburner_git_restore_worktree. Remove unintended untracked files before continuing. " +
320
+ "Then call backburner_git_continue_merge again.");
321
+ }
322
+ await this.runGit(workstream.repoRoot, ["commit", "--no-edit"]);
323
+ const state = await this.getState(workstream);
324
+ return {
325
+ commitSha: expectString(state.currentHeadSha, "currentHeadSha missing from Backburner Git state"),
326
+ state
327
+ };
328
+ }
329
+ async findConflictMarkersInChangedTrackedFiles(workstream) {
330
+ const result = await this.runGit(workstream.repoRoot, ["diff", "--name-only", "--cached"]);
331
+ const files = result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
332
+ const markerFiles = [];
333
+ for (const repoPath of files) {
334
+ if (await fileHasConflictMarkers(path.join(workstream.repoRoot, repoPath))) {
335
+ markerFiles.push(repoPath);
336
+ }
337
+ }
338
+ return markerFiles;
339
+ }
340
+ async rejectConflictMarkers(workstream, repoPath) {
341
+ if (await fileHasConflictMarkers(path.join(workstream.repoRoot, repoPath))) {
342
+ throw new BackburnerGitToolError("conflict_markers_present", `Path "${repoPath}" still contains conflict markers`);
343
+ }
344
+ }
345
+ async readGitStage(workstream, stage, repoPath) {
346
+ try {
347
+ const result = await this.runGit(workstream.repoRoot, ["show", `:${stage}:${repoPath}`]);
348
+ const bounded = boundText(result.stdout, MAX_FILE_BYTES);
349
+ return { exists: true, content: bounded.text, truncated: bounded.truncated };
350
+ }
351
+ catch (error) {
352
+ if (error instanceof CommandExecutionError) {
353
+ return { exists: false, content: null, truncated: false };
354
+ }
355
+ throw error;
356
+ }
357
+ }
358
+ async getActiveOperation(repoRoot) {
359
+ const gitDir = await this.getGitDir(repoRoot);
360
+ if (await pathExists(path.join(gitDir, "MERGE_HEAD")))
361
+ return "merge";
362
+ if (await pathExists(path.join(gitDir, "CHERRY_PICK_HEAD")))
363
+ return "cherry-pick";
364
+ if (await pathExists(path.join(gitDir, "REVERT_HEAD")))
365
+ return "revert";
366
+ if (await pathExists(path.join(gitDir, "REBASE_HEAD")) ||
367
+ await pathExists(path.join(gitDir, "rebase-merge")) ||
368
+ await pathExists(path.join(gitDir, "rebase-apply"))) {
369
+ return "rebase";
370
+ }
371
+ return "none";
372
+ }
373
+ async getGitDir(repoRoot) {
374
+ const result = await this.runGit(repoRoot, ["rev-parse", "--git-dir"]);
375
+ const raw = result.stdout.trim();
376
+ return path.isAbsolute(raw) ? raw : path.resolve(repoRoot, raw);
377
+ }
378
+ async getCurrentBranch(repoRoot) {
379
+ try {
380
+ const result = await this.runGit(repoRoot, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
381
+ return result.stdout.trim() || null;
382
+ }
383
+ catch {
384
+ return null;
385
+ }
386
+ }
387
+ async getStatusSummary(repoRoot) {
388
+ const result = await this.runGit(repoRoot, ["status", "--short"]);
389
+ return result.stdout.trim();
390
+ }
391
+ async getUnresolvedConflictPaths(repoRoot) {
392
+ const entries = await this.getPorcelainEntries(repoRoot);
393
+ return entries.filter(isUnmergedEntry).map((entry) => entry.path);
394
+ }
395
+ async getPorcelainEntries(repoRoot) {
396
+ const result = await this.runGit(repoRoot, ["status", "--porcelain"]);
397
+ return result.stdout.split(/\r?\n/).filter(Boolean).map(parsePorcelainEntry);
398
+ }
399
+ async getPorcelainEntryMap(repoRoot) {
400
+ return new Map((await this.getPorcelainEntries(repoRoot)).map((entry) => [entry.path, entry]));
401
+ }
402
+ async runGit(cwd, args) {
403
+ return await this.commandRunner.run("git", args, { cwd });
404
+ }
405
+ logInvocation(timestamp, toolName, rawArguments, status, message) {
406
+ const backburnerGitSessionId = isRecord(rawArguments) && typeof rawArguments.backburnerGitSessionId === "string"
407
+ ? rawArguments.backburnerGitSessionId
408
+ : undefined;
409
+ this.logger.info(JSON.stringify({
410
+ component: "backburner-git-mcp",
411
+ timestamp,
412
+ toolName,
413
+ backburnerGitSessionId: backburnerGitSessionId ?? "unknown",
414
+ workstreamId: backburnerGitSessionId
415
+ ? this.sessions.get(backburnerGitSessionId)?.workstreamId ?? "unknown"
416
+ : "unknown",
417
+ resultStatus: status,
418
+ ...(message ? { message } : {})
419
+ }));
420
+ }
421
+ }
422
+ function validateRepoRelativePath(repoRoot, inputPath) {
423
+ if (path.isAbsolute(inputPath) || /^[A-Za-z]:/.test(inputPath) || inputPath.startsWith("\\\\")) {
424
+ throw new BackburnerGitToolError("invalid_path", "Path must be repo-relative, not absolute");
425
+ }
426
+ const normalized = path.posix.normalize(inputPath.replaceAll("\\", "/"));
427
+ if (path.posix.isAbsolute(normalized) ||
428
+ normalized === "." ||
429
+ normalized === "" ||
430
+ normalized.startsWith("../") ||
431
+ normalized.includes("/../")) {
432
+ throw new BackburnerGitToolError("invalid_path", "Path must not contain '..' or be empty");
433
+ }
434
+ if (normalized === ".git" || normalized.startsWith(".git/")) {
435
+ throw new BackburnerGitToolError("invalid_path", "Path must not point inside .git");
436
+ }
437
+ const resolved = path.resolve(repoRoot, normalized);
438
+ const relative = path.relative(repoRoot, resolved);
439
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
440
+ throw new BackburnerGitToolError("invalid_path", "Path escapes the registered repo root");
441
+ }
442
+ return normalized;
443
+ }
444
+ function parsePorcelainEntry(line) {
445
+ const x = line[0] ?? " ";
446
+ const y = line[1] ?? " ";
447
+ const rawPath = line.slice(3);
448
+ if (rawPath.includes(" -> ")) {
449
+ const [originalPath, newPath] = rawPath.split(" -> ");
450
+ return originalPath === undefined
451
+ ? { x, y, path: newPath ?? rawPath }
452
+ : { x, y, path: newPath ?? rawPath, originalPath };
453
+ }
454
+ return { x, y, path: rawPath };
455
+ }
456
+ function isUnmergedEntry(entry) {
457
+ return ["DD", "AU", "UD", "UA", "DU", "AA", "UU"].includes(`${entry.x}${entry.y}`);
458
+ }
459
+ function isDeletionResolutionEntry(entry) {
460
+ return ["DD", "DU", "UD"].includes(`${entry.x}${entry.y}`);
461
+ }
462
+ function isCurrentDeletionEntry(entry) {
463
+ return isDeletionResolutionEntry(entry) || entry.y === "D";
464
+ }
465
+ function classifyPorcelainEntries(entries) {
466
+ return {
467
+ stagedFiles: entries
468
+ .filter((entry) => entry.x !== " " && entry.x !== "?" && !isUnmergedEntry(entry))
469
+ .map((entry) => entry.path),
470
+ unstagedFiles: entries
471
+ .filter((entry) => entry.y !== " " && entry.y !== "?" && !isUnmergedEntry(entry))
472
+ .map((entry) => entry.path),
473
+ untrackedFiles: entries
474
+ .filter((entry) => entry.x === "?" && entry.y === "?")
475
+ .map((entry) => entry.path)
476
+ };
477
+ }
478
+ function conflictType(entry) {
479
+ switch (`${entry.x}${entry.y}`) {
480
+ case "DD": return "deleted by us and them";
481
+ case "AU": return "added by us";
482
+ case "UD": return "deleted by them";
483
+ case "UA": return "added by them";
484
+ case "DU": return "deleted by us";
485
+ case "AA": return "both added";
486
+ case "UU": return "both modified";
487
+ default: return "unmerged";
488
+ }
489
+ }
490
+ async function fileHasConflictMarkers(filePath) {
491
+ const file = await readBoundedWorkingTreeFile(filePath);
492
+ return file.exists && CONFLICT_MARKER_RE.test(file.content ?? "");
493
+ }
494
+ async function readBoundedWorkingTreeFile(filePath) {
495
+ try {
496
+ const fileStat = await stat(filePath);
497
+ if (!fileStat.isFile()) {
498
+ return { exists: false, content: null, truncated: false };
499
+ }
500
+ const raw = await readFile(filePath, "utf8");
501
+ const bounded = boundText(raw, MAX_FILE_BYTES);
502
+ return { exists: true, content: bounded.text, truncated: bounded.truncated };
503
+ }
504
+ catch {
505
+ return { exists: false, content: null, truncated: false };
506
+ }
507
+ }
508
+ async function pathExists(filePath) {
509
+ try {
510
+ await stat(filePath);
511
+ return true;
512
+ }
513
+ catch {
514
+ return false;
515
+ }
516
+ }
517
+ function boundText(text, maxBytes) {
518
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) {
519
+ return { text, truncated: false };
520
+ }
521
+ return { text: text.slice(0, maxBytes) + "\n...[truncated]", truncated: true };
522
+ }
523
+ function expectBackburnerGitSessionId(value) {
524
+ const object = expectObject(value, "tool arguments must be an object");
525
+ return expectString(object.backburnerGitSessionId, '"backburnerGitSessionId" is required');
526
+ }
527
+ function expectObject(value, message) {
528
+ if (!isRecord(value)) {
529
+ throw new BackburnerGitToolError("invalid_arguments", message);
530
+ }
531
+ return value;
532
+ }
533
+ function expectString(value, message) {
534
+ if (typeof value !== "string" || value.trim() === "") {
535
+ throw new BackburnerGitToolError("invalid_arguments", message);
536
+ }
537
+ return value.trim();
538
+ }
539
+ function expectDiffOptions(value) {
540
+ const object = value === undefined ? {} : expectObject(value, '"options" must be an object');
541
+ const staged = object.staged === true;
542
+ const unstaged = object.unstaged !== false;
543
+ const filePath = object.path === undefined ? undefined : expectString(object.path, '"options.path" must be a string');
544
+ return {
545
+ staged,
546
+ unstaged,
547
+ ...(filePath ? { path: filePath } : {})
548
+ };
549
+ }
550
+ function toFailure(name, error) {
551
+ if (error instanceof BackburnerGitToolError) {
552
+ return { ok: false, tool: name, error: { code: error.code, message: error.message } };
553
+ }
554
+ return {
555
+ ok: false,
556
+ tool: name,
557
+ error: {
558
+ code: "backburner_git_error",
559
+ message: error instanceof Error ? error.message : String(error)
560
+ }
561
+ };
562
+ }
563
+ function success(name, summary, result) {
564
+ return { ok: true, tool: name, summary, result };
565
+ }
566
+ function isRecord(value) {
567
+ return value !== null && typeof value === "object" && !Array.isArray(value);
568
+ }
569
+ const TOOL_DEFINITIONS = [
570
+ {
571
+ name: "backburner_git_get_state",
572
+ title: "Get Backburner Git state",
573
+ description: "Return scoped Git state for the orchestrator-issued prepare-for-merge Backburner Git session.",
574
+ inputSchema: {
575
+ type: "object",
576
+ properties: { backburnerGitSessionId: { type: "string" } },
577
+ required: ["backburnerGitSessionId"]
578
+ }
579
+ },
580
+ {
581
+ name: "backburner_git_get_conflicts",
582
+ title: "Get Backburner Git conflicts",
583
+ description: "Return conflicted files for the orchestrator-issued prepare-for-merge Backburner Git session.",
584
+ inputSchema: {
585
+ type: "object",
586
+ properties: { backburnerGitSessionId: { type: "string" } },
587
+ required: ["backburnerGitSessionId"]
588
+ }
589
+ },
590
+ {
591
+ name: "backburner_git_get_conflict_versions",
592
+ title: "Get Backburner Git conflict versions",
593
+ description: "Return base, ours, theirs, and current versions for one conflicted or changed path.",
594
+ inputSchema: {
595
+ type: "object",
596
+ properties: { backburnerGitSessionId: { type: "string" }, path: { type: "string" } },
597
+ required: ["backburnerGitSessionId", "path"]
598
+ }
599
+ },
600
+ {
601
+ name: "backburner_git_get_diff",
602
+ title: "Get Backburner Git diff",
603
+ description: "Return a bounded diff for the orchestrator-issued prepare-for-merge Backburner Git session.",
604
+ inputSchema: {
605
+ type: "object",
606
+ properties: {
607
+ backburnerGitSessionId: { type: "string" },
608
+ options: {
609
+ type: "object",
610
+ properties: {
611
+ staged: { type: "boolean" },
612
+ unstaged: { type: "boolean" },
613
+ path: { type: "string" }
614
+ }
615
+ }
616
+ },
617
+ required: ["backburnerGitSessionId"]
618
+ }
619
+ },
620
+ {
621
+ name: "backburner_git_mark_resolved",
622
+ title: "Mark Backburner Git paths resolved",
623
+ description: "Stage resolved paths in the orchestrator-issued merge-conflict Backburner Git session.",
624
+ inputSchema: {
625
+ type: "object",
626
+ properties: { backburnerGitSessionId: { type: "string" }, paths: { type: "array", items: { type: "string" } } },
627
+ required: ["backburnerGitSessionId", "paths"]
628
+ }
629
+ },
630
+ {
631
+ name: "backburner_git_mark_deleted",
632
+ title: "Mark Backburner Git deleted paths resolved",
633
+ description: "Resolve intentional deletions in the orchestrator-issued merge-conflict Backburner Git session.",
634
+ inputSchema: {
635
+ type: "object",
636
+ properties: { backburnerGitSessionId: { type: "string" }, paths: { type: "array", items: { type: "string" } } },
637
+ required: ["backburnerGitSessionId", "paths"]
638
+ }
639
+ },
640
+ {
641
+ name: "backburner_git_unstage",
642
+ title: "Unstage Backburner Git paths",
643
+ description: "Unstage paths in the orchestrator-issued prepare-for-merge Backburner Git session.",
644
+ inputSchema: {
645
+ type: "object",
646
+ properties: { backburnerGitSessionId: { type: "string" }, paths: { type: "array", items: { type: "string" } } },
647
+ required: ["backburnerGitSessionId", "paths"]
648
+ }
649
+ },
650
+ {
651
+ name: "backburner_git_restore_worktree",
652
+ title: "Restore Backburner Git worktree paths",
653
+ description: "Restore tracked unstaged worktree changes from the index without changing staged merge resolutions.",
654
+ inputSchema: {
655
+ type: "object",
656
+ properties: { backburnerGitSessionId: { type: "string" }, paths: { type: "array", items: { type: "string" } } },
657
+ required: ["backburnerGitSessionId", "paths"]
658
+ }
659
+ },
660
+ {
661
+ name: "backburner_git_continue_merge",
662
+ title: "Continue Backburner Git merge",
663
+ description: "Complete the active merge after all conflicts are resolved and the worktree has no unstaged tracked or untracked files. This tool never pushes.",
664
+ inputSchema: {
665
+ type: "object",
666
+ properties: { backburnerGitSessionId: { type: "string" } },
667
+ required: ["backburnerGitSessionId"]
668
+ }
669
+ },
670
+ {
671
+ name: "backburner_git_abort_merge",
672
+ title: "Abort Backburner Git merge",
673
+ description: "Abort the active merge for the orchestrator-issued prepare-for-merge Backburner Git session. This tool never deletes clones.",
674
+ inputSchema: {
675
+ type: "object",
676
+ properties: { backburnerGitSessionId: { type: "string" } },
677
+ required: ["backburnerGitSessionId"]
678
+ }
679
+ }
680
+ ];