@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.
- package/LICENSE +21 -0
- package/README.md +329 -0
- package/dist/src/agents/antigravity-launch.js +162 -0
- package/dist/src/agents/antigravity.js +251 -0
- package/dist/src/agents/claude-launch.js +117 -0
- package/dist/src/agents/claude.js +271 -0
- package/dist/src/agents/codex-launch.js +82 -0
- package/dist/src/agents/codex.js +261 -0
- package/dist/src/agents/composite-runner.js +13 -0
- package/dist/src/agents/extraction.js +442 -0
- package/dist/src/agents/failure-feedback.js +132 -0
- package/dist/src/agents/gemini-launch.js +96 -0
- package/dist/src/agents/gemini.js +392 -0
- package/dist/src/agents/job-agent-assignments.js +180 -0
- package/dist/src/agents/lifecycle-feedback.js +147 -0
- package/dist/src/agents/limit-detector.js +83 -0
- package/dist/src/agents/opencode-launch.js +138 -0
- package/dist/src/agents/opencode.js +156 -0
- package/dist/src/agents/plan-breakdown-feedback.js +97 -0
- package/dist/src/agents/pool.js +168 -0
- package/dist/src/agents/prompt.js +462 -0
- package/dist/src/agents/provider-sessions.js +105 -0
- package/dist/src/agents/review-feedback.js +255 -0
- package/dist/src/agents/selection.js +113 -0
- package/dist/src/agents/service.js +1087 -0
- package/dist/src/agents/types.js +1 -0
- package/dist/src/agents/wave-assessment-feedback.js +69 -0
- package/dist/src/attention/derive.js +337 -0
- package/dist/src/capabilities/index.js +2 -0
- package/dist/src/capabilities/projection.js +183 -0
- package/dist/src/capabilities/types.js +1 -0
- package/dist/src/capabilities/validator.js +87 -0
- package/dist/src/cli/commands/broker-smoke.js +96 -0
- package/dist/src/cli/commands/broker.js +56 -0
- package/dist/src/cli/commands/init.js +4 -0
- package/dist/src/cli/commands/journal.js +43 -0
- package/dist/src/cli/commands/run-loop.js +56 -0
- package/dist/src/cli/commands/run.js +1241 -0
- package/dist/src/cli/commands/tui.js +187 -0
- package/dist/src/cli/dispatcher.js +51 -0
- package/dist/src/cli/errors.js +2 -0
- package/dist/src/cli/init.js +92 -0
- package/dist/src/cli/options.js +86 -0
- package/dist/src/cli/output.js +231 -0
- package/dist/src/cli/run-journal.js +342 -0
- package/dist/src/cli/run-types.js +1 -0
- package/dist/src/cli/run.js +32 -0
- package/dist/src/cli/runtime/dispatch-lifecycle.js +772 -0
- package/dist/src/cli/runtime/provider-runtime-types.js +1 -0
- package/dist/src/cli/runtime/provider-runtime.js +152 -0
- package/dist/src/cli/runtime/provider-tools.js +73 -0
- package/dist/src/cli/runtime/providers/antigravity.js +26 -0
- package/dist/src/cli/runtime/providers/claude.js +20 -0
- package/dist/src/cli/runtime/providers/codex.js +20 -0
- package/dist/src/cli/runtime/providers/gemini.js +20 -0
- package/dist/src/cli/runtime/providers/opencode.js +20 -0
- package/dist/src/cli/runtime/run-context.js +131 -0
- package/dist/src/cli/tui/index.js +3 -0
- package/dist/src/cli/tui/screen.js +388 -0
- package/dist/src/cli/tui/tracker.js +558 -0
- package/dist/src/cli/tui/types.js +1 -0
- package/dist/src/cli/tui/views.js +1006 -0
- package/dist/src/config/loader.js +136 -0
- package/dist/src/config/schemas.js +403 -0
- package/dist/src/config/types.js +1 -0
- package/dist/src/context/types.js +331 -0
- package/dist/src/domain/entities.js +1 -0
- package/dist/src/git/backburner-git-tool-service.js +680 -0
- package/dist/src/git/gitops-lifecycle.js +56 -0
- package/dist/src/git/merge-prep-workspace.js +357 -0
- package/dist/src/git/types.js +484 -0
- package/dist/src/github/broker-model-content.js +215 -0
- package/dist/src/github/broker-server.js +237 -0
- package/dist/src/github/broker.js +638 -0
- package/dist/src/github/composed.js +53 -0
- package/dist/src/github/gateway.js +478 -0
- package/dist/src/github/normalize.js +222 -0
- package/dist/src/github/pr-classification.js +73 -0
- package/dist/src/github/scoped-broker.js +35 -0
- package/dist/src/github/security.js +126 -0
- package/dist/src/github/service.js +332 -0
- package/dist/src/github/types.js +1 -0
- package/dist/src/github/utils.js +16 -0
- package/dist/src/journal/index.js +4 -0
- package/dist/src/journal/reader.js +62 -0
- package/dist/src/journal/renderer.js +50 -0
- package/dist/src/journal/service.js +72 -0
- package/dist/src/journal/types.js +1 -0
- package/dist/src/journal/writer.js +19 -0
- package/dist/src/mcps/bridge.js +166 -0
- package/dist/src/mcps/index.js +5 -0
- package/dist/src/mcps/reconcile.js +113 -0
- package/dist/src/mcps/smithery-client.js +186 -0
- package/dist/src/mcps/smithery.js +164 -0
- package/dist/src/mcps/types.js +1 -0
- package/dist/src/memory/json-store.js +71 -0
- package/dist/src/memory/prompt.js +16 -0
- package/dist/src/memory/scope.js +31 -0
- package/dist/src/memory/store.js +1 -0
- package/dist/src/memory/tool-service.js +142 -0
- package/dist/src/memory/types.js +12 -0
- package/dist/src/onboarding/config-writer.js +96 -0
- package/dist/src/onboarding/engine.js +86 -0
- package/dist/src/onboarding/prompt.js +102 -0
- package/dist/src/onboarding/services/provider-discovery.js +58 -0
- package/dist/src/onboarding/services/repo-discovery.js +246 -0
- package/dist/src/onboarding/steps/check-github-auth.js +32 -0
- package/dist/src/onboarding/steps/check-requirements.js +34 -0
- package/dist/src/onboarding/steps/configure-models.js +105 -0
- package/dist/src/onboarding/steps/configure-paths.js +33 -0
- package/dist/src/onboarding/steps/discover-providers.js +38 -0
- package/dist/src/onboarding/steps/discover-repos.js +39 -0
- package/dist/src/onboarding/steps/generate-config.js +106 -0
- package/dist/src/onboarding/steps/readiness-validation.js +158 -0
- package/dist/src/onboarding/steps/select-repos.js +76 -0
- package/dist/src/onboarding/steps/show-summary.js +97 -0
- package/dist/src/onboarding/steps/welcome.js +27 -0
- package/dist/src/onboarding/types.js +1 -0
- package/dist/src/onboarding/validators/git-access.js +63 -0
- package/dist/src/onboarding/validators/github-auth.js +85 -0
- package/dist/src/onboarding/validators/tool-checker.js +153 -0
- package/dist/src/onboarding/validators/workspace.js +83 -0
- package/dist/src/prompts/resolver.js +106 -0
- package/dist/src/retrospectives/candidates.js +224 -0
- package/dist/src/retrospectives/derive.js +169 -0
- package/dist/src/retrospectives/ingest.js +321 -0
- package/dist/src/retrospectives/management-prs.js +380 -0
- package/dist/src/retrospectives/proposals.js +199 -0
- package/dist/src/retrospectives/scopes.js +272 -0
- package/dist/src/retrospectives/scoring.js +171 -0
- package/dist/src/retrospectives/types.js +1 -0
- package/dist/src/state/loader.js +473 -0
- package/dist/src/state/types.js +15 -0
- package/dist/src/tasks/derivation/builders.js +70 -0
- package/dist/src/tasks/derivation/handlers/comment-response.js +169 -0
- package/dist/src/tasks/derivation/handlers/implement-plan.js +103 -0
- package/dist/src/tasks/derivation/handlers/index.js +54 -0
- package/dist/src/tasks/derivation/handlers/packet-pr-control.js +92 -0
- package/dist/src/tasks/derivation/handlers/packet-worktree.js +29 -0
- package/dist/src/tasks/derivation/handlers/plan-breakdown.js +45 -0
- package/dist/src/tasks/derivation/handlers/prepare-for-merge.js +91 -0
- package/dist/src/tasks/derivation/handlers/product-discovery.js +36 -0
- package/dist/src/tasks/derivation/handlers/retry-failed-task.js +125 -0
- package/dist/src/tasks/derivation/handlers/review-pr.js +28 -0
- package/dist/src/tasks/derivation/handlers/shared.js +24 -0
- package/dist/src/tasks/derivation/handlers/sync-parent-branch.js +100 -0
- package/dist/src/tasks/derivation/handlers/wave-assessment.js +223 -0
- package/dist/src/tasks/derivation/handlers/write-plan.js +101 -0
- package/dist/src/tasks/derivation/handlers/write-product-spec.js +49 -0
- package/dist/src/tasks/derivation/runner.js +18 -0
- package/dist/src/tasks/derivation/types.js +1 -0
- package/dist/src/tasks/derive.js +810 -0
- package/dist/src/tasks/mcp-skip.js +10 -0
- package/dist/src/tasks/retry.js +10 -0
- package/dist/src/tasks/task.js +184 -0
- package/dist/src/tasks/types.js +1 -0
- package/dist/src/utils/command-runner.js +192 -0
- package/dist/src/utils/json.js +38 -0
- package/dist/src/utils/logger.js +43 -0
- package/dist/src/utils/paths.js +38 -0
- package/dist/src/utils/slug.js +10 -0
- package/dist/src/workflows/registry.js +174 -0
- package/dist/src/workflows/triage.js +90 -0
- package/dist/src/workflows/workstream-machine/derive-events.js +121 -0
- package/dist/src/workflows/workstream-machine/evaluate.js +24 -0
- package/dist/src/workflows/workstream-machine/events.js +1 -0
- package/dist/src/workflows/workstream-machine/machine.js +355 -0
- package/dist/src/workflows/workstream-machine/selectors.js +19 -0
- package/dist/src/workflows/workstream-machine/types.js +1 -0
- package/dist/src/workstreams/branch-sync.js +87 -0
- package/dist/src/workstreams/derive.js +629 -0
- package/dist/src/workstreams/mcp-blockers.js +420 -0
- package/dist/src/workstreams/packet-candidates.js +42 -0
- package/dist/src/workstreams/packet-lifecycle-feedback.js +60 -0
- package/dist/src/workstreams/packet-lifecycle-report.js +199 -0
- package/dist/src/workstreams/packet-plan.js +597 -0
- package/dist/src/workstreams/packet-projections.js +190 -0
- package/dist/src/workstreams/parent-branch-sync-action.js +312 -0
- package/dist/src/workstreams/parent-branch-sync.js +148 -0
- package/dist/src/workstreams/phases.js +4 -0
- package/dist/src/workstreams/plan-breakdown-generator/export.js +19 -0
- package/dist/src/workstreams/plan-breakdown-generator/index.js +6 -0
- package/dist/src/workstreams/plan-breakdown-generator/inspect.js +50 -0
- package/dist/src/workstreams/plan-breakdown-generator/patch.js +139 -0
- package/dist/src/workstreams/plan-breakdown-generator/session.js +19 -0
- package/dist/src/workstreams/plan-breakdown-generator/store.js +27 -0
- package/dist/src/workstreams/plan-breakdown-generator/tools.js +407 -0
- package/dist/src/workstreams/plan-breakdown-generator/types.js +6 -0
- package/dist/src/workstreams/plan-breakdown-generator/validate.js +212 -0
- package/dist/src/workstreams/plan-breakdown-schema.js +175 -0
- package/dist/src/workstreams/utils.js +15 -0
- package/dist/src/workstreams/wave-assessment/checkpoint.js +172 -0
- package/dist/src/workstreams/wave-assessment/index.js +3 -0
- package/dist/src/workstreams/wave-assessment/session.js +84 -0
- package/dist/src/workstreams/wave-assessment/store.js +51 -0
- package/dist/src/workstreams/wave-assessment/tools.js +419 -0
- package/dist/src/workstreams/wave-assessment/types.js +1 -0
- package/dist/src/workstreams/wave-assessment/validate.js +78 -0
- package/dist/src/worktrees/service.js +839 -0
- package/dist/src/worktrees/workspace-ownership.js +34 -0
- package/dist/src/worktrees/workspace-recovery.js +177 -0
- 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
|
+
}
|