terramend 0.2.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 +661 -0
- package/README.md +145 -0
- package/dist/agents/claude.d.ts +73 -0
- package/dist/agents/claudePretoolGate.d.ts +99 -0
- package/dist/agents/gateServer.d.ts +7 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/nativeFsDenies.d.ts +28 -0
- package/dist/agents/opencode.d.ts +231 -0
- package/dist/agents/opencodePlugin.d.ts +85 -0
- package/dist/agents/opencodeShared.d.ts +40 -0
- package/dist/agents/postRun.d.ts +132 -0
- package/dist/agents/reviewer.d.ts +38 -0
- package/dist/agents/sessionLabeler.d.ts +97 -0
- package/dist/agents/shared.d.ts +189 -0
- package/dist/agents/subagentModels.d.ts +19 -0
- package/dist/agents/subagentToolGates.d.ts +55 -0
- package/dist/cli.mjs +197426 -0
- package/dist/external.d.ts +227 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +196783 -0
- package/dist/internal/index.d.ts +18 -0
- package/dist/internal.js +1714 -0
- package/dist/lifecycle.d.ts +2 -0
- package/dist/main.d.ts +8 -0
- package/dist/mcp/arkConfig.d.ts +1 -0
- package/dist/mcp/checkSuite.d.ts +25 -0
- package/dist/mcp/checkout.d.ts +77 -0
- package/dist/mcp/comment.d.ts +119 -0
- package/dist/mcp/commitInfo.d.ts +9 -0
- package/dist/mcp/crosswalk.d.ts +105 -0
- package/dist/mcp/dependencies.d.ts +8 -0
- package/dist/mcp/geminiSanitizer.d.ts +28 -0
- package/dist/mcp/git.d.ts +46 -0
- package/dist/mcp/guardrails.d.ts +104 -0
- package/dist/mcp/issue.d.ts +18 -0
- package/dist/mcp/issueComments.d.ts +9 -0
- package/dist/mcp/issueEvents.d.ts +9 -0
- package/dist/mcp/issueInfo.d.ts +9 -0
- package/dist/mcp/labels.d.ts +12 -0
- package/dist/mcp/localContext.d.ts +19 -0
- package/dist/mcp/moduleExtraction.d.ts +71 -0
- package/dist/mcp/moduleTests.d.ts +104 -0
- package/dist/mcp/modules.d.ts +179 -0
- package/dist/mcp/output.d.ts +12 -0
- package/dist/mcp/pathSafety.d.ts +14 -0
- package/dist/mcp/policy.d.ts +48 -0
- package/dist/mcp/pr.d.ts +49 -0
- package/dist/mcp/prInfo.d.ts +9 -0
- package/dist/mcp/providerSchema.d.ts +50 -0
- package/dist/mcp/review.d.ts +199 -0
- package/dist/mcp/reviewComments.d.ts +178 -0
- package/dist/mcp/roots.d.ts +58 -0
- package/dist/mcp/scope.d.ts +15 -0
- package/dist/mcp/selectMode.d.ts +18 -0
- package/dist/mcp/server.d.ts +48 -0
- package/dist/mcp/shared.d.ts +47 -0
- package/dist/mcp/shell.d.ts +37 -0
- package/dist/mcp/staleFix.d.ts +51 -0
- package/dist/mcp/terraform/cost.d.ts +55 -0
- package/dist/mcp/terraform/currency.d.ts +94 -0
- package/dist/mcp/terraform/decisions.d.ts +178 -0
- package/dist/mcp/terraform/findings.d.ts +75 -0
- package/dist/mcp/terraform/plan.d.ts +157 -0
- package/dist/mcp/terraform/scanners.d.ts +131 -0
- package/dist/mcp/terraform/tools.d.ts +63 -0
- package/dist/mcp/terraform/types.d.ts +172 -0
- package/dist/mcp/terraform.d.ts +22 -0
- package/dist/mcp/terratest.d.ts +83 -0
- package/dist/mcp/upload.d.ts +6 -0
- package/dist/models.d.ts +171 -0
- package/dist/modes.d.ts +26 -0
- package/dist/prep/index.d.ts +7 -0
- package/dist/prep/installNodeDependencies.d.ts +2 -0
- package/dist/prep/installPythonDependencies.d.ts +2 -0
- package/dist/prep/types.d.ts +31 -0
- package/dist/reviewQuality.d.ts +64 -0
- package/dist/skills/terraform-best-practices/SKILL.md +369 -0
- package/dist/toolState.d.ts +135 -0
- package/dist/utils/activity.d.ts +40 -0
- package/dist/utils/agent.d.ts +20 -0
- package/dist/utils/agentHangReport.d.ts +38 -0
- package/dist/utils/apiFetch.d.ts +19 -0
- package/dist/utils/apiKeys.d.ts +41 -0
- package/dist/utils/apiUrl.d.ts +20 -0
- package/dist/utils/assets.d.ts +8 -0
- package/dist/utils/billingErrors.d.ts +85 -0
- package/dist/utils/body.d.ts +34 -0
- package/dist/utils/buildTerramendFooter.d.ts +25 -0
- package/dist/utils/byokFallback.d.ts +85 -0
- package/dist/utils/claudeSubscription.d.ts +30 -0
- package/dist/utils/cli.d.ts +10 -0
- package/dist/utils/codexHome.d.ts +29 -0
- package/dist/utils/codexOAuth.d.ts +60 -0
- package/dist/utils/diffCoverage.d.ts +63 -0
- package/dist/utils/errorReport.d.ts +17 -0
- package/dist/utils/exitHandler.d.ts +8 -0
- package/dist/utils/fixDoubleEscapedString.d.ts +1 -0
- package/dist/utils/gitAuth.d.ts +84 -0
- package/dist/utils/gitAuthServer.d.ts +24 -0
- package/dist/utils/github.d.ts +78 -0
- package/dist/utils/globals.d.ts +3 -0
- package/dist/utils/install.d.ts +60 -0
- package/dist/utils/instructions.d.ts +48 -0
- package/dist/utils/leapingComment.d.ts +11 -0
- package/dist/utils/learnings.d.ts +62 -0
- package/dist/utils/learningsTruncate.d.ts +25 -0
- package/dist/utils/lifecycle.d.ts +57 -0
- package/dist/utils/log.d.ts +111 -0
- package/dist/utils/normalizeEnv.d.ts +30 -0
- package/dist/utils/openCodeModels.d.ts +11 -0
- package/dist/utils/overrides.d.ts +40 -0
- package/dist/utils/packageManager.d.ts +49 -0
- package/dist/utils/patchWorkflowRunFields.d.ts +29 -0
- package/dist/utils/payload.d.ts +105 -0
- package/dist/utils/prSummary.d.ts +61 -0
- package/dist/utils/progressComment.d.ts +146 -0
- package/dist/utils/providerErrors.d.ts +31 -0
- package/dist/utils/rangeDiff.d.ts +51 -0
- package/dist/utils/remediationCommand.d.ts +55 -0
- package/dist/utils/retry.d.ts +13 -0
- package/dist/utils/reviewCleanup.d.ts +14 -0
- package/dist/utils/run.d.ts +9 -0
- package/dist/utils/runContext.d.ts +60 -0
- package/dist/utils/runContextData.d.ts +23 -0
- package/dist/utils/runErrorRenderer.d.ts +64 -0
- package/dist/utils/runLifecycle.d.ts +86 -0
- package/dist/utils/runStartupLog.d.ts +15 -0
- package/dist/utils/secrets.d.ts +22 -0
- package/dist/utils/setup.d.ts +90 -0
- package/dist/utils/shell.d.ts +32 -0
- package/dist/utils/skills.d.ts +10 -0
- package/dist/utils/subprocess.d.ts +80 -0
- package/dist/utils/terraformMcp.d.ts +42 -0
- package/dist/utils/time.d.ts +15 -0
- package/dist/utils/timer.d.ts +23 -0
- package/dist/utils/todoTracking.d.ts +16 -0
- package/dist/utils/token.d.ts +39 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/versioning.d.ts +7 -0
- package/dist/utils/vertex.d.ts +16 -0
- package/dist/utils/workflow.d.ts +13 -0
- package/package.json +119 -0
- package/src/agents/claude.test.ts +1016 -0
- package/src/agents/claude.ts +1246 -0
- package/src/agents/claudePretoolGate.test.ts +28 -0
- package/src/agents/claudePretoolGate.ts +173 -0
- package/src/agents/gateServer.test.ts +204 -0
- package/src/agents/gateServer.ts +124 -0
- package/src/agents/index.ts +10 -0
- package/src/agents/nativeFsDenies.ts +82 -0
- package/src/agents/opencode.test.ts +1440 -0
- package/src/agents/opencode.ts +1312 -0
- package/src/agents/opencodePlugin.ts +222 -0
- package/src/agents/opencodeShared.test.ts +34 -0
- package/src/agents/opencodeShared.ts +121 -0
- package/src/agents/postRun.test.ts +549 -0
- package/src/agents/postRun.ts +535 -0
- package/src/agents/reviewer.ts +104 -0
- package/src/agents/sessionLabeler.test.ts +247 -0
- package/src/agents/sessionLabeler.ts +178 -0
- package/src/agents/shared.test.ts +76 -0
- package/src/agents/shared.ts +292 -0
- package/src/agents/subagentModels.test.ts +113 -0
- package/src/agents/subagentModels.ts +40 -0
- package/src/agents/subagentRegistration.test.ts +41 -0
- package/src/agents/subagentToolGates.ts +114 -0
- package/src/cli.test.ts +129 -0
- package/src/cli.ts +105 -0
- package/src/commands/gha.test.ts +192 -0
- package/src/commands/gha.ts +188 -0
- package/src/commands/mcp.ts +122 -0
- package/src/config.ts +1 -0
- package/src/entry.ts +7 -0
- package/src/entryPost.stdlibOnly.test.ts +109 -0
- package/src/entryPost.ts +99 -0
- package/src/external.test.ts +16 -0
- package/src/external.ts +302 -0
- package/src/index.ts +11 -0
- package/src/internal/index.ts +71 -0
- package/src/lifecycle.ts +2 -0
- package/src/main.test.ts +873 -0
- package/src/main.ts +712 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-49-review-3485940013.json +110 -0
- package/src/mcp/__fixtures__/terramend-scratch-pr-64-review-3531000326.json +14 -0
- package/src/mcp/__fixtures__/terramend-test-repo-pr-1.diff.json +67 -0
- package/src/mcp/__snapshots__/checkout.test.ts.snap +109 -0
- package/src/mcp/__snapshots__/reviewComments.test.ts.snap +71 -0
- package/src/mcp/arkConfig.ts +7 -0
- package/src/mcp/checkSuite.test.ts +245 -0
- package/src/mcp/checkSuite.ts +255 -0
- package/src/mcp/checkout.test.ts +752 -0
- package/src/mcp/checkout.ts +886 -0
- package/src/mcp/comment.test.ts +772 -0
- package/src/mcp/comment.ts +582 -0
- package/src/mcp/commitInfo.test.ts +127 -0
- package/src/mcp/commitInfo.ts +61 -0
- package/src/mcp/crosswalk.test.ts +106 -0
- package/src/mcp/crosswalk.ts +339 -0
- package/src/mcp/dependencies.test.ts +309 -0
- package/src/mcp/dependencies.ts +189 -0
- package/src/mcp/geminiSanitizer.test.ts +287 -0
- package/src/mcp/geminiSanitizer.ts +207 -0
- package/src/mcp/git.test.ts +1083 -0
- package/src/mcp/git.ts +890 -0
- package/src/mcp/guardrails.test.ts +705 -0
- package/src/mcp/guardrails.ts +465 -0
- package/src/mcp/issue.test.ts +113 -0
- package/src/mcp/issue.ts +73 -0
- package/src/mcp/issueComments.test.ts +69 -0
- package/src/mcp/issueComments.ts +48 -0
- package/src/mcp/issueEvents.test.ts +134 -0
- package/src/mcp/issueEvents.ts +100 -0
- package/src/mcp/issueInfo.test.ts +104 -0
- package/src/mcp/issueInfo.ts +72 -0
- package/src/mcp/labels.test.ts +52 -0
- package/src/mcp/labels.ts +34 -0
- package/src/mcp/localContext.ts +28 -0
- package/src/mcp/localServer.test.ts +75 -0
- package/src/mcp/localServer.ts +131 -0
- package/src/mcp/moduleExtraction.test.ts +261 -0
- package/src/mcp/moduleExtraction.ts +313 -0
- package/src/mcp/moduleTests.test.ts +269 -0
- package/src/mcp/moduleTests.ts +421 -0
- package/src/mcp/modules.test.ts +640 -0
- package/src/mcp/modules.ts +696 -0
- package/src/mcp/output.test.ts +96 -0
- package/src/mcp/output.ts +70 -0
- package/src/mcp/pathSafety.test.ts +44 -0
- package/src/mcp/pathSafety.ts +28 -0
- package/src/mcp/policy.test.ts +282 -0
- package/src/mcp/policy.ts +199 -0
- package/src/mcp/pr.test.ts +387 -0
- package/src/mcp/pr.ts +194 -0
- package/src/mcp/prInfo.test.ts +96 -0
- package/src/mcp/prInfo.ts +91 -0
- package/src/mcp/providerSchema.test.ts +85 -0
- package/src/mcp/providerSchema.ts +175 -0
- package/src/mcp/review.test.ts +936 -0
- package/src/mcp/review.ts +923 -0
- package/src/mcp/reviewComments.test.ts +549 -0
- package/src/mcp/reviewComments.ts +896 -0
- package/src/mcp/roots.test.ts +175 -0
- package/src/mcp/roots.ts +217 -0
- package/src/mcp/scope.test.ts +59 -0
- package/src/mcp/scope.ts +65 -0
- package/src/mcp/security.test.ts +720 -0
- package/src/mcp/selectMode.test.ts +210 -0
- package/src/mcp/selectMode.ts +181 -0
- package/src/mcp/server.test.ts +292 -0
- package/src/mcp/server.ts +403 -0
- package/src/mcp/shared.ts +100 -0
- package/src/mcp/shell.test.ts +520 -0
- package/src/mcp/shell.ts +505 -0
- package/src/mcp/staleFix.test.ts +237 -0
- package/src/mcp/staleFix.ts +277 -0
- package/src/mcp/terraform/cost.ts +163 -0
- package/src/mcp/terraform/currency.test.ts +338 -0
- package/src/mcp/terraform/currency.ts +336 -0
- package/src/mcp/terraform/decisions.ts +527 -0
- package/src/mcp/terraform/findings.ts +333 -0
- package/src/mcp/terraform/plan.ts +348 -0
- package/src/mcp/terraform/scanners.ts +809 -0
- package/src/mcp/terraform/tools.test.ts +1071 -0
- package/src/mcp/terraform/tools.ts +908 -0
- package/src/mcp/terraform/types.ts +305 -0
- package/src/mcp/terraform.test.ts +1957 -0
- package/src/mcp/terraform.ts +23 -0
- package/src/mcp/terratest.test.ts +105 -0
- package/src/mcp/terratest.ts +196 -0
- package/src/mcp/toolFiltering.test.ts +85 -0
- package/src/mcp/upload.test.ts +180 -0
- package/src/mcp/upload.ts +112 -0
- package/src/models.test.ts +300 -0
- package/src/models.ts +708 -0
- package/src/modes.test.ts +107 -0
- package/src/modes.ts +880 -0
- package/src/prep/index.ts +43 -0
- package/src/prep/installNodeDependencies.test.ts +298 -0
- package/src/prep/installNodeDependencies.ts +196 -0
- package/src/prep/installPythonDependencies.test.ts +268 -0
- package/src/prep/installPythonDependencies.ts +199 -0
- package/src/prep/types.ts +38 -0
- package/src/reviewQuality.test.ts +63 -0
- package/src/reviewQuality.ts +134 -0
- package/src/runCli.test.ts +214 -0
- package/src/runCli.ts +282 -0
- package/src/skills/terraform-best-practices/SKILL.md +369 -0
- package/src/toolState.test.ts +45 -0
- package/src/toolState.ts +252 -0
- package/src/utils/activity.test.ts +188 -0
- package/src/utils/activity.ts +210 -0
- package/src/utils/agent.test.ts +251 -0
- package/src/utils/agent.ts +139 -0
- package/src/utils/agentHangReport.test.ts +203 -0
- package/src/utils/agentHangReport.ts +170 -0
- package/src/utils/apiFetch.test.ts +115 -0
- package/src/utils/apiFetch.ts +62 -0
- package/src/utils/apiKeys.test.ts +344 -0
- package/src/utils/apiKeys.ts +206 -0
- package/src/utils/apiUrl.test.ts +30 -0
- package/src/utils/apiUrl.ts +59 -0
- package/src/utils/assets.test.ts +153 -0
- package/src/utils/assets.ts +107 -0
- package/src/utils/billingErrors.test.ts +121 -0
- package/src/utils/billingErrors.ts +189 -0
- package/src/utils/body.test.ts +217 -0
- package/src/utils/body.ts +168 -0
- package/src/utils/buildTerramendFooter.test.ts +38 -0
- package/src/utils/buildTerramendFooter.ts +82 -0
- package/src/utils/byokFallback.test.ts +205 -0
- package/src/utils/byokFallback.ts +128 -0
- package/src/utils/claudeSubscription.test.ts +179 -0
- package/src/utils/claudeSubscription.ts +93 -0
- package/src/utils/cli.ts +31 -0
- package/src/utils/codexHome.test.ts +190 -0
- package/src/utils/codexHome.ts +191 -0
- package/src/utils/codexOAuth.ts +147 -0
- package/src/utils/codexRefreshDetect.test.ts +85 -0
- package/src/utils/codexRefreshDetect.ts +35 -0
- package/src/utils/diffCoverage.test.ts +468 -0
- package/src/utils/diffCoverage.ts +404 -0
- package/src/utils/errorReport.test.ts +135 -0
- package/src/utils/errorReport.ts +83 -0
- package/src/utils/exitHandler.ts +35 -0
- package/src/utils/fixDoubleEscapedString.ts +9 -0
- package/src/utils/ghaCore.ts +13 -0
- package/src/utils/gitAuth.test.ts +322 -0
- package/src/utils/gitAuth.ts +263 -0
- package/src/utils/gitAuthServer.test.ts +260 -0
- package/src/utils/gitAuthServer.ts +182 -0
- package/src/utils/github.test.ts +615 -0
- package/src/utils/github.ts +538 -0
- package/src/utils/globals.ts +9 -0
- package/src/utils/humanEditCapture.test.ts +100 -0
- package/src/utils/humanEditCapture.ts +193 -0
- package/src/utils/install.test.ts +768 -0
- package/src/utils/install.ts +492 -0
- package/src/utils/instructions.test.ts +240 -0
- package/src/utils/instructions.ts +543 -0
- package/src/utils/leapingComment.test.ts +51 -0
- package/src/utils/leapingComment.ts +18 -0
- package/src/utils/learnings.test.ts +87 -0
- package/src/utils/learnings.ts +138 -0
- package/src/utils/learningsTocRender.test.ts +116 -0
- package/src/utils/learningsTruncate.test.ts +39 -0
- package/src/utils/learningsTruncate.ts +42 -0
- package/src/utils/lifecycle.test.ts +195 -0
- package/src/utils/lifecycle.ts +198 -0
- package/src/utils/log.test.ts +402 -0
- package/src/utils/log.ts +432 -0
- package/src/utils/normalizeEnv.test.ts +91 -0
- package/src/utils/normalizeEnv.ts +106 -0
- package/src/utils/openCodeModels.ts +82 -0
- package/src/utils/overrides.test.ts +89 -0
- package/src/utils/overrides.ts +98 -0
- package/src/utils/packageManager.test.ts +321 -0
- package/src/utils/packageManager.ts +257 -0
- package/src/utils/patchWorkflowRunFields.test.ts +92 -0
- package/src/utils/patchWorkflowRunFields.ts +150 -0
- package/src/utils/payload.test.ts +497 -0
- package/src/utils/payload.ts +371 -0
- package/src/utils/postApiFetch.ts +51 -0
- package/src/utils/prSummary.test.ts +224 -0
- package/src/utils/prSummary.ts +147 -0
- package/src/utils/progressComment.ts +261 -0
- package/src/utils/providerErrors.test.ts +315 -0
- package/src/utils/providerErrors.ts +172 -0
- package/src/utils/rangeDiff.test.ts +236 -0
- package/src/utils/rangeDiff.ts +182 -0
- package/src/utils/remediationCommand.test.ts +163 -0
- package/src/utils/remediationCommand.ts +119 -0
- package/src/utils/retry.test.ts +153 -0
- package/src/utils/retry.ts +58 -0
- package/src/utils/reviewCleanup.ts +106 -0
- package/src/utils/run.ts +99 -0
- package/src/utils/runContext.ts +145 -0
- package/src/utils/runContextData.ts +58 -0
- package/src/utils/runErrorRenderer.test.ts +95 -0
- package/src/utils/runErrorRenderer.ts +259 -0
- package/src/utils/runFixture.ts +76 -0
- package/src/utils/runLifecycle.ts +237 -0
- package/src/utils/runStartupLog.ts +60 -0
- package/src/utils/secrets.test.ts +103 -0
- package/src/utils/secrets.ts +177 -0
- package/src/utils/setup.test.ts +509 -0
- package/src/utils/setup.ts +352 -0
- package/src/utils/shell.ts +103 -0
- package/src/utils/skills.test.ts +46 -0
- package/src/utils/skills.ts +67 -0
- package/src/utils/subprocess.test.ts +170 -0
- package/src/utils/subprocess.ts +438 -0
- package/src/utils/terraformMcp.test.ts +63 -0
- package/src/utils/terraformMcp.ts +83 -0
- package/src/utils/time.test.ts +105 -0
- package/src/utils/time.ts +59 -0
- package/src/utils/timer.test.ts +91 -0
- package/src/utils/timer.ts +72 -0
- package/src/utils/todoTracking.test.ts +223 -0
- package/src/utils/todoTracking.ts +167 -0
- package/src/utils/token.test.ts +239 -0
- package/src/utils/token.ts +186 -0
- package/src/utils/version.ts +10 -0
- package/src/utils/versioning.test.ts +34 -0
- package/src/utils/versioning.ts +44 -0
- package/src/utils/vertex.ts +85 -0
- package/src/utils/workflow.ts +25 -0
package/src/main.ts
ADDED
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
// changes to tool permissions should be reflected in wiki/granular-tools.md
|
|
2
|
+
|
|
3
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { agents } from "#app/agents/index";
|
|
7
|
+
import { reportProgress } from "#app/mcp/comment";
|
|
8
|
+
import { startInstallation } from "#app/mcp/dependencies";
|
|
9
|
+
import { startMcpHttpServer, type ToolContext } from "#app/mcp/server";
|
|
10
|
+
import { computeModes } from "#app/modes";
|
|
11
|
+
import { mergeReviewModeInstructions } from "#app/reviewQuality";
|
|
12
|
+
import { initToolState } from "#app/toolState";
|
|
13
|
+
import {
|
|
14
|
+
type ActivityTimeout,
|
|
15
|
+
AGENT_ACTIVITY_TIMEOUT_MS,
|
|
16
|
+
createProcessOutputActivityTimeout,
|
|
17
|
+
DEFAULT_ACTIVITY_CHECK_INTERVAL_MS,
|
|
18
|
+
} from "#app/utils/activity";
|
|
19
|
+
import { resolveAgent, resolveModel } from "#app/utils/agent";
|
|
20
|
+
import { validateAgentApiKey } from "#app/utils/apiKeys";
|
|
21
|
+
import { isBackendConfigured } from "#app/utils/apiUrl";
|
|
22
|
+
import { resolveBody } from "#app/utils/body";
|
|
23
|
+
import {
|
|
24
|
+
buildUnavailableModelError,
|
|
25
|
+
hasProviderKeyForModel,
|
|
26
|
+
selectFallbackModelIfNeeded,
|
|
27
|
+
} from "#app/utils/byokFallback";
|
|
28
|
+
import { log } from "#app/utils/cli";
|
|
29
|
+
import { installCodexAuth, TERRAMEND_DATA_DIR } from "#app/utils/codexHome";
|
|
30
|
+
import { recordDiffReadFromToolUse } from "#app/utils/diffCoverage";
|
|
31
|
+
import { onExitSignal } from "#app/utils/exitHandler";
|
|
32
|
+
import { resolveGit, setGitAuthServer } from "#app/utils/gitAuth";
|
|
33
|
+
import { startGitAuthServer } from "#app/utils/gitAuthServer";
|
|
34
|
+
import { createOctokit, writeGitHubUsageSummaryToFile } from "#app/utils/github";
|
|
35
|
+
import { resolveInstructions } from "#app/utils/instructions";
|
|
36
|
+
import { persistLearnings, seedLearningsFile } from "#app/utils/learnings";
|
|
37
|
+
import { describeSetupFailure, executeLifecycleHook } from "#app/utils/lifecycle";
|
|
38
|
+
import { normalizeEnv } from "#app/utils/normalizeEnv";
|
|
39
|
+
import {
|
|
40
|
+
captureAuthorizedModels,
|
|
41
|
+
captureBaselineModels,
|
|
42
|
+
getAuthorizedModels,
|
|
43
|
+
} from "#app/utils/openCodeModels";
|
|
44
|
+
import { applyOverrides } from "#app/utils/overrides";
|
|
45
|
+
import {
|
|
46
|
+
ensurePackageManager,
|
|
47
|
+
packageManagerBinDir,
|
|
48
|
+
resolvePackageManagerSpec,
|
|
49
|
+
} from "#app/utils/packageManager";
|
|
50
|
+
import { aggregateUsage, patchWorkflowRunFields } from "#app/utils/patchWorkflowRunFields";
|
|
51
|
+
import { resolveOutputSchema, resolvePayload, resolvePromptInput } from "#app/utils/payload";
|
|
52
|
+
import { fetchPreviousSnapshot, persistSummary, seedSummaryFile } from "#app/utils/prSummary";
|
|
53
|
+
import { handleAgentResult } from "#app/utils/run";
|
|
54
|
+
import { resolveRunContextData } from "#app/utils/runContextData";
|
|
55
|
+
import { renderRunError } from "#app/utils/runErrorRenderer";
|
|
56
|
+
import {
|
|
57
|
+
finalizeSuccessRun,
|
|
58
|
+
persistRunArtifacts,
|
|
59
|
+
writeRunErrorOutputs,
|
|
60
|
+
} from "#app/utils/runLifecycle";
|
|
61
|
+
import { logRunStartup } from "#app/utils/runStartupLog";
|
|
62
|
+
import { setEnvAllowlist } from "#app/utils/secrets";
|
|
63
|
+
import { createTempDirectory, setupGit, wipeRunnerLeakSurface } from "#app/utils/setup";
|
|
64
|
+
import { killTrackedChildren } from "#app/utils/subprocess";
|
|
65
|
+
import { resolveTimeoutMs, TIMEOUT_DISABLED } from "#app/utils/time";
|
|
66
|
+
import { Timer } from "#app/utils/timer";
|
|
67
|
+
import { createTodoTracker } from "#app/utils/todoTracking";
|
|
68
|
+
import { getJobToken, resolveTokens } from "#app/utils/token";
|
|
69
|
+
import {
|
|
70
|
+
cleanupVertexCredentials,
|
|
71
|
+
materializeVertexCredentials,
|
|
72
|
+
type VertexCredentials,
|
|
73
|
+
} from "#app/utils/vertex";
|
|
74
|
+
import { resolveRun } from "#app/utils/workflow";
|
|
75
|
+
|
|
76
|
+
export { Inputs } from "#app/utils/payload";
|
|
77
|
+
|
|
78
|
+
export interface MainResult {
|
|
79
|
+
success: boolean;
|
|
80
|
+
output?: string | undefined;
|
|
81
|
+
error?: string | undefined;
|
|
82
|
+
result?: string | undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function main(): Promise<MainResult> {
|
|
86
|
+
// normalize env var names to uppercase (handles case-insensitive workflow files)
|
|
87
|
+
normalizeEnv();
|
|
88
|
+
|
|
89
|
+
// apply caller-supplied env overrides — JSON object forwarded as the
|
|
90
|
+
// UNSAFE_OVERRIDES env var (NOT a `with:` input). gated by `actions:write`
|
|
91
|
+
// on the repo and refuses integrity-critical names; see utils/overrides.ts
|
|
92
|
+
// for the deny-list and wiki/e2e-testing.md for usage + threat model.
|
|
93
|
+
// the `unsafe` prefix is intentional: GH echoes the env-block value in the
|
|
94
|
+
// step-header log, so the raw JSON is visible to anyone with `actions:read`.
|
|
95
|
+
const overridesRaw = process.env.UNSAFE_OVERRIDES ?? "";
|
|
96
|
+
if (overridesRaw.trim()) {
|
|
97
|
+
const result = applyOverrides({ raw: overridesRaw, env: process.env });
|
|
98
|
+
if (result.applied.length > 0) {
|
|
99
|
+
log.info(`» applied ${result.applied.length} env override(s): ${result.applied.join(", ")}`);
|
|
100
|
+
}
|
|
101
|
+
if (result.denied.length > 0) {
|
|
102
|
+
log.warning(
|
|
103
|
+
`» refused to override ${result.denied.length} protected env var(s): ${result.denied.join(", ")}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// write usage summary on SIGINT/SIGTERM so the worker can read it after sandbox.exec
|
|
109
|
+
const usageSummaryPath = process.env.TERRAMEND_USAGE_SUMMARY_PATH;
|
|
110
|
+
if (usageSummaryPath) {
|
|
111
|
+
onExitSignal(() => writeGitHubUsageSummaryToFile(usageSummaryPath));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const timer = new Timer();
|
|
115
|
+
let activityTimeout: ActivityTimeout | null = null;
|
|
116
|
+
let safetyNetTimer: NodeJS.Timeout | undefined;
|
|
117
|
+
|
|
118
|
+
// parse prompt early to extract progressComment for toolState
|
|
119
|
+
const resolvedPromptInput = resolvePromptInput();
|
|
120
|
+
|
|
121
|
+
const toolState = initToolState({
|
|
122
|
+
progressComment:
|
|
123
|
+
typeof resolvedPromptInput !== "string" ? resolvedPromptInput.progressComment : undefined,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// resolve and fingerprint git binary before any agent code runs
|
|
127
|
+
resolveGit();
|
|
128
|
+
|
|
129
|
+
// get job token for initial API calls
|
|
130
|
+
const jobToken = getJobToken();
|
|
131
|
+
const initialOctokit = createOctokit(jobToken);
|
|
132
|
+
const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
|
|
133
|
+
timer.checkpoint("runContextData");
|
|
134
|
+
|
|
135
|
+
// Called for its side effect: `createTempDirectory()` sets TERRAMEND_TEMP_DIR,
|
|
136
|
+
// which `installFromNpmTarball` reads when the opencode CLI install runs below
|
|
137
|
+
// for BYOK introspection, and which agent + mcp server setup further down also
|
|
138
|
+
// consume. The returned path isn't needed directly here.
|
|
139
|
+
createTempDirectory();
|
|
140
|
+
|
|
141
|
+
// install OpenCode + capture the BASELINE model set BEFORE Codex auth.json
|
|
142
|
+
// is in scope. this is the set of models OpenCode can route from the runner's
|
|
143
|
+
// pre-existing environment alone (workflow `env:` block + GH Actions secrets).
|
|
144
|
+
// install is fs-cached, so the duplicate call inside the opencode agent's
|
|
145
|
+
// run() is a no-op.
|
|
146
|
+
const opencodeCliPath = await agents.opencode.install();
|
|
147
|
+
captureBaselineModels(opencodeCliPath);
|
|
148
|
+
|
|
149
|
+
// materialize Codex auth.json (idempotent — opencode agent re-calls inside
|
|
150
|
+
// run() and writes the same file). this has to land BEFORE
|
|
151
|
+
// captureAuthorizedModels so OpenCode's model introspection sees the
|
|
152
|
+
// OAuth-routed openai/* models.
|
|
153
|
+
installCodexAuth();
|
|
154
|
+
|
|
155
|
+
// capture the AUTHORIZED model set after Codex auth.json is applied. this is
|
|
156
|
+
// the authoritative source for the BYOK fallback decision and the
|
|
157
|
+
// opencode-agent path of validateAgentApiKey — strictly more accurate than
|
|
158
|
+
// the static envVars/managedCredentials catalog, which can miss new auth
|
|
159
|
+
// shapes.
|
|
160
|
+
captureAuthorizedModels(opencodeCliPath);
|
|
161
|
+
|
|
162
|
+
// configure env allowlist for subprocess filtering
|
|
163
|
+
if (runContext.repoSettings.envAllowlist) {
|
|
164
|
+
setEnvAllowlist(runContext.repoSettings.envAllowlist);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// resolve payload to determine shell permission
|
|
168
|
+
const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
|
|
169
|
+
toolState.model = payload.model;
|
|
170
|
+
if (payload.event.trigger === "pull_request_synchronize") {
|
|
171
|
+
toolState.beforeSha = payload.event.before_sha;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// resolve tokens first — acquireNewToken needs OIDC env vars for token exchange
|
|
175
|
+
await using tokenRef = await resolveTokens({ push: payload.push });
|
|
176
|
+
|
|
177
|
+
// wipe the GHA runner's known credential leak surface inside $RUNNER_TEMP
|
|
178
|
+
// before the agent spawns. our installation token is already in memory
|
|
179
|
+
// (tokenRef above), and setupGit's includeIf strip handles the matching
|
|
180
|
+
// dangling references in the user's .git/config. see wipeRunnerLeakSurface
|
|
181
|
+
// for the leak inventory and threat model.
|
|
182
|
+
wipeRunnerLeakSurface();
|
|
183
|
+
|
|
184
|
+
// clear OIDC env vars in restricted mode to prevent agent from minting tokens
|
|
185
|
+
if (payload.shell !== "enabled") {
|
|
186
|
+
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
187
|
+
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// create octokit with MCP token for GitHub API calls
|
|
191
|
+
const octokit = createOctokit(tokenRef.mcpToken);
|
|
192
|
+
|
|
193
|
+
const runInfo = await resolveRun({ octokit });
|
|
194
|
+
let toolContext: ToolContext | undefined;
|
|
195
|
+
let progressCallbackDisabled = false;
|
|
196
|
+
let todoTracker: ReturnType<typeof createTodoTracker> | undefined;
|
|
197
|
+
let vertexCredentials: VertexCredentials | undefined;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
if (payload.cwd && process.cwd() !== payload.cwd) {
|
|
201
|
+
process.chdir(payload.cwd);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const tmpdir = createTempDirectory();
|
|
205
|
+
|
|
206
|
+
// resolve body - fetches body_html and converts to markdown if images present
|
|
207
|
+
// this ensures agents receive markdown with working signed image URLs
|
|
208
|
+
const originalBody = payload.event.body;
|
|
209
|
+
const resolvedBody = await resolveBody({
|
|
210
|
+
event: payload.event,
|
|
211
|
+
octokit,
|
|
212
|
+
repo: runContext.repo,
|
|
213
|
+
tmpdir,
|
|
214
|
+
githubToken: tokenRef.mcpToken,
|
|
215
|
+
});
|
|
216
|
+
if (resolvedBody !== originalBody) {
|
|
217
|
+
payload.event.body = resolvedBody;
|
|
218
|
+
// also update prompt if original body was included there
|
|
219
|
+
if (originalBody && payload.prompt.includes(originalBody)) {
|
|
220
|
+
payload.prompt = payload.prompt.replace(originalBody, resolvedBody ?? "");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await using gitAuthServer = await startGitAuthServer(tmpdir);
|
|
225
|
+
setGitAuthServer(gitAuthServer);
|
|
226
|
+
|
|
227
|
+
const initialResolvedModel = resolveModel({ slug: payload.model });
|
|
228
|
+
|
|
229
|
+
// BYOK model gate. Three outcomes (see `selectFallbackModelIfNeeded`):
|
|
230
|
+
// - use-resolved: run the configured model as-is.
|
|
231
|
+
// - fallback: NO provider key present → swap to the free OpenCode model
|
|
232
|
+
// so the run still produces value. Without this, the agent launches
|
|
233
|
+
// with no key, the LLM provider 401s, and the run dies in seconds with
|
|
234
|
+
// a synthetic "Invalid API key" — the silent-churn pattern that took
|
|
235
|
+
// out 15 accounts before this landed.
|
|
236
|
+
// - unavailable: a provider key IS present but the configured model is
|
|
237
|
+
// not one the key can route. Fail loudly with the authorized list
|
|
238
|
+
// rather than silently downgrading to free (which hides a wrong model
|
|
239
|
+
// id — see PR #2, where a Google key was set but the slug was invalid
|
|
240
|
+
// and the run silently used big-pickle).
|
|
241
|
+
const authorized = getAuthorizedModels();
|
|
242
|
+
// the gate needs the agent to spare claude-harness runs (own auth,
|
|
243
|
+
// invisible to `opencode models`) from a spurious downgrade/failure.
|
|
244
|
+
const decision = selectFallbackModelIfNeeded({
|
|
245
|
+
resolvedModel: initialResolvedModel,
|
|
246
|
+
authorized,
|
|
247
|
+
providerKeyPresent: initialResolvedModel
|
|
248
|
+
? hasProviderKeyForModel(initialResolvedModel)
|
|
249
|
+
: false,
|
|
250
|
+
agentName: resolveAgent({ model: initialResolvedModel }).name,
|
|
251
|
+
});
|
|
252
|
+
if (decision.kind === "unavailable") {
|
|
253
|
+
throw new Error(buildUnavailableModelError({ model: decision.model, authorized }));
|
|
254
|
+
}
|
|
255
|
+
// when fallback engages we bypass `resolveModel` for the new slug —
|
|
256
|
+
// `TERRAMEND_MODEL` has higher priority than the slug arg inside that
|
|
257
|
+
// helper and would otherwise re-override back to the unkeyed model.
|
|
258
|
+
// the free fallback slug is already a CLI-ready specifier, so using
|
|
259
|
+
// it verbatim is correct and avoids the override.
|
|
260
|
+
const effectiveSlug = decision.kind === "fallback" ? decision.to : payload.model;
|
|
261
|
+
const resolvedModel = decision.kind === "fallback" ? decision.to : initialResolvedModel;
|
|
262
|
+
if (decision.kind === "fallback") {
|
|
263
|
+
log.warning(
|
|
264
|
+
`» fell back from ${decision.from} to ${decision.to} — no provider key present in runner env. add a provider key in repo secrets to use ${decision.from} instead.`,
|
|
265
|
+
);
|
|
266
|
+
toolState.modelFallback = { from: decision.from };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
vertexCredentials = materializeVertexCredentials({ model: resolvedModel });
|
|
270
|
+
|
|
271
|
+
const agent = resolveAgent({ model: resolvedModel });
|
|
272
|
+
|
|
273
|
+
// surface the effective model in comment/review footers. payload.model is
|
|
274
|
+
// just the stored slug (often undefined when the agent auto-selects).
|
|
275
|
+
// matching priority with resolveModelForLog so the "Using `…`" badge
|
|
276
|
+
// reflects what actually ran.
|
|
277
|
+
toolState.model = resolvedModel ?? effectiveSlug;
|
|
278
|
+
|
|
279
|
+
// skip validation when fallback engaged: the effective model is the
|
|
280
|
+
// free fallback (`opencode/big-pickle`) and the fallback gate already
|
|
281
|
+
// authoritatively decided "this model is OK to run". re-validating
|
|
282
|
+
// would spuriously throw if `opencode models` doesn't list big-pickle.
|
|
283
|
+
if (decision.kind !== "fallback") {
|
|
284
|
+
validateAgentApiKey({
|
|
285
|
+
agent,
|
|
286
|
+
model: resolvedModel ?? effectiveSlug,
|
|
287
|
+
authorized,
|
|
288
|
+
owner: runContext.repo.owner,
|
|
289
|
+
name: runContext.repo.name,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
await setupGit({
|
|
294
|
+
gitToken: tokenRef.gitToken,
|
|
295
|
+
owner: runContext.repo.owner,
|
|
296
|
+
name: runContext.repo.name,
|
|
297
|
+
octokit,
|
|
298
|
+
toolState,
|
|
299
|
+
shell: payload.shell,
|
|
300
|
+
postCheckoutScript: runContext.repoSettings.postCheckoutScript,
|
|
301
|
+
});
|
|
302
|
+
timer.checkpoint("git");
|
|
303
|
+
|
|
304
|
+
// pin the project's package manager via corepack BEFORE the setup hook
|
|
305
|
+
// runs. without this, a customer setup script like `npm i -g pnpm &&
|
|
306
|
+
// pnpm install` installs whatever pnpm is "latest" today and writes
|
|
307
|
+
// unrelated lockfile drift (e.g. the `packageManagerDependencies`
|
|
308
|
+
// block pnpm 11.3 added) into our commit — see #844. resolution
|
|
309
|
+
// honors pnpm 11+ precedence (`devEngines.packageManager` over
|
|
310
|
+
// `packageManager`); failure is non-fatal — we fall back to whatever
|
|
311
|
+
// is on PATH with a warning. the shim lands in a private bin dir
|
|
312
|
+
// (prepended to PATH), not the node bin dir, so a setup `npm i -g pnpm`
|
|
313
|
+
// can't collide with it.
|
|
314
|
+
const pmSpec = await resolvePackageManagerSpec(process.cwd());
|
|
315
|
+
if (pmSpec) {
|
|
316
|
+
await ensurePackageManager({ spec: pmSpec, binDir: packageManagerBinDir(tmpdir) });
|
|
317
|
+
}
|
|
318
|
+
timer.checkpoint("packageManager");
|
|
319
|
+
|
|
320
|
+
// execute the setup lifecycle hook (runs once at initialization). best-effort:
|
|
321
|
+
// a failure no longer aborts the run — we warn the operator and surface it to
|
|
322
|
+
// the agent via the SETUP HOOK FAILED banner so it can verify the env and adapt.
|
|
323
|
+
const setupHook = await executeLifecycleHook({
|
|
324
|
+
event: "setup",
|
|
325
|
+
script: runContext.repoSettings.setupScript,
|
|
326
|
+
normalizeWorkingTreeAfter: true,
|
|
327
|
+
});
|
|
328
|
+
if (setupHook.warning) {
|
|
329
|
+
log.warning(setupHook.warning);
|
|
330
|
+
}
|
|
331
|
+
timer.checkpoint("lifecycleHooks::setup");
|
|
332
|
+
|
|
333
|
+
const agentId = agent.name;
|
|
334
|
+
const modes = [...computeModes(agentId), ...runContext.repoSettings.modes];
|
|
335
|
+
|
|
336
|
+
const outputSchema = resolveOutputSchema();
|
|
337
|
+
|
|
338
|
+
// mcpServerUrl and tmpdir are set after server starts
|
|
339
|
+
toolContext = {
|
|
340
|
+
agentId,
|
|
341
|
+
repo: runContext.repo,
|
|
342
|
+
payload,
|
|
343
|
+
octokit,
|
|
344
|
+
githubInstallationToken: tokenRef.mcpToken,
|
|
345
|
+
gitToken: tokenRef.gitToken,
|
|
346
|
+
apiToken: runContext.apiToken,
|
|
347
|
+
modes,
|
|
348
|
+
postCheckoutScript: runContext.repoSettings.postCheckoutScript,
|
|
349
|
+
prepushScript: runContext.repoSettings.prepushScript,
|
|
350
|
+
prApproveEnabled: runContext.repoSettings.prApproveEnabled,
|
|
351
|
+
// §5.5 — workflow-file review policy / FP precedents compose with the
|
|
352
|
+
// backend-provided per-mode instructions (both owner-controlled).
|
|
353
|
+
modeInstructions: mergeReviewModeInstructions(runContext.repoSettings.modeInstructions, {
|
|
354
|
+
reviewInstructions: payload.reviewInstructions,
|
|
355
|
+
fpFilteringInstructions: payload.fpFilteringInstructions,
|
|
356
|
+
}),
|
|
357
|
+
toolState,
|
|
358
|
+
runId: runInfo.runId,
|
|
359
|
+
mcpServerUrl: "",
|
|
360
|
+
mcpServerToken: "",
|
|
361
|
+
tmpdir,
|
|
362
|
+
oss: runContext.oss,
|
|
363
|
+
plan: runContext.plan,
|
|
364
|
+
resolvedModel,
|
|
365
|
+
};
|
|
366
|
+
await using mcpHttpServer = await startMcpHttpServer(toolContext, { outputSchema });
|
|
367
|
+
toolContext.mcpServerUrl = mcpHttpServer.url;
|
|
368
|
+
toolContext.mcpServerToken = mcpHttpServer.token;
|
|
369
|
+
log.info(`» MCP server started at ${mcpHttpServer.url}`);
|
|
370
|
+
timer.checkpoint("mcpServer");
|
|
371
|
+
|
|
372
|
+
// seed the rolling repo-level learnings tmpfile. the agent reads the file
|
|
373
|
+
// at startup (path is surfaced in the LEARNINGS section of the prompt) and
|
|
374
|
+
// may edit it during the post-run reflection turn. persistLearnings reads
|
|
375
|
+
// it back at end-of-run and PATCHes any changes to Repo.learnings.
|
|
376
|
+
//
|
|
377
|
+
// gated on a configured backend: learnings only have somewhere to persist
|
|
378
|
+
// when a real backend is wired (hosted SaaS / local dev). in a standalone
|
|
379
|
+
// BYOK run there is no store, so seeding the file would only provoke the
|
|
380
|
+
// post-run reflection turn (an extra billed LLM turn) to edit a file whose
|
|
381
|
+
// changes get dropped — and persistLearnings would then PATCH the default
|
|
382
|
+
// marketing host and warn on its 404. leaving learningsFilePath unset makes
|
|
383
|
+
// every downstream consumer (`persistLearnings`, agent harnesses,
|
|
384
|
+
// `resolveInstructions`) treat this as "no learnings affordance this run".
|
|
385
|
+
//
|
|
386
|
+
// wrapped in best-effort try/catch: an unwrapped filesystem failure
|
|
387
|
+
// (ENOSPC, EACCES, hostile sandbox) would unwind into the outer main()
|
|
388
|
+
// catch and flip an otherwise-successful run to "❌ Terramend failed".
|
|
389
|
+
if (isBackendConfigured()) {
|
|
390
|
+
try {
|
|
391
|
+
const learningsPath = await seedLearningsFile({
|
|
392
|
+
tmpdir,
|
|
393
|
+
current: runContext.repoSettings.learnings,
|
|
394
|
+
});
|
|
395
|
+
toolState.learningsFilePath = learningsPath;
|
|
396
|
+
// file on disk is the verbatim DB body, so the seed used for
|
|
397
|
+
// change-detection is just `current ?? ""` (trimmed). persistLearnings
|
|
398
|
+
// byte-compares against the trimmed read-back to skip no-op PATCHes.
|
|
399
|
+
toolState.learningsSeed = (runContext.repoSettings.learnings ?? "").trim();
|
|
400
|
+
log.info(
|
|
401
|
+
`» learnings seeded at ${learningsPath} (existing=${runContext.repoSettings.learnings ? "yes" : "no"})`,
|
|
402
|
+
);
|
|
403
|
+
const ctxForExit = toolContext;
|
|
404
|
+
onExitSignal(() => persistLearnings(ctxForExit));
|
|
405
|
+
} catch (err) {
|
|
406
|
+
log.warning(
|
|
407
|
+
`» learnings seed failed: ${err instanceof Error ? err.message : String(err)} — continuing without learnings file`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
log.debug(
|
|
412
|
+
"no backend configured (API_URL unset) — skipping learnings seed + reflection turn",
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// seed the rolling PR summary tmpfile when the dispatcher requested it.
|
|
417
|
+
// gated on event being a PR — issue/workflow_dispatch runs have no
|
|
418
|
+
// summarySnapshot to maintain. file path is exposed to the agent via
|
|
419
|
+
// the select_mode response addendum (action/mcp/selectMode.ts).
|
|
420
|
+
if (payload.generateSummary && payload.event.is_pr && payload.event.issue_number) {
|
|
421
|
+
const previousSnapshot = await fetchPreviousSnapshot(toolContext, payload.event.issue_number);
|
|
422
|
+
const filePath = await seedSummaryFile({ tmpdir, previousSnapshot });
|
|
423
|
+
toolState.summaryFilePath = filePath;
|
|
424
|
+
// capture the exact bytes the agent will see at startup. used by
|
|
425
|
+
// the post-run retry loop to detect the agent forgetting to edit
|
|
426
|
+
// the file (byte-identical to seed → nudge once via resume turn)
|
|
427
|
+
// and by persistSummary to skip the DB write when nothing changed.
|
|
428
|
+
try {
|
|
429
|
+
toolState.summarySeed = await readFile(filePath, "utf8");
|
|
430
|
+
} catch {
|
|
431
|
+
// intentionally empty — summarySeed stays undefined
|
|
432
|
+
}
|
|
433
|
+
log.info(
|
|
434
|
+
`» summary snapshot seeded at ${filePath} (previous=${previousSnapshot ? "yes" : "no"})`,
|
|
435
|
+
);
|
|
436
|
+
// on SIGINT/SIGTERM we still want to persist whatever the agent has
|
|
437
|
+
// written so far. handler is best-effort: any failure inside is
|
|
438
|
+
// swallowed by Promise.allSettled in exitHandler.ts, and the
|
|
439
|
+
// summaryPersistAttempted guard prevents double-execution if the
|
|
440
|
+
// signal arrives after the normal path already persisted. capture a
|
|
441
|
+
// narrowed reference so the closure doesn't depend on the outer
|
|
442
|
+
// `toolContext` variable being defined later.
|
|
443
|
+
const ctxForExit = toolContext;
|
|
444
|
+
onExitSignal(() => persistSummary(ctxForExit));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
startInstallation(toolContext);
|
|
448
|
+
|
|
449
|
+
logRunStartup({ payload, resolvedModel, agentName: agent.name });
|
|
450
|
+
|
|
451
|
+
const instructions = resolveInstructions({
|
|
452
|
+
payload,
|
|
453
|
+
repo: runContext.repo,
|
|
454
|
+
modes,
|
|
455
|
+
agentId,
|
|
456
|
+
outputSchema,
|
|
457
|
+
learningsFilePath: toolState.learningsFilePath ?? null,
|
|
458
|
+
learningsHeadings: runContext.repoSettings.learningsHeadings,
|
|
459
|
+
setupHookFailure: describeSetupFailure(setupHook.failure),
|
|
460
|
+
});
|
|
461
|
+
const logParts = [
|
|
462
|
+
instructions.eventInstructions
|
|
463
|
+
? `EVENT-LEVEL INSTRUCTIONS:\n${instructions.eventInstructions}`
|
|
464
|
+
: null,
|
|
465
|
+
instructions.user ? `USER REQUEST:\n${instructions.user}` : null,
|
|
466
|
+
instructions.event,
|
|
467
|
+
].filter(Boolean);
|
|
468
|
+
log.box(logParts.join("\n\n---\n\n"), {
|
|
469
|
+
title: "Instructions",
|
|
470
|
+
});
|
|
471
|
+
log.group("View full prompt", () => {
|
|
472
|
+
log.info(instructions.full);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// OpenCode loads .opencode/plugin/ files at startup. if the repo has any,
|
|
476
|
+
// eagerly await dependency installation so plugin imports can resolve.
|
|
477
|
+
if (agentId === "opencode") {
|
|
478
|
+
const pluginDir = join(process.cwd(), ".opencode", "plugin");
|
|
479
|
+
const hasPlugins =
|
|
480
|
+
existsSync(pluginDir) && readdirSync(pluginDir).some((f) => /\.[jt]sx?$/.test(f));
|
|
481
|
+
if (hasPlugins && toolState.dependencyInstallation?.promise) {
|
|
482
|
+
log.info(
|
|
483
|
+
"» .opencode/plugin/ detected — awaiting dependency installation before agent start",
|
|
484
|
+
);
|
|
485
|
+
await toolState.dependencyInstallation.promise.catch(() => {});
|
|
486
|
+
timer.checkpoint("awaitDepsForPlugins");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// run agent, optionally with timeout enforcement
|
|
491
|
+
activityTimeout = createProcessOutputActivityTimeout({
|
|
492
|
+
timeoutMs: AGENT_ACTIVITY_TIMEOUT_MS,
|
|
493
|
+
checkIntervalMs: DEFAULT_ACTIVITY_CHECK_INTERVAL_MS,
|
|
494
|
+
});
|
|
495
|
+
activityTimeout.promise.catch(() => {}); // prevent unhandled rejection if agent wins race
|
|
496
|
+
todoTracker = createTodoTracker(async (body) => {
|
|
497
|
+
if (progressCallbackDisabled || !toolContext) return;
|
|
498
|
+
try {
|
|
499
|
+
// liveProgress: this is the auto-rendered checklist, not a deliberate
|
|
500
|
+
// final answer — must not flip wasUpdated / lastProgressBody (see #868).
|
|
501
|
+
await reportProgress(toolContext, { body, liveProgress: true });
|
|
502
|
+
} catch (err) {
|
|
503
|
+
log.debug(`progress update failed: ${err}`);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
toolState.todoTracker = todoTracker;
|
|
507
|
+
|
|
508
|
+
// on cancellation, stop scheduling new tracker writes immediately. without this, a
|
|
509
|
+
// debounced write queued just before SIGTERM could land at GitHub *after* the
|
|
510
|
+
// workflow_run.completed webhook has already replaced the comment with the
|
|
511
|
+
// "This run was cancelled" body, clobbering it back to the task list. we can't
|
|
512
|
+
// await in-flight writes (the process is exiting), but cancelling the timer
|
|
513
|
+
// shrinks the race window.
|
|
514
|
+
onExitSignal(() => {
|
|
515
|
+
todoTracker?.cancel();
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// when the agent subprocess is killed for inner activity timeout, stop
|
|
519
|
+
// the MCP HTTP server so mcp-proxy's SSE reconnect attempts don't keep
|
|
520
|
+
// the outer activity timer alive. start a short safety-net timer — if
|
|
521
|
+
// the agent promise hasn't resolved within 5min after the inner kill,
|
|
522
|
+
// force-reject the outer timer so the run can exit.
|
|
523
|
+
let innerTimeoutFired = false;
|
|
524
|
+
const onInnerActivityTimeout = () => {
|
|
525
|
+
if (innerTimeoutFired) return;
|
|
526
|
+
innerTimeoutFired = true;
|
|
527
|
+
log.info(
|
|
528
|
+
"» inner activity timeout fired — stopping MCP server and starting 5min safety-net timer",
|
|
529
|
+
);
|
|
530
|
+
// fire and forget — the server's dispose is idempotent so the
|
|
531
|
+
// `await using` cleanup at block exit is still safe.
|
|
532
|
+
mcpHttpServer[Symbol.asyncDispose]().catch((err) => {
|
|
533
|
+
log.debug(
|
|
534
|
+
`mcp server stop after inner kill failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
535
|
+
);
|
|
536
|
+
});
|
|
537
|
+
safetyNetTimer = setTimeout(
|
|
538
|
+
() => {
|
|
539
|
+
activityTimeout?.forceReject(
|
|
540
|
+
"agent still pending 5min after inner activity kill — forcing exit",
|
|
541
|
+
);
|
|
542
|
+
},
|
|
543
|
+
5 * 60 * 1000,
|
|
544
|
+
);
|
|
545
|
+
safetyNetTimer.unref?.();
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const agentPromise = agent.run({
|
|
549
|
+
payload,
|
|
550
|
+
resolvedModel,
|
|
551
|
+
mcpServerUrl: mcpHttpServer.url,
|
|
552
|
+
mcpServerToken: mcpHttpServer.token,
|
|
553
|
+
tmpdir,
|
|
554
|
+
// TERRAMEND_DATA_DIR (/var/lib/terramend) holds codex auth.json + any
|
|
555
|
+
// future terramend-managed on-disk secrets. bash via MCP tmpfs-overlays
|
|
556
|
+
// it; agent native FS tools deny it via the same secretDenyPaths plumbing
|
|
557
|
+
// used for vertex creds. see wiki/security.md "Filesystem Sandbox".
|
|
558
|
+
secretDenyPaths: [
|
|
559
|
+
TERRAMEND_DATA_DIR,
|
|
560
|
+
...(vertexCredentials ? [vertexCredentials.secretDir] : []),
|
|
561
|
+
],
|
|
562
|
+
instructions,
|
|
563
|
+
todoTracker,
|
|
564
|
+
stopScript: runContext.repoSettings.stopScript,
|
|
565
|
+
toolState,
|
|
566
|
+
apiToken: runContext.apiToken,
|
|
567
|
+
onActivityTimeout: onInnerActivityTimeout,
|
|
568
|
+
onToolUse: (event) => {
|
|
569
|
+
const wasTracked = recordDiffReadFromToolUse({
|
|
570
|
+
state: toolState.diffCoverage,
|
|
571
|
+
toolName: event.toolName,
|
|
572
|
+
input: event.input,
|
|
573
|
+
cwd: process.cwd(),
|
|
574
|
+
});
|
|
575
|
+
if (!wasTracked) return;
|
|
576
|
+
const trackedRanges = toolState.diffCoverage?.coveredRanges ?? [];
|
|
577
|
+
log.debug(
|
|
578
|
+
`» diff coverage tracked from tool ${event.toolName} (${trackedRanges.length} merged range${trackedRanges.length === 1 ? "" : "s"})`,
|
|
579
|
+
);
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
// symmetric with the activityTimeout/timeoutPromise catches below: if a
|
|
583
|
+
// timeout wins the race, agentPromise is stranded and its later rejection
|
|
584
|
+
// becomes an unhandled rejection. node 15+ terminates the process on
|
|
585
|
+
// unhandled rejection by default, which would kill main() mid-cleanup and
|
|
586
|
+
// lose the error-reporting / usage-summary work that follows. the race
|
|
587
|
+
// still sees the rejection (the original promise is shared); this catch
|
|
588
|
+
// only keeps node from treating a post-race rejection as unobserved.
|
|
589
|
+
agentPromise.catch(() => {});
|
|
590
|
+
|
|
591
|
+
// timeout enforcement: default is 1 hour, but can be overridden via flags in the prompt:
|
|
592
|
+
// - --timeout=2h (or any duration like "--timeout=30m", "--timeout=1h30m") to set a custom timeout
|
|
593
|
+
// - --notimeout to disable timeout entirely
|
|
594
|
+
let result: Awaited<typeof agentPromise>;
|
|
595
|
+
if (payload.timeout === TIMEOUT_DISABLED) {
|
|
596
|
+
result = await Promise.race([agentPromise, activityTimeout.promise]);
|
|
597
|
+
} else {
|
|
598
|
+
// resolveTimeoutMs rejects unparseable / zero / setTimeout-overflow inputs
|
|
599
|
+
// so a bad string can't silently resolve to an instant timeout. fall back
|
|
600
|
+
// to the 1h default with a warning — users who want runtime measured in
|
|
601
|
+
// weeks should use --notimeout.
|
|
602
|
+
const usable = resolveTimeoutMs(payload.timeout);
|
|
603
|
+
if (payload.timeout && usable === null) {
|
|
604
|
+
log.warning(`invalid timeout "${payload.timeout}" (use --notimeout to disable), using 1h`);
|
|
605
|
+
}
|
|
606
|
+
const timeoutMs = usable ?? 3600000;
|
|
607
|
+
const actualTimeout = usable !== null ? payload.timeout : "1h";
|
|
608
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
609
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
610
|
+
timeoutId = setTimeout(() => {
|
|
611
|
+
reject(new Error(`agent run timed out after ${actualTimeout}`));
|
|
612
|
+
}, timeoutMs);
|
|
613
|
+
});
|
|
614
|
+
timeoutPromise.catch(() => {}); // prevent unhandled rejection if agent wins race
|
|
615
|
+
try {
|
|
616
|
+
result = await Promise.race([agentPromise, timeoutPromise, activityTimeout.promise]);
|
|
617
|
+
} finally {
|
|
618
|
+
clearTimeout(timeoutId);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// accumulate top-level agent usage
|
|
623
|
+
if (result.usage) {
|
|
624
|
+
toolState.usageEntries.push(result.usage);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// validate this before writing job summary to avoid masking the error
|
|
628
|
+
if (outputSchema && !toolState.output) {
|
|
629
|
+
throw new Error(
|
|
630
|
+
"output_schema was provided but agent did not call set_output — structured output is required",
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// success-path cleanup: postReview → persistSummary → persistLearnings →
|
|
635
|
+
// failure-error-report → stranded-comment cleanup → job summary → output
|
|
636
|
+
// marker. each step is best-effort; see `finalizeSuccessRun` for ordering
|
|
637
|
+
// rationale (notably: progress-comment deletion lives in
|
|
638
|
+
// create_pull_request_review for review-mode runs, so deletion here
|
|
639
|
+
// covers the non-review success paths).
|
|
640
|
+
await finalizeSuccessRun({ toolContext, toolState, result, repo: runContext.repo });
|
|
641
|
+
|
|
642
|
+
return await handleAgentResult({
|
|
643
|
+
result,
|
|
644
|
+
toolContext,
|
|
645
|
+
silent: payload.event.silent ?? false,
|
|
646
|
+
});
|
|
647
|
+
} catch (error) {
|
|
648
|
+
const errorMessage = error instanceof Error ? error.message : "unknown error occurred";
|
|
649
|
+
progressCallbackDisabled = true;
|
|
650
|
+
todoTracker?.cancel();
|
|
651
|
+
killTrackedChildren();
|
|
652
|
+
log.error(errorMessage);
|
|
653
|
+
|
|
654
|
+
// classify (BillingError reclassification + hang detection + API-key auth
|
|
655
|
+
// detection) and render to {summary, comment} markdown bodies.
|
|
656
|
+
const rendered = renderRunError({
|
|
657
|
+
errorMessage,
|
|
658
|
+
repo: runContext.repo,
|
|
659
|
+
agentDiagnostic: toolState.agentDiagnostic,
|
|
660
|
+
});
|
|
661
|
+
await writeRunErrorOutputs({ rendered, toolState });
|
|
662
|
+
|
|
663
|
+
// best-effort cleanup: review dispatch, summary persist, learnings persist.
|
|
664
|
+
// a partial edit before the crash is still worth keeping.
|
|
665
|
+
if (toolContext) {
|
|
666
|
+
await persistRunArtifacts(toolContext);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return {
|
|
670
|
+
success: false,
|
|
671
|
+
error: errorMessage,
|
|
672
|
+
};
|
|
673
|
+
} finally {
|
|
674
|
+
activityTimeout?.stop();
|
|
675
|
+
if (safetyNetTimer) clearTimeout(safetyNetTimer);
|
|
676
|
+
// also reap on the success-with-failure path (agent returned
|
|
677
|
+
// `{success: false}`) which skips the catch above and would otherwise hang
|
|
678
|
+
// ~60s on the eager dep install. idempotent SIGKILL. see #862.
|
|
679
|
+
killTrackedChildren();
|
|
680
|
+
if (usageSummaryPath) {
|
|
681
|
+
// a write error here (ENOSPC, EACCES, dirname removed) must not mask
|
|
682
|
+
// either the try's successful return or the catch's error return.
|
|
683
|
+
// the summary is informational — log and move on.
|
|
684
|
+
try {
|
|
685
|
+
await writeGitHubUsageSummaryToFile(usageSummaryPath);
|
|
686
|
+
} catch (err) {
|
|
687
|
+
log.debug(
|
|
688
|
+
`failed to write usage summary to ${usageSummaryPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// persist aggregated token + cost usage to the WorkflowRun row.
|
|
694
|
+
// this is the single shared cleanup path across every agent implementation:
|
|
695
|
+
// each agent harness returns a single AgentUsage from agent.run() that
|
|
696
|
+
// already aggregates its internal retries via mergeAgentUsage, and the
|
|
697
|
+
// success branch above pushes that entry into toolState.usageEntries.
|
|
698
|
+
// aggregateUsage sums across those entries (one per agent.run()).
|
|
699
|
+
//
|
|
700
|
+
// caveat: if the agent promise rejected (timeout or uncaught throw) the
|
|
701
|
+
// usage was never pushed, so nothing gets persisted for that run. runs
|
|
702
|
+
// that returned AgentResult with success=false still report their partial
|
|
703
|
+
// usage because the harness populates AgentUsage before returning.
|
|
704
|
+
if (toolContext) {
|
|
705
|
+
const patch = aggregateUsage(toolState.usageEntries);
|
|
706
|
+
if (Object.keys(patch).length > 0) {
|
|
707
|
+
await patchWorkflowRunFields(toolContext, patch);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
cleanupVertexCredentials(vertexCredentials);
|
|
711
|
+
}
|
|
712
|
+
}
|