@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,392 @@
1
+ import { closeSync, openSync, writeSync } from "node:fs";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import { writeJsonFileAtomic } from "../utils/json.js";
5
+ import { buildGeminiCliArgs, GEMINI_MODEL_FALLBACKS, getGeminiEnvOverrides, mapPermissionToApprovalMode, prependGlobalGeminiLaunchInstructions, writeGeminiSystemSettings } from "./gemini-launch.js";
6
+ import { buildExecutionId, extractExitCode, extractAndExportPlanBreakdownResult, extractAndExportWaveAssessmentResult, isRecord, extractReviewResult, extractStderr, extractStdout, extractTriageResult, detectProviderLimit } from "./extraction.js";
7
+ import { buildPromptPacket, renderPrompt } from "./prompt.js";
8
+ const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
9
+ const GEMINI_RETRYABLE_EXHAUSTION_PATTERN = /\b(?:QUOTA_EXHAUSTED|RESOURCE_EXHAUSTED|MODEL_CAPACITY_EXHAUSTED)\b|no capacity available|model capacity|quota/i;
10
+ export class GeminiCliAgent {
11
+ options;
12
+ now;
13
+ timeoutMs;
14
+ logger;
15
+ constructor(options) {
16
+ this.options = options;
17
+ this.now = options.now ?? (() => new Date().toISOString());
18
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
19
+ this.logger = options.logger;
20
+ }
21
+ async dispatch(task, context, agentConfig, sessionDecision, promptContent) {
22
+ const promptPacket = buildPromptPacket(task, context);
23
+ const promptText = prependGlobalGeminiLaunchInstructions(renderPrompt(task, promptPacket, {
24
+ providerIdentity: "Gemini CLI",
25
+ customConstraints: promptContent?.content,
26
+ learningOverlays: promptContent?.learningOverlays
27
+ }), { ...(this.options.githubBroker ? { mcp: this.options.githubBroker } : {}) });
28
+ const startedAt = this.now();
29
+ const executionId = buildExecutionId(task.id, startedAt);
30
+ const artifactDir = path.join(this.options.logDir, "artifacts", executionId);
31
+ const promptPacketPath = path.join(artifactDir, "prompt-packet.json");
32
+ const promptArtifactPath = path.join(artifactDir, "prompt.md");
33
+ const stdoutArtifactPath = path.join(artifactDir, "stdout.txt");
34
+ const stderrArtifactPath = path.join(artifactDir, "stderr.txt");
35
+ const streamArtifactPath = path.join(artifactDir, "stdout-stream.jsonl");
36
+ const settingsArtifactPath = path.join(artifactDir, "gemini-system-settings.json");
37
+ await mkdir(artifactDir, { recursive: true });
38
+ await writeGeminiSystemSettings(settingsArtifactPath, {
39
+ ...(this.options.githubBroker ? { mcp: this.options.githubBroker } : {})
40
+ });
41
+ await Promise.all([
42
+ writeJsonFileAtomic(promptPacketPath, promptPacket),
43
+ writeFile(promptArtifactPath, promptText, "utf8")
44
+ ]);
45
+ const approvalMode = mapPermissionToApprovalMode(task.executionPermission);
46
+ const envOverrides = getGeminiEnvOverrides(settingsArtifactPath);
47
+ const cwd = context.localRepoPath ?? process.cwd();
48
+ this.logger?.info(JSON.stringify({
49
+ component: "gemini-launch",
50
+ taskKind: task.kind,
51
+ targetRepoPath: cwd,
52
+ systemSettingsPath: settingsArtifactPath,
53
+ mcpServerUrl: this.options.githubBroker?.serverUrl
54
+ }));
55
+ let stdoutStreamFd;
56
+ let stderrStreamFd;
57
+ const resumeSessionId = sessionDecision?.mode === "native_resume_candidate"
58
+ ? sessionDecision.record.providerSessionId
59
+ : undefined;
60
+ try {
61
+ stdoutStreamFd = openSync(streamArtifactPath, "w");
62
+ stderrStreamFd = openSync(stderrArtifactPath, "w");
63
+ const result = await this.runWithModelFallbacks({
64
+ command: agentConfig.command,
65
+ approvalMode,
66
+ cwd,
67
+ env: envOverrides,
68
+ promptText,
69
+ resumeSessionId,
70
+ ...(agentConfig.model ? { model: agentConfig.model } : {}),
71
+ onOutputChunk: (stream, chunk) => {
72
+ if (stream === "stdout" && stdoutStreamFd !== undefined) {
73
+ writeSync(stdoutStreamFd, chunk);
74
+ }
75
+ if (stream === "stderr" && stderrStreamFd !== undefined) {
76
+ writeSync(stderrStreamFd, chunk);
77
+ }
78
+ }
79
+ });
80
+ const derivedStdout = result.exitCode === 0 ? extractGeminiStreamJsonAssistantText(result.stdout) ?? result.stdout : result.stdout;
81
+ const stdout = withAttemptNotes(derivedStdout, result.attemptNotes);
82
+ const stderr = result.stderr;
83
+ const endedAt = this.now();
84
+ await Promise.all([
85
+ writeFile(stdoutArtifactPath, stdout, "utf8"),
86
+ writeFile(stderrArtifactPath, stderr, "utf8")
87
+ ]);
88
+ const triageResult = extractTriageResult(task, stdout);
89
+ const reviewResult = extractReviewResult(task, stdout);
90
+ const planBreakdownExtraction = extractAndExportPlanBreakdownResult(task, stdout, this.options.planBreakdownSessionStore);
91
+ const waveAssessmentExtraction = extractAndExportWaveAssessmentResult(task, stdout, this.options.waveAssessmentSessionStore);
92
+ let taskStatus = result.exitCode === 0 ? "completed" : "failed";
93
+ let errorMessage = result.exitCode === 0 ? undefined : stderr.trim() || "Gemini CLI exited with a non-zero status.";
94
+ if (task.kind === "review_pr" && result.exitCode === 0 && !reviewResult) {
95
+ taskStatus = "failed";
96
+ errorMessage = "Agent output did not contain a valid structured review result.";
97
+ }
98
+ if ((task.kind === "plan_breakdown" || task.kind === "wave_plan_replanning") && result.exitCode === 0) {
99
+ if (!planBreakdownExtraction.ok) {
100
+ taskStatus = "failed";
101
+ errorMessage = planBreakdownExtraction.message;
102
+ }
103
+ }
104
+ if (task.kind === "wave_plan_assessment" && result.exitCode === 0) {
105
+ if (!waveAssessmentExtraction.ok) {
106
+ taskStatus = "failed";
107
+ errorMessage = waveAssessmentExtraction.message;
108
+ }
109
+ }
110
+ const execution = {
111
+ id: executionId,
112
+ taskId: task.id,
113
+ taskKind: task.kind,
114
+ taskStatus,
115
+ repoId: task.repoId,
116
+ repoSlug: task.repoSlug,
117
+ agentId: agentConfig.id,
118
+ agentProvider: "gemini",
119
+ ...(agentConfig.model ? { agentModel: agentConfig.model } : {}),
120
+ command: {
121
+ executable: agentConfig.command,
122
+ args: result.args,
123
+ cwd,
124
+ timeoutMs: this.timeoutMs
125
+ },
126
+ promptArtifactPath,
127
+ stdoutArtifactPath,
128
+ stderrArtifactPath,
129
+ streamArtifactPath,
130
+ promptPacketPath,
131
+ settingsArtifactPath,
132
+ startedAt,
133
+ endedAt,
134
+ exitCode: result.exitCode,
135
+ ...(errorMessage ? { errorMessage } : {}),
136
+ ...(triageResult || reviewResult || planBreakdownExtraction.ok || waveAssessmentExtraction.ok
137
+ ? {
138
+ result: {
139
+ ...(triageResult ? { triage: triageResult } : {}),
140
+ ...(reviewResult ? { review: reviewResult } : {}),
141
+ ...(planBreakdownExtraction.ok
142
+ ? {
143
+ planBreakdown: planBreakdownExtraction.planBreakdown,
144
+ planBreakdownSessionId: planBreakdownExtraction.sessionId
145
+ }
146
+ : {}),
147
+ ...(waveAssessmentExtraction.ok
148
+ ? {
149
+ waveAssessment: waveAssessmentExtraction.waveAssessment
150
+ }
151
+ : {})
152
+ }
153
+ }
154
+ : {})
155
+ };
156
+ const parsedSessionId = extractSessionIdFromGeminiStream(result.stdout);
157
+ const providerLimitSignal = detectProviderLimit("gemini", result.exitCode, result.stdout, result.stderr);
158
+ return {
159
+ taskStatus: execution.taskStatus,
160
+ execution,
161
+ ...(providerLimitSignal ? { providerLimitSignal } : {}),
162
+ ...(parsedSessionId ? { providerSession: { providerSessionId: parsedSessionId, status: "active" } } : {})
163
+ };
164
+ }
165
+ finally {
166
+ if (stdoutStreamFd !== undefined) {
167
+ const fd = stdoutStreamFd;
168
+ stdoutStreamFd = undefined;
169
+ closeSync(fd);
170
+ }
171
+ if (stderrStreamFd !== undefined) {
172
+ const fd = stderrStreamFd;
173
+ stderrStreamFd = undefined;
174
+ closeSync(fd);
175
+ }
176
+ }
177
+ }
178
+ async runWithModelFallbacks(input) {
179
+ let lastResult;
180
+ const attemptNotes = [];
181
+ let resumeSessionId = input.resumeSessionId;
182
+ const models = input.model ? [input.model] : [...GEMINI_MODEL_FALLBACKS];
183
+ for (const [index, model] of models.entries()) {
184
+ const args = buildGeminiCliArgs({
185
+ approvalMode: input.approvalMode,
186
+ model,
187
+ outputFormat: "stream-json",
188
+ resumeSessionId
189
+ });
190
+ try {
191
+ const result = await this.options.commandRunner.run(input.command, args, {
192
+ cwd: input.cwd,
193
+ env: input.env,
194
+ input: input.promptText,
195
+ timeoutMs: this.timeoutMs,
196
+ ...(input.onOutputChunk ? { onOutputChunk: input.onOutputChunk } : {})
197
+ });
198
+ return { args, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr, attemptNotes };
199
+ }
200
+ catch (error) {
201
+ const exitCode = extractExitCode(error);
202
+ const stdout = extractStdout(error);
203
+ const stderr = extractStderr(error);
204
+ lastResult = { args, exitCode, stdout, stderr };
205
+ resumeSessionId = undefined; // Do not attempt to resume on fallback models!
206
+ if (!isGeminiQuotaExhaustion(stdout, stderr) || index === models.length - 1) {
207
+ break;
208
+ }
209
+ const nextModel = models[index + 1];
210
+ attemptNotes.push(`Gemini model ${model} hit quota exhaustion; retrying with ${nextModel}.`);
211
+ this.logger?.info(JSON.stringify({
212
+ component: "gemini-launch",
213
+ event: "quota-fallback",
214
+ exhaustedModel: model,
215
+ nextModel
216
+ }));
217
+ }
218
+ }
219
+ if (!lastResult) {
220
+ return {
221
+ args: buildGeminiCliArgs({
222
+ approvalMode: input.approvalMode,
223
+ model: GEMINI_MODEL_FALLBACKS[0],
224
+ outputFormat: "stream-json"
225
+ }),
226
+ exitCode: 1,
227
+ stdout: "",
228
+ stderr: "Gemini CLI did not return an execution result.",
229
+ attemptNotes
230
+ };
231
+ }
232
+ return {
233
+ ...lastResult,
234
+ attemptNotes
235
+ };
236
+ }
237
+ }
238
+ function extractGeminiStreamJsonAssistantText(streamText) {
239
+ if (!streamText.trim()) {
240
+ return undefined;
241
+ }
242
+ let assistantText = "";
243
+ let sawAssistantMessage = false;
244
+ let resultText;
245
+ for (const line of streamText.split(/\r?\n/)) {
246
+ const trimmed = line.trim();
247
+ if (!trimmed) {
248
+ continue;
249
+ }
250
+ let event;
251
+ try {
252
+ event = JSON.parse(trimmed);
253
+ }
254
+ catch {
255
+ continue;
256
+ }
257
+ if (!isRecord(event)) {
258
+ continue;
259
+ }
260
+ if (event.type === "message" && getStreamRole(event) === "assistant") {
261
+ const messageText = extractStreamText(event);
262
+ if (messageText) {
263
+ assistantText += messageText;
264
+ sawAssistantMessage = true;
265
+ }
266
+ continue;
267
+ }
268
+ if (resultText === undefined && event.type === "result") {
269
+ const candidate = extractStreamText(event);
270
+ if (candidate) {
271
+ resultText = candidate;
272
+ }
273
+ }
274
+ }
275
+ if (sawAssistantMessage && assistantText.trim()) {
276
+ return assistantText;
277
+ }
278
+ return resultText;
279
+ }
280
+ function extractStreamText(value) {
281
+ if (typeof value === "string") {
282
+ return value;
283
+ }
284
+ if (Array.isArray(value)) {
285
+ return value.map((item) => extractStreamText(item) ?? "").join("");
286
+ }
287
+ if (!isRecord(value)) {
288
+ return undefined;
289
+ }
290
+ const keys = ["content", "text", "delta", "response", "output", "message", "result"];
291
+ const parts = [];
292
+ for (const key of keys) {
293
+ const candidate = extractStreamText(value[key]);
294
+ if (candidate) {
295
+ parts.push(candidate);
296
+ }
297
+ }
298
+ return parts.length > 0 ? parts.join("") : undefined;
299
+ }
300
+ function getStreamRole(value) {
301
+ if (typeof value.role === "string") {
302
+ return value.role;
303
+ }
304
+ if (isRecord(value.message) && typeof value.message.role === "string") {
305
+ return value.message.role;
306
+ }
307
+ if (isRecord(value.metadata) && typeof value.metadata.role === "string") {
308
+ return value.metadata.role;
309
+ }
310
+ return undefined;
311
+ }
312
+ function isGeminiQuotaExhaustion(stdout, stderr) {
313
+ const combined = `${stdout}\n${stderr}`;
314
+ if (GEMINI_RETRYABLE_EXHAUSTION_PATTERN.test(combined)) {
315
+ return true;
316
+ }
317
+ const jsonCandidates = [parseJsonObject(stdout), parseJsonObject(stderr)];
318
+ for (const json of jsonCandidates) {
319
+ if (json && hasRetryableValue(json)) {
320
+ return true;
321
+ }
322
+ }
323
+ return false;
324
+ }
325
+ function hasRetryableValue(obj) {
326
+ if (typeof obj === "string") {
327
+ return GEMINI_RETRYABLE_EXHAUSTION_PATTERN.test(obj);
328
+ }
329
+ if (Array.isArray(obj)) {
330
+ return obj.some((val) => hasRetryableValue(val));
331
+ }
332
+ if (obj !== null && typeof obj === "object") {
333
+ return Object.values(obj).some((val) => hasRetryableValue(val));
334
+ }
335
+ return false;
336
+ }
337
+ function parseJsonObject(text) {
338
+ if (!text.trim()) {
339
+ return undefined;
340
+ }
341
+ try {
342
+ const parsed = JSON.parse(text);
343
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
344
+ return parsed;
345
+ }
346
+ }
347
+ catch {
348
+ // Attempt substring extraction if full parse failed
349
+ }
350
+ const start = text.indexOf("{");
351
+ const end = text.lastIndexOf("}");
352
+ if (start !== -1 && end !== -1 && end > start) {
353
+ try {
354
+ const substring = text.substring(start, end + 1);
355
+ const parsed = JSON.parse(substring);
356
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
357
+ return parsed;
358
+ }
359
+ }
360
+ catch {
361
+ // Ignore parse errors for candidates
362
+ }
363
+ }
364
+ return undefined;
365
+ }
366
+ function withAttemptNotes(stdout, attemptNotes) {
367
+ if (attemptNotes.length === 0) {
368
+ return stdout;
369
+ }
370
+ return `${attemptNotes.map((note) => `[orchestrator] ${note}`).join("\n")}\n${stdout}`;
371
+ }
372
+ function extractSessionIdFromGeminiStream(streamText) {
373
+ if (!streamText.trim()) {
374
+ return undefined;
375
+ }
376
+ for (const line of streamText.split(/\r?\n/)) {
377
+ const trimmed = line.trim();
378
+ if (!trimmed) {
379
+ continue;
380
+ }
381
+ try {
382
+ const event = JSON.parse(trimmed);
383
+ if (event && typeof event === "object" && event.type === "init" && typeof event.session_id === "string") {
384
+ return event.session_id;
385
+ }
386
+ }
387
+ catch {
388
+ continue;
389
+ }
390
+ }
391
+ return undefined;
392
+ }
@@ -0,0 +1,180 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { workflowRegistry } from "../workflows/registry.js";
3
+ import { roleForTask, selectAgentForTask } from "./selection.js";
4
+ export function getJobKey(role, modelClass) {
5
+ return `${role}-${modelClass}`;
6
+ }
7
+ export function getJobAgentAssignmentId(input) {
8
+ const base = `job-assignment:${input.repoKey.replaceAll("/", "~")}:${input.workstreamId}:${input.jobKey}`;
9
+ return input.uniqueSuffix ? `${base}:${input.uniqueSuffix}` : base;
10
+ }
11
+ export function resolveJobAgentAssignment(input) {
12
+ const providerLimits = input.providerLimits ?? [];
13
+ const definition = workflowRegistry.getDefinition(input.task.kind);
14
+ const role = roleForTask(input.task);
15
+ const modelClass = definition.modelClass;
16
+ const jobKey = getJobKey(role, modelClass);
17
+ if (!input.task.workstreamId) {
18
+ const configuredAgent = selectAgentForTask(input.task, input.agents, providerLimits, {
19
+ ...(input.replacementForProvider ? { replacementForProvider: input.replacementForProvider } : {}),
20
+ ...(input.providerBrokerToolAllowLists ? { providerBrokerToolAllowLists: input.providerBrokerToolAllowLists } : {}),
21
+ ...(input.capabilitiesByProvider ? { capabilitiesByProvider: input.capabilitiesByProvider } : {})
22
+ });
23
+ return {
24
+ mode: "unscoped_fallback",
25
+ configuredAgent,
26
+ initialized: false,
27
+ assignments: input.existingAssignments
28
+ };
29
+ }
30
+ const existingAssignment = input.existingAssignments.find((a) => a.repoKey === input.task.repoSlug &&
31
+ a.workstreamId === input.task.workstreamId &&
32
+ a.role === role &&
33
+ a.modelClass === modelClass &&
34
+ a.jobKey === jobKey &&
35
+ a.status === "active");
36
+ if (existingAssignment) {
37
+ const agent = input.agents.find((a) => a.id === existingAssignment.configuredAgentId);
38
+ let staleReason = getPinnedAssignmentStaleReason(existingAssignment, agent, role, modelClass);
39
+ if (!staleReason && agent) {
40
+ const providerLimit = providerLimits.find((limit) => limit.provider === agent.provider && limit.status === "limited");
41
+ if (providerLimit) {
42
+ staleReason = "Pinned provider limit exceeded";
43
+ }
44
+ }
45
+ if (staleReason) {
46
+ const staleAssignments = input.existingAssignments.map((assignment) => assignment.id === existingAssignment.id
47
+ ? {
48
+ ...assignment,
49
+ status: "stale",
50
+ updatedAt: input.now,
51
+ notes: staleReason
52
+ }
53
+ : assignment);
54
+ return createNewAssignment({
55
+ task: input.task,
56
+ agents: input.agents,
57
+ existingAssignments: staleAssignments,
58
+ providerLimits,
59
+ replacementForProvider: existingAssignment.provider,
60
+ ...(input.providerBrokerToolAllowLists ? { providerBrokerToolAllowLists: input.providerBrokerToolAllowLists } : {}),
61
+ ...(input.capabilitiesByProvider ? { capabilitiesByProvider: input.capabilitiesByProvider } : {}),
62
+ now: input.now,
63
+ role,
64
+ modelClass,
65
+ jobKey
66
+ });
67
+ }
68
+ return {
69
+ mode: "pinned",
70
+ configuredAgent: agent,
71
+ assignment: existingAssignment,
72
+ initialized: false,
73
+ assignments: input.existingAssignments
74
+ };
75
+ }
76
+ return createNewAssignment({
77
+ task: input.task,
78
+ agents: input.agents,
79
+ existingAssignments: input.existingAssignments,
80
+ providerLimits,
81
+ ...(input.replacementForProvider ? { replacementForProvider: input.replacementForProvider } : {}),
82
+ ...(input.providerBrokerToolAllowLists ? { providerBrokerToolAllowLists: input.providerBrokerToolAllowLists } : {}),
83
+ ...(input.capabilitiesByProvider ? { capabilitiesByProvider: input.capabilitiesByProvider } : {}),
84
+ now: input.now,
85
+ role,
86
+ modelClass,
87
+ jobKey
88
+ });
89
+ }
90
+ function createNewAssignment(input) {
91
+ const configuredAgent = selectAgentForTask(input.task, input.agents, input.providerLimits, {
92
+ ...(input.replacementForProvider ? { replacementForProvider: input.replacementForProvider } : {}),
93
+ ...(input.providerBrokerToolAllowLists ? { providerBrokerToolAllowLists: input.providerBrokerToolAllowLists } : {}),
94
+ ...(input.capabilitiesByProvider ? { capabilitiesByProvider: input.capabilitiesByProvider } : {})
95
+ });
96
+ if (!input.task.workstreamId) {
97
+ throw new Error(`Task ${input.task.id} has no workstreamId; scoped job assignment cannot be created.`);
98
+ }
99
+ const newAssignment = {
100
+ id: getJobAgentAssignmentId({
101
+ repoKey: input.task.repoSlug,
102
+ workstreamId: input.task.workstreamId,
103
+ jobKey: input.jobKey,
104
+ uniqueSuffix: randomUUID().slice(0, 8)
105
+ }),
106
+ repoKey: input.task.repoSlug,
107
+ workstreamId: input.task.workstreamId,
108
+ role: input.role,
109
+ modelClass: input.modelClass,
110
+ jobKey: input.jobKey,
111
+ configuredAgentId: configuredAgent.id,
112
+ provider: configuredAgent.provider,
113
+ status: "active",
114
+ createdAt: input.now,
115
+ updatedAt: input.now
116
+ };
117
+ return {
118
+ mode: "pinned",
119
+ configuredAgent,
120
+ assignment: newAssignment,
121
+ initialized: true,
122
+ assignments: [...input.existingAssignments, newAssignment].sort(compareJobAgentAssignments)
123
+ };
124
+ }
125
+ function getPinnedAssignmentStaleReason(assignment, agent, role, modelClass) {
126
+ if (!agent) {
127
+ return `Pinned agent "${assignment.configuredAgentId}" no longer exists in agents.json.`;
128
+ }
129
+ if (!agent.enabled) {
130
+ return `Pinned agent "${agent.id}" is disabled in agents.json.`;
131
+ }
132
+ if (agent.provider !== assignment.provider) {
133
+ return `Pinned agent "${agent.id}" provider changed from "${assignment.provider}" to "${agent.provider}".`;
134
+ }
135
+ if (agent.modelClass !== modelClass) {
136
+ return `Pinned agent "${agent.id}" modelClass changed from "${modelClass}" to "${agent.modelClass}".`;
137
+ }
138
+ if (!agent.roles.includes(role)) {
139
+ return `Pinned agent "${agent.id}" no longer has required role "${role}".`;
140
+ }
141
+ return undefined;
142
+ }
143
+ export function recordJobAgentAssignmentExecution(input) {
144
+ const index = input.assignments.findIndex((a) => a.id === input.assignmentId);
145
+ if (index === -1) {
146
+ throw new Error(`Assignment "${input.assignmentId}" not found for execution update`);
147
+ }
148
+ const existing = input.assignments[index];
149
+ const updated = {
150
+ ...existing,
151
+ lastTaskId: input.taskId,
152
+ lastExecutionId: input.executionId,
153
+ lastStartedAt: input.startedAt,
154
+ updatedAt: input.endedAt
155
+ };
156
+ if (input.taskStatus === "completed") {
157
+ updated.lastCompletedAt = input.endedAt;
158
+ }
159
+ else if (input.taskStatus === "failed") {
160
+ updated.lastFailedAt = input.endedAt;
161
+ }
162
+ const nextAssignments = [...input.assignments];
163
+ nextAssignments[index] = updated;
164
+ return nextAssignments.sort(compareJobAgentAssignments);
165
+ }
166
+ function compareJobAgentAssignments(left, right) {
167
+ const repoComparison = left.repoKey.localeCompare(right.repoKey);
168
+ if (repoComparison !== 0) {
169
+ return repoComparison;
170
+ }
171
+ const workstreamComparison = left.workstreamId.localeCompare(right.workstreamId);
172
+ if (workstreamComparison !== 0) {
173
+ return workstreamComparison;
174
+ }
175
+ const jobKeyComparison = left.jobKey.localeCompare(right.jobKey);
176
+ if (jobKeyComparison !== 0) {
177
+ return jobKeyComparison;
178
+ }
179
+ return left.id.localeCompare(right.id);
180
+ }