@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,442 @@
1
+ import { exportSession } from "../workstreams/plan-breakdown-generator/export.js";
2
+ import { buildPacketPlanFromPlanBreakdown } from "../workstreams/packet-plan.js";
3
+ import { exportWaveAssessmentSession } from "../workstreams/wave-assessment/session.js";
4
+ export function extractAndExportPlanBreakdownResult(task, stdout, sessionStore) {
5
+ if (!requiresPlanBreakdownResult(task)) {
6
+ return { ok: false, reason: "not_plan_breakdown", message: "Task does not require a plan breakdown result." };
7
+ }
8
+ const trimmed = stdout.trim();
9
+ if (trimmed.length === 0) {
10
+ return { ok: false, reason: "empty_stdout", message: "Agent output is empty." };
11
+ }
12
+ const candidate = extractTrailingJsonObject(trimmed);
13
+ if (!candidate) {
14
+ return { ok: false, reason: "missing_json", message: "No JSON object found in agent output." };
15
+ }
16
+ let parsed;
17
+ try {
18
+ parsed = JSON.parse(candidate);
19
+ }
20
+ catch {
21
+ return { ok: false, reason: "malformed_json", message: "Malformed JSON object in agent output." };
22
+ }
23
+ if (!isRecord(parsed) || typeof parsed.sessionId !== "string") {
24
+ return {
25
+ ok: false,
26
+ reason: "missing_session_id",
27
+ message: 'Agent response must be a JSON object with a "sessionId" string.'
28
+ };
29
+ }
30
+ const { sessionId } = parsed;
31
+ const normalizedSessionId = sessionId.trim();
32
+ if (!normalizedSessionId) {
33
+ return {
34
+ ok: false,
35
+ reason: "missing_session_id",
36
+ message: 'Agent response "sessionId" must not be empty.'
37
+ };
38
+ }
39
+ if (!sessionStore) {
40
+ return {
41
+ ok: false,
42
+ reason: "session_store_unavailable",
43
+ message: "Plan breakdown session store is unavailable for this agent."
44
+ };
45
+ }
46
+ const session = sessionStore.getSessionSync(normalizedSessionId);
47
+ if (!session) {
48
+ return {
49
+ ok: false,
50
+ reason: "unknown_session_id",
51
+ message: `Unknown session ID: "${normalizedSessionId}"`
52
+ };
53
+ }
54
+ let planBreakdown;
55
+ try {
56
+ planBreakdown = exportSession(session);
57
+ }
58
+ catch (error) {
59
+ return {
60
+ ok: false,
61
+ reason: "export_failed",
62
+ message: error instanceof Error ? error.message : String(error)
63
+ };
64
+ }
65
+ const workstreamId = getTaskWorkstreamId(task);
66
+ if (!workstreamId) {
67
+ return {
68
+ ok: false,
69
+ reason: "missing_workstream_id",
70
+ message: "Plan breakdown task is missing canonical workstream identity."
71
+ };
72
+ }
73
+ try {
74
+ buildPacketPlanFromPlanBreakdown({
75
+ workstreamId,
76
+ planBreakdown,
77
+ generatedAt: new Date().toISOString()
78
+ });
79
+ }
80
+ catch (error) {
81
+ return {
82
+ ok: false,
83
+ reason: "packet_plan_invalid",
84
+ message: error instanceof Error ? error.message : String(error)
85
+ };
86
+ }
87
+ return { ok: true, sessionId: normalizedSessionId, planBreakdown };
88
+ }
89
+ export function extractAndExportWaveAssessmentResult(task, stdout, sessionStore) {
90
+ if (task.kind !== "wave_plan_assessment") {
91
+ return {
92
+ ok: false,
93
+ reason: "not_wave_plan_assessment",
94
+ message: "Task is not a wave plan assessment."
95
+ };
96
+ }
97
+ const trimmed = stdout.trim();
98
+ if (trimmed.length === 0) {
99
+ return { ok: false, reason: "empty_stdout", message: "Agent output is empty." };
100
+ }
101
+ const candidate = extractTrailingJsonObject(trimmed);
102
+ if (!candidate) {
103
+ return { ok: false, reason: "missing_json", message: "No JSON object found in agent output." };
104
+ }
105
+ let parsed;
106
+ try {
107
+ parsed = JSON.parse(candidate);
108
+ }
109
+ catch {
110
+ return { ok: false, reason: "malformed_json", message: "Malformed JSON object in agent output." };
111
+ }
112
+ if (!isRecord(parsed) || typeof parsed.sessionId !== "string" || parsed.sessionId.trim() === "") {
113
+ return {
114
+ ok: false,
115
+ reason: "missing_session_id",
116
+ message: 'Agent response must be a JSON object with a non-empty "sessionId" string.'
117
+ };
118
+ }
119
+ if (!sessionStore) {
120
+ return {
121
+ ok: false,
122
+ reason: "session_store_unavailable",
123
+ message: "Wave assessment session store is unavailable for this agent."
124
+ };
125
+ }
126
+ const checkpoint = task.context.waveAssessmentCheckpoint;
127
+ if (!checkpoint) {
128
+ return {
129
+ ok: false,
130
+ reason: "checkpoint_missing",
131
+ message: "Wave assessment task is missing checkpoint context."
132
+ };
133
+ }
134
+ const sessionId = parsed.sessionId.trim();
135
+ const session = sessionStore.getSessionSync(sessionId);
136
+ if (!session) {
137
+ return {
138
+ ok: false,
139
+ reason: "unknown_session_id",
140
+ message: `Unknown wave assessment session ID: "${sessionId}"`
141
+ };
142
+ }
143
+ let exported;
144
+ try {
145
+ exported = exportWaveAssessmentSession(session);
146
+ }
147
+ catch (error) {
148
+ return {
149
+ ok: false,
150
+ reason: "export_failed",
151
+ message: error instanceof Error ? error.message : String(error)
152
+ };
153
+ }
154
+ const mismatch = findWaveAssessmentCheckpointMismatch(exported, checkpoint.identity);
155
+ if (mismatch) {
156
+ return {
157
+ ok: false,
158
+ reason: "checkpoint_mismatch",
159
+ message: mismatch
160
+ };
161
+ }
162
+ return {
163
+ ok: true,
164
+ sessionId,
165
+ waveAssessment: {
166
+ id: `assessment:${checkpoint.id}`,
167
+ sessionId,
168
+ checkpoint,
169
+ decision: exported.decision,
170
+ summary: exported.summary,
171
+ findings: exported.findings,
172
+ status: "finalized",
173
+ createdAt: exported.timestamps.createdAt,
174
+ finalizedAt: exported.timestamps.finalizedAt,
175
+ updatedAt: exported.timestamps.finalizedAt
176
+ }
177
+ };
178
+ }
179
+ function findWaveAssessmentCheckpointMismatch(exported, identity) {
180
+ const checks = [
181
+ ["workstreamId", exported.workstreamId, identity.workstreamId],
182
+ ["waveId", exported.waveId, identity.waveId],
183
+ ["parentPrNumber", exported.parentPrNumber, identity.parentPrNumber],
184
+ ["parentHeadSha", exported.assessedParentHeadSha, identity.parentHeadSha],
185
+ ["planRevision", exported.currentPlanRevision, identity.planRevision],
186
+ ["originalPlanningSessionId", exported.originalPlanningSessionId, identity.originalPlanningSessionId]
187
+ ];
188
+ const mismatch = checks.find(([, left, right]) => left !== right);
189
+ if (mismatch) {
190
+ return `Wave assessment session ${mismatch[0]} "${mismatch[1]}" does not match checkpoint "${mismatch[2]}".`;
191
+ }
192
+ if (!arraysEqual(exported.planNodeIds, identity.planNodeIds)) {
193
+ return `Wave assessment session planNodeIds "${formatStringArray(exported.planNodeIds)}" do not match checkpoint "${formatStringArray(identity.planNodeIds)}".`;
194
+ }
195
+ return undefined;
196
+ }
197
+ function arraysEqual(left, right) {
198
+ if (!left || !right) {
199
+ return false;
200
+ }
201
+ return left.length === right.length && left.every((value, index) => value === right[index]);
202
+ }
203
+ function formatStringArray(values) {
204
+ return values?.join(",") ?? "<missing>";
205
+ }
206
+ function getTaskWorkstreamId(task) {
207
+ return typeof task.workstreamId === "string" && task.workstreamId.trim()
208
+ ? task.workstreamId
209
+ : undefined;
210
+ }
211
+ export function extractTriageResult(task, stdout) {
212
+ if (task.kind !== "triage_issue") {
213
+ return undefined;
214
+ }
215
+ const trimmed = stdout.trim();
216
+ if (trimmed.length === 0) {
217
+ return undefined;
218
+ }
219
+ const candidate = extractTrailingJsonObject(trimmed);
220
+ if (!candidate) {
221
+ return undefined;
222
+ }
223
+ try {
224
+ const parsed = JSON.parse(candidate);
225
+ if (parsed.decision !== "answer_only" && parsed.decision !== "start_implementation") {
226
+ return undefined;
227
+ }
228
+ return {
229
+ decision: parsed.decision,
230
+ ...(typeof parsed.summary === "string" ? { summary: parsed.summary } : {}),
231
+ ...(typeof parsed.reply === "string" ? { reply: parsed.reply } : {})
232
+ };
233
+ }
234
+ catch {
235
+ return undefined;
236
+ }
237
+ }
238
+ export function extractReviewResult(task, stdout) {
239
+ if (task.kind !== "review_pr") {
240
+ return undefined;
241
+ }
242
+ const trimmed = stdout.trim();
243
+ if (trimmed.length === 0) {
244
+ return undefined;
245
+ }
246
+ const candidate = extractTrailingJsonObject(trimmed);
247
+ if (!candidate) {
248
+ return undefined;
249
+ }
250
+ try {
251
+ const parsed = JSON.parse(candidate);
252
+ // Handle new shape
253
+ if (typeof parsed.action === "string" && ["COMMENT", "APPROVE", "REQUEST_CHANGES"].includes(parsed.action)) {
254
+ const action = parsed.action;
255
+ const summary = typeof parsed.summary === "string" ? parsed.summary.trim() : "";
256
+ if (!summary) {
257
+ return undefined;
258
+ }
259
+ const inlineComments = Array.isArray(parsed.inlineComments)
260
+ ? parsed.inlineComments
261
+ .map((comment) => {
262
+ if (!isRecord(comment)) {
263
+ return undefined;
264
+ }
265
+ const path = typeof comment.path === "string" ? comment.path.trim() : undefined;
266
+ const line = typeof comment.line === "number" ? comment.line : undefined;
267
+ const body = typeof comment.body === "string" ? comment.body.trim() : undefined;
268
+ const side = comment.side === "LEFT" || comment.side === "RIGHT"
269
+ ? comment.side
270
+ : undefined;
271
+ const startLine = typeof comment.startLine === "number" ? comment.startLine : undefined;
272
+ const startSide = comment.startSide === "LEFT" || comment.startSide === "RIGHT"
273
+ ? comment.startSide
274
+ : undefined;
275
+ return {
276
+ ...(path ? { path } : {}),
277
+ ...(line !== undefined ? { line } : {}),
278
+ ...(body ? { body } : {}),
279
+ ...(side ? { side } : {}),
280
+ ...(startLine !== undefined ? { startLine } : {}),
281
+ ...(startSide ? { startSide } : {})
282
+ };
283
+ })
284
+ .filter((c) => c !== undefined)
285
+ : undefined;
286
+ return {
287
+ action,
288
+ summary,
289
+ ...(inlineComments && inlineComments.length > 0 ? { inlineComments } : {})
290
+ };
291
+ }
292
+ // Handle legacy shape
293
+ if (parsed.decision === "approve" || parsed.decision === "request_changes") {
294
+ const action = parsed.decision === "approve" ? "APPROVE" : "REQUEST_CHANGES";
295
+ const summary = typeof parsed.summary === "string" ? parsed.summary.trim() : "Review submitted.";
296
+ const findings = Array.isArray(parsed.findings)
297
+ ? parsed.findings
298
+ .map((finding) => {
299
+ if (!isRecord(finding)) {
300
+ return undefined;
301
+ }
302
+ const path = typeof finding.path === "string" ? finding.path.trim() : undefined;
303
+ const line = typeof finding.line === "number" ? finding.line : undefined;
304
+ const body = typeof finding.body === "string" ? finding.body.trim() : undefined;
305
+ return {
306
+ ...(path ? { path } : {}),
307
+ ...(line !== undefined ? { line } : {}),
308
+ ...(body ? { body } : {})
309
+ };
310
+ })
311
+ .filter((finding) => finding !== undefined)
312
+ : undefined;
313
+ return {
314
+ action,
315
+ summary,
316
+ decision: parsed.decision,
317
+ ...(findings && findings.length > 0
318
+ ? {
319
+ inlineComments: findings.map((f) => ({ ...f, side: "RIGHT" })),
320
+ findings
321
+ }
322
+ : {})
323
+ };
324
+ }
325
+ return undefined;
326
+ }
327
+ catch {
328
+ return undefined;
329
+ }
330
+ }
331
+ export function extractPlanBreakdownResult(task, stdout, sessionStore) {
332
+ if (!requiresPlanBreakdownResult(task)) {
333
+ return undefined;
334
+ }
335
+ const trimmed = stdout.trim();
336
+ if (trimmed.length === 0) {
337
+ return undefined;
338
+ }
339
+ const candidate = extractTrailingJsonObject(trimmed);
340
+ if (!candidate) {
341
+ return undefined;
342
+ }
343
+ try {
344
+ const parsed = JSON.parse(candidate);
345
+ // Support for sessionId format
346
+ if (isRecord(parsed) && typeof parsed.sessionId === "string" && sessionStore) {
347
+ const sessionId = parsed.sessionId.trim();
348
+ if (!sessionId) {
349
+ return undefined;
350
+ }
351
+ const session = sessionStore.getSessionSync(sessionId);
352
+ if (session) {
353
+ return exportSession(session);
354
+ }
355
+ }
356
+ return undefined;
357
+ }
358
+ catch {
359
+ return undefined;
360
+ }
361
+ }
362
+ function requiresPlanBreakdownResult(task) {
363
+ return task.kind === "plan_breakdown" || task.kind === "wave_plan_replanning";
364
+ }
365
+ function requiredStringArray(value) {
366
+ if (!Array.isArray(value))
367
+ return undefined;
368
+ if (value.some((v) => typeof v !== "string"))
369
+ return undefined;
370
+ return value;
371
+ }
372
+ function nonEmptyString(value) {
373
+ if (typeof value !== "string")
374
+ return undefined;
375
+ const trimmed = value.trim();
376
+ return trimmed.length > 0 ? trimmed : undefined;
377
+ }
378
+ export function isRecord(value) {
379
+ return typeof value === "object" && value !== null;
380
+ }
381
+ export function extractTrailingJsonObject(value) {
382
+ const end = value.lastIndexOf("}");
383
+ if (end === -1) {
384
+ return undefined;
385
+ }
386
+ let depth = 0;
387
+ let inString = false;
388
+ let escaped = false;
389
+ for (let index = end; index >= 0; index -= 1) {
390
+ const character = value[index];
391
+ if (escaped) {
392
+ escaped = false;
393
+ continue;
394
+ }
395
+ if (character === "\\") {
396
+ escaped = true;
397
+ continue;
398
+ }
399
+ if (character === '"') {
400
+ inString = !inString;
401
+ continue;
402
+ }
403
+ if (inString) {
404
+ continue;
405
+ }
406
+ if (character === "}") {
407
+ depth += 1;
408
+ continue;
409
+ }
410
+ if (character === "{") {
411
+ depth -= 1;
412
+ if (depth === 0) {
413
+ return value.slice(index, end + 1);
414
+ }
415
+ }
416
+ }
417
+ return undefined;
418
+ }
419
+ export function buildExecutionId(taskId, startedAt) {
420
+ const safeTaskId = taskId.replace(/[^a-zA-Z0-9_-]+/g, "_");
421
+ const safeTimestamp = startedAt.replace(/[:.]/g, "-");
422
+ return `exec-${safeTaskId}-${safeTimestamp}`;
423
+ }
424
+ export function extractExitCode(error) {
425
+ if (typeof error === "object" && error !== null && "exitCode" in error && typeof error.exitCode === "number") {
426
+ return error.exitCode;
427
+ }
428
+ return 1;
429
+ }
430
+ export function extractStdout(error) {
431
+ if (typeof error === "object" && error !== null && "stdout" in error && typeof error.stdout === "string") {
432
+ return error.stdout;
433
+ }
434
+ return "";
435
+ }
436
+ export function extractStderr(error) {
437
+ if (typeof error === "object" && error !== null && "stderr" in error && typeof error.stderr === "string") {
438
+ return error.stderr;
439
+ }
440
+ return error instanceof Error ? error.message : String(error);
441
+ }
442
+ export { detectProviderLimit } from "./limit-detector.js";
@@ -0,0 +1,132 @@
1
+ import { Task } from "../tasks/task.js";
2
+ export class GitHubTaskFailureNotifier {
3
+ gitHubGateway;
4
+ logger;
5
+ constructor(gitHubGateway, repos, logger) {
6
+ this.gitHubGateway = gitHubGateway;
7
+ this.logger = logger;
8
+ this.reposById = new Map(repos.map((repo) => [repo.id, repo]));
9
+ }
10
+ reposById;
11
+ async notifyFailure(task, execution, customMessage) {
12
+ const wrappedTask = Task.from(task);
13
+ const repo = this.reposById.get(task.repoId);
14
+ if (!repo) {
15
+ return undefined;
16
+ }
17
+ const targetNumber = resolveFailureThreadNumber(wrappedTask);
18
+ if (targetNumber === undefined) {
19
+ return undefined;
20
+ }
21
+ const body = customMessage ?? buildTechnicalFailureComment(task, execution);
22
+ const result = await this.gitHubGateway.postIssueComment({
23
+ owner: repo.owner,
24
+ name: repo.name,
25
+ issueNumber: targetNumber,
26
+ body
27
+ });
28
+ this.logger?.warn(JSON.stringify({
29
+ component: "task-failure-comment",
30
+ taskId: task.id,
31
+ taskKind: task.kind,
32
+ repoSlug: task.repoSlug,
33
+ threadNumber: targetNumber,
34
+ executionId: execution.id,
35
+ commentId: result.id,
36
+ commentUrl: result.url
37
+ }));
38
+ return String(result.id);
39
+ }
40
+ }
41
+ export function buildTechnicalFailureComment(task, execution) {
42
+ const detail = summarizeExecutionFailure(execution);
43
+ return [
44
+ "🤖 I tried to handle this orchestrator task, but it failed for a technical reason and did not complete.",
45
+ "",
46
+ `Task: \`${task.kind}\``,
47
+ `Execution: \`${execution.id}\``,
48
+ `Failure: ${detail}`,
49
+ "",
50
+ 'If you want me to try again after the underlying issue is fixed, reply with `retry: <url-of-this-comment>`.'
51
+ ].join("\n");
52
+ }
53
+ function resolveFailureThreadNumber(task) {
54
+ const targetId = task.getOwningThreadId() ?? task.sourceId;
55
+ const match = targetId.match(/:(\d+)$/);
56
+ return match ? Number(match[1]) : undefined;
57
+ }
58
+ function summarizeExecutionFailure(execution) {
59
+ if (isTimeoutExecution(execution)) {
60
+ return formatTimeoutFailure(execution);
61
+ }
62
+ return summarizeExecutionError(execution.errorMessage);
63
+ }
64
+ function summarizeExecutionError(errorMessage) {
65
+ if (!errorMessage) {
66
+ return "Unknown technical failure.";
67
+ }
68
+ const trimmed = errorMessage.trim();
69
+ const firstNonEmptyLine = trimmed
70
+ .split(/\r?\n/)
71
+ .map((line) => line.trim())
72
+ .find((line) => line.length > 0);
73
+ const summary = firstNonEmptyLine ?? "Unknown technical failure.";
74
+ return summary.length > 240 ? `${summary.slice(0, 237)}...` : summary;
75
+ }
76
+ function isTimeoutExecution(execution) {
77
+ return execution.exitCode === 124 || /timed out/i.test(execution.errorMessage ?? "");
78
+ }
79
+ function formatTimeoutFailure(execution) {
80
+ const timeoutMs = execution.command.timeoutMs;
81
+ const timeoutLimit = timeoutMs > 0 ? `${timeoutMs}ms` : "the configured timeout";
82
+ return `Timed out after ${timeoutLimit} (exitCode: ${execution.exitCode}).`;
83
+ }
84
+ export function buildMcpBlockedFailureComment(task, execution, blockers) {
85
+ const blockerDetails = blockers
86
+ .map((b) => `- **${b.mcpHandle}**: ${b.safeReason}`)
87
+ .join("\n");
88
+ return [
89
+ "🤖 I tried to handle this orchestrator task, but it is blocked because a required MCP capability requires authorization, configuration, or setup.",
90
+ "",
91
+ `Task: \`${task.kind}\``,
92
+ `Execution: \`${execution.id}\``,
93
+ "",
94
+ "### Blocked Capabilities/MCPs",
95
+ blockerDetails,
96
+ "",
97
+ "Please resolve the MCP setup requirement. Once resolved, the task will be resumed automatically."
98
+ ].join("\n");
99
+ }
100
+ export function buildMcpBlockerNotificationComment(task, blocker, execution) {
101
+ return [
102
+ "🤖 **MCP capability blocked**",
103
+ "",
104
+ `The task \`${task.id}\` (\`${task.kind}\`) requires MCP-backed capability ${formatCapabilityNames(blocker.affectedCapabilityNames)}, but the \`${blocker.mcpHandle}\` MCP connection needs authorization or configuration.`,
105
+ ...(execution ? [`Execution: \`${execution.id}\``] : []),
106
+ "",
107
+ `Reason: ${sanitizeMcpCommentText(blocker.safeReason)}`,
108
+ "",
109
+ "Options:",
110
+ `1. Complete the required setup for \`${blocker.mcpHandle}\`, then rerun the orchestrator. The task will remain pending until the MCP becomes ready.`,
111
+ `2. Skip this MCP for this task only by replying exactly: \`mcp-skip: ${task.id} ${blocker.mcpHandle}\``,
112
+ "",
113
+ "If skipped, the agent will proceed without these tools and must find a workaround or report that the task cannot be completed correctly."
114
+ ].join("\n");
115
+ }
116
+ function formatCapabilityNames(names) {
117
+ if (names.length === 0) {
118
+ return "the configured capability";
119
+ }
120
+ if (names.length === 1) {
121
+ return `\`${names[0]}\``;
122
+ }
123
+ return names.map((name) => `\`${name}\``).join(", ");
124
+ }
125
+ function sanitizeMcpCommentText(value) {
126
+ const text = value?.trim() || "MCP requires user authorization or configuration.";
127
+ return text
128
+ .replace(/https?:\/\/\S+/gi, "[redacted-url]")
129
+ .replace(/\b(token|secret|api[_-]?key)\s*[:=]\s*\S+/gi, "$1=[redacted]")
130
+ .replace(/\bBearer\s+\S+/gi, "Bearer [redacted]")
131
+ .slice(0, 300);
132
+ }
@@ -0,0 +1,96 @@
1
+ import { writeJsonFileAtomic } from "../utils/json.js";
2
+ export const DEFAULT_GEMINI_MODEL = "gemini-3-flash-preview";
3
+ export const GEMINI_MODEL_FALLBACKS = [
4
+ DEFAULT_GEMINI_MODEL,
5
+ "gemini-3.1-pro-preview",
6
+ "gemini-3.1-flash-lite-preview"
7
+ ];
8
+ const GEMINI_HEADLESS_PROMPT = "";
9
+ /**
10
+ * Maps orchestrator permissions to Gemini approval modes.
11
+ * - read-only -> "plan"
12
+ * - workspace-write -> "yolo"
13
+ */
14
+ export function mapPermissionToApprovalMode(permission) {
15
+ switch (permission) {
16
+ case "read-only":
17
+ return "plan";
18
+ case "workspace-write":
19
+ return "yolo";
20
+ }
21
+ }
22
+ /**
23
+ * Builds Gemini CLI arguments while sending the prompt over stdin.
24
+ */
25
+ export function buildGeminiCliArgs(options) {
26
+ const args = [
27
+ "--model",
28
+ options.model ?? DEFAULT_GEMINI_MODEL,
29
+ "--approval-mode",
30
+ options.approvalMode,
31
+ "--output-format",
32
+ options.outputFormat ?? "text"
33
+ ];
34
+ if (options.resumeSessionId) {
35
+ args.push("--resume", options.resumeSessionId);
36
+ }
37
+ args.push("--prompt", GEMINI_HEADLESS_PROMPT);
38
+ return args;
39
+ }
40
+ export async function writeGeminiSystemSettings(settingsPath, options) {
41
+ const settings = options.mcp ? buildMcpSettings(options.mcp) : {};
42
+ await writeJsonFileAtomic(settingsPath, settings);
43
+ }
44
+ /**
45
+ * Removes auth-switching env vars and points Gemini at a run-scoped system settings file.
46
+ */
47
+ export function getGeminiEnvOverrides(systemSettingsPath) {
48
+ const overrides = {
49
+ GEMINI_CLI_SYSTEM_SETTINGS_PATH: systemSettingsPath
50
+ };
51
+ // We explicitly want to reuse Google-account login and avoid drifting into API-key mode.
52
+ const toRemove = ["GEMINI_API_KEY", "GOOGLE_API_KEY", "GOOGLE_GENAI_USE_VERTEXAI"];
53
+ for (const key of toRemove) {
54
+ overrides[key] = undefined;
55
+ }
56
+ return overrides;
57
+ }
58
+ /**
59
+ * Builds Gemini MCP system settings for the local broker.
60
+ * Inspection showed the broker uses streamable HTTP, so we use httpUrl.
61
+ */
62
+ function buildMcpSettings(mcp) {
63
+ return {
64
+ mcp: {
65
+ allowed: [mcp.serverName]
66
+ },
67
+ mcpServers: {
68
+ [mcp.serverName]: {
69
+ httpUrl: mcp.serverUrl,
70
+ includeTools: mcp.toolAllowList
71
+ }
72
+ }
73
+ };
74
+ }
75
+ export function buildGlobalGeminiLaunchInstructions(input) {
76
+ return [
77
+ "# Global Gemini Instructions",
78
+ "",
79
+ "These instructions apply across repositories for this orchestrated run.",
80
+ "",
81
+ "## MCP Server",
82
+ `Use the local MCP server \`${input.mcp.serverName}\` for managed GitHub operations.`,
83
+ "- Invoke broker tools instead of using `gh` directly.",
84
+ "",
85
+ "Enabled broker tools for this run:",
86
+ ...input.mcp.toolAllowList.map((tool) => `- \`${tool}\``),
87
+ "",
88
+ "Any GitHub-authored message you post must begin with `🤖`."
89
+ ].join("\n");
90
+ }
91
+ export function prependGlobalGeminiLaunchInstructions(promptText, options) {
92
+ if (!options.mcp) {
93
+ return promptText;
94
+ }
95
+ return `${buildGlobalGeminiLaunchInstructions({ mcp: options.mcp })}\n\n${promptText}`;
96
+ }