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
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import type { AgentId } from "#app/external";
|
|
3
|
+
import type { ToolState } from "#app/toolState";
|
|
4
|
+
import { log } from "#app/utils/cli";
|
|
5
|
+
import type { ResolvedInstructions } from "#app/utils/instructions";
|
|
6
|
+
import type { ResolvedPayload } from "#app/utils/payload";
|
|
7
|
+
import type { TodoTracker } from "#app/utils/todoTracking";
|
|
8
|
+
|
|
9
|
+
// maximum number of stderr lines to keep in the rolling buffer during agent execution
|
|
10
|
+
export const MAX_STDERR_LINES = 20;
|
|
11
|
+
|
|
12
|
+
// ── post-run retry loop ────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* how many times the post-run loop may resume the agent to fix a dirty tree
|
|
16
|
+
* or a failing stop hook before giving up.
|
|
17
|
+
*/
|
|
18
|
+
export const MAX_POST_RUN_RETRIES = 3;
|
|
19
|
+
|
|
20
|
+
export function getGitStatus(): string {
|
|
21
|
+
try {
|
|
22
|
+
return execFileSync("git", ["status", "--porcelain"], {
|
|
23
|
+
encoding: "utf-8",
|
|
24
|
+
timeout: 10_000,
|
|
25
|
+
}).trim();
|
|
26
|
+
} catch {
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildCommitPrompt(status: string): string {
|
|
32
|
+
return [
|
|
33
|
+
`UNCOMMITTED CHANGES — the working tree is dirty. push all changes to a pull request (new or existing). \`git status\` must be clean before you finish.`,
|
|
34
|
+
"",
|
|
35
|
+
"```",
|
|
36
|
+
status,
|
|
37
|
+
"```",
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface StopHookFailure {
|
|
42
|
+
exitCode: number;
|
|
43
|
+
output: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SummaryStale {
|
|
47
|
+
/** absolute path to the seeded snapshot file the agent was meant to edit. */
|
|
48
|
+
filePath: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface PostRunIssues {
|
|
52
|
+
stopHook?: StopHookFailure;
|
|
53
|
+
dirtyTree?: string;
|
|
54
|
+
/** populated when the rolling PR summary file is byte-identical to its
|
|
55
|
+
* seed, i.e. the agent never touched it. soft gate — nudges once via a
|
|
56
|
+
* resume turn but never fails the run, parallel to dirtyTree semantics. */
|
|
57
|
+
summaryStale?: SummaryStale;
|
|
58
|
+
/**
|
|
59
|
+
* populated when the agent selected a review mode but the post-run check
|
|
60
|
+
* over toolState shows neither a `create_pull_request_review` submission
|
|
61
|
+
* nor a final `report_progress` write happened. derived inline from
|
|
62
|
+
* `toolState.selectedMode` + `toolState.review` + `toolState.finalSummaryWritten`
|
|
63
|
+
* via {@link getUnsubmittedReview} — no parallel toolState flag is stored.
|
|
64
|
+
* carries the mode name so the resume prompt can reference it. handled like
|
|
65
|
+
* `stopHook`: nudge via resume, hard-fail if still unsatisfied after
|
|
66
|
+
* `MAX_POST_RUN_RETRIES`.
|
|
67
|
+
*/
|
|
68
|
+
unsubmittedReview?: "Review" | "IncrementalReview";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function hasPostRunIssues(issues: PostRunIssues): boolean {
|
|
72
|
+
return (
|
|
73
|
+
issues.stopHook !== undefined ||
|
|
74
|
+
issues.dirtyTree !== undefined ||
|
|
75
|
+
issues.summaryStale !== undefined ||
|
|
76
|
+
issues.unsubmittedReview !== undefined
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* token/cost usage data from a single agent run.
|
|
82
|
+
*
|
|
83
|
+
* NOTE on semantics: `inputTokens` here is the *total* billable input for the
|
|
84
|
+
* run — non-cached input + cache read + cache write — matching the per-agent
|
|
85
|
+
* SDK conventions. This is what gets persisted to `WorkflowRun.inputTokens`.
|
|
86
|
+
*
|
|
87
|
+
* The stdout token table and markdown step summary display a different "Input"
|
|
88
|
+
* column that shows only the non-cached portion (derivable as
|
|
89
|
+
* `inputTokens - cacheReadTokens - cacheWriteTokens`) so humans can see the
|
|
90
|
+
* cache hit ratio at a glance. Dashboards that query `WorkflowRun.inputTokens`
|
|
91
|
+
* directly are seeing the full total, not the log column.
|
|
92
|
+
*/
|
|
93
|
+
export interface AgentUsage {
|
|
94
|
+
agent: string;
|
|
95
|
+
/** full billable input: non-cached + cache read + cache write */
|
|
96
|
+
inputTokens: number;
|
|
97
|
+
outputTokens: number;
|
|
98
|
+
cacheReadTokens?: number | undefined;
|
|
99
|
+
cacheWriteTokens?: number | undefined;
|
|
100
|
+
costUsd?: number | undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface AgentToolUseEvent {
|
|
104
|
+
toolName: string;
|
|
105
|
+
input: unknown;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Result returned by agent execution
|
|
110
|
+
*/
|
|
111
|
+
export interface AgentResult {
|
|
112
|
+
success: boolean;
|
|
113
|
+
output?: string | undefined;
|
|
114
|
+
error?: string | undefined;
|
|
115
|
+
metadata?: Record<string, unknown>;
|
|
116
|
+
usage?: AgentUsage | undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Env var that carries the per-run MCP bearer token to BOTH agent harnesses,
|
|
121
|
+
* which reference it from their MCP client config so the on-disk/in-config form
|
|
122
|
+
* holds only a placeholder, never the raw token:
|
|
123
|
+
* - Claude Code expands `${TERRAMEND_MCP_TOKEN}` in .mcp.json headers.
|
|
124
|
+
* - opencode expands `{env:TERRAMEND_MCP_TOKEN}` in remote-MCP headers.
|
|
125
|
+
* The `_TOKEN` suffix also makes filterEnv() strip it from the MCP shell
|
|
126
|
+
* sandbox. Set only on the agent/server spawn env (never process.env), so a
|
|
127
|
+
* co-located dependency-install subprocess never inherits it. See
|
|
128
|
+
* ToolContext.mcpServerToken and gateServer.ts for the same pattern.
|
|
129
|
+
*/
|
|
130
|
+
export const MCP_SERVER_TOKEN_ENV = "TERRAMEND_MCP_TOKEN";
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Context passed to agent.run() and threaded through the post-run loop.
|
|
134
|
+
*
|
|
135
|
+
* design rule: this is the single object that flows through the harness and
|
|
136
|
+
* downstream utilities by reference. derived predicates (e.g.
|
|
137
|
+
* `getUnsubmittedReview`), tmpfile paths, and seed bytes live on
|
|
138
|
+
* `toolState` — read them at the call site, do not duplicate them onto this
|
|
139
|
+
* interface. utilities that need run state should accept `ctx` whole, not
|
|
140
|
+
* destructure a narrow subset.
|
|
141
|
+
*/
|
|
142
|
+
export interface AgentRunContext {
|
|
143
|
+
payload: ResolvedPayload;
|
|
144
|
+
resolvedModel?: string | undefined;
|
|
145
|
+
mcpServerUrl: string;
|
|
146
|
+
/** per-run bearer token the agent's MCP client presents to mcpServerUrl. See
|
|
147
|
+
* ToolContext.mcpServerToken — delivered to the client config out-of-band so
|
|
148
|
+
* it never lands in a readable file. */
|
|
149
|
+
mcpServerToken: string;
|
|
150
|
+
tmpdir: string;
|
|
151
|
+
/** harness-owned secret paths that agent filesystem tools must never read. */
|
|
152
|
+
secretDenyPaths?: string[] | undefined;
|
|
153
|
+
instructions: ResolvedInstructions;
|
|
154
|
+
todoTracker?: TodoTracker | undefined;
|
|
155
|
+
/**
|
|
156
|
+
* user-configured stop hook script. runs after the agent finishes each
|
|
157
|
+
* attempt; non-zero exit resumes the agent with the hook output as
|
|
158
|
+
* guidance. null when the repo has no stop hook configured.
|
|
159
|
+
*/
|
|
160
|
+
stopScript?: string | null | undefined;
|
|
161
|
+
/**
|
|
162
|
+
* mutable per-run state shared with the MCP server (by reference). post-run
|
|
163
|
+
* gates read fresh values from it after each agent attempt — `summaryFilePath`,
|
|
164
|
+
* `summarySeed`, `selectedMode`, `review`, `finalSummaryWritten`,
|
|
165
|
+
* `hadProgressComment` are all consulted by `collectPostRunIssues`. see
|
|
166
|
+
* `action/toolState.ts` for the literal-state design rule.
|
|
167
|
+
*/
|
|
168
|
+
toolState: ToolState;
|
|
169
|
+
/**
|
|
170
|
+
* called synchronously when the agent subprocess is killed for inner
|
|
171
|
+
* activity timeout. lets main.ts tear down shared resources (MCP HTTP
|
|
172
|
+
* server) so lingering SSE reconnects don't keep the outer timer alive.
|
|
173
|
+
*/
|
|
174
|
+
onActivityTimeout?: (() => void) | undefined;
|
|
175
|
+
onToolUse?: ((event: AgentToolUseEvent) => void) | undefined;
|
|
176
|
+
/**
|
|
177
|
+
* Terramend API JWT scoped to this run. agents only need this when they
|
|
178
|
+
* have to write state back to Terramend mid-run (today: opencode.ts uses
|
|
179
|
+
* it to seed the post-hook's writeback envelope for Codex auth refresh).
|
|
180
|
+
* empty string when the run wasn't context-resolved (e.g. local dry-runs).
|
|
181
|
+
*/
|
|
182
|
+
apiToken: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface Agent {
|
|
186
|
+
name: AgentId;
|
|
187
|
+
install: (token?: string) => Promise<string>;
|
|
188
|
+
run: (ctx: AgentRunContext) => Promise<AgentResult>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export const agent = (input: Agent): Agent => {
|
|
192
|
+
return {
|
|
193
|
+
...input,
|
|
194
|
+
run: async (ctx: AgentRunContext): Promise<AgentResult> => {
|
|
195
|
+
log.debug(`» payload: ${JSON.stringify(ctx.payload, null, 2)}`);
|
|
196
|
+
return input.run(ctx);
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/** format a USD cost to 4 decimal places, always showing the leading zero */
|
|
202
|
+
export function formatCostUsd(costUsd: number): string {
|
|
203
|
+
return costUsd.toFixed(4);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* merge two AgentUsage snapshots into one running total.
|
|
208
|
+
*
|
|
209
|
+
* both agent harnesses invoke their runner multiple times per `run()` when the
|
|
210
|
+
* post-run retry loop kicks in (MAX_POST_RUN_RETRIES). each invocation
|
|
211
|
+
* produces its own AgentUsage; we sum them so downstream callers (usage
|
|
212
|
+
* summary, WorkflowRun persistence) see the whole session — not just the
|
|
213
|
+
* final retry's slice.
|
|
214
|
+
*
|
|
215
|
+
* returns `undefined` when both sides are empty so callers can short-circuit
|
|
216
|
+
* without a special case. zero-valued cache / cost fields are dropped to
|
|
217
|
+
* `undefined` for symmetry with each harness's `buildUsage`.
|
|
218
|
+
*/
|
|
219
|
+
export function mergeAgentUsage(
|
|
220
|
+
a: AgentUsage | undefined,
|
|
221
|
+
b: AgentUsage | undefined,
|
|
222
|
+
): AgentUsage | undefined {
|
|
223
|
+
// always return a fresh object — callers treat AgentUsage as immutable, and
|
|
224
|
+
// returning `a` / `b` directly would leak that invariant to future callers
|
|
225
|
+
if (!a && !b) return undefined;
|
|
226
|
+
if (!a) return { ...(b as AgentUsage) };
|
|
227
|
+
if (!b) return { ...a };
|
|
228
|
+
const cacheRead = (a.cacheReadTokens ?? 0) + (b.cacheReadTokens ?? 0);
|
|
229
|
+
const cacheWrite = (a.cacheWriteTokens ?? 0) + (b.cacheWriteTokens ?? 0);
|
|
230
|
+
const cost = (a.costUsd ?? 0) + (b.costUsd ?? 0);
|
|
231
|
+
return {
|
|
232
|
+
agent: a.agent,
|
|
233
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
234
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
235
|
+
cacheReadTokens: cacheRead > 0 ? cacheRead : undefined,
|
|
236
|
+
cacheWriteTokens: cacheWrite > 0 ? cacheWrite : undefined,
|
|
237
|
+
costUsd: cost > 0 ? cost : undefined,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* unified per-run token table used by every agent harness.
|
|
243
|
+
*
|
|
244
|
+
* columns are kept stable across agents and models so downstream log parsers
|
|
245
|
+
* (scripts/token-usage.ts, cost dashboards) only have to understand one format:
|
|
246
|
+
*
|
|
247
|
+
* Input non-cached input tokens sent this run
|
|
248
|
+
* Cache Read input tokens served from prompt cache (Anthropic, etc.)
|
|
249
|
+
* Cache Write input tokens written to prompt cache this run
|
|
250
|
+
* Output assistant output tokens
|
|
251
|
+
* Total sum of the four columns — the real billable quantity
|
|
252
|
+
* Cost ($) USD cost reported by the provider (only rendered when known)
|
|
253
|
+
*
|
|
254
|
+
* models that don't report prompt caching leave Cache Read / Write at 0.
|
|
255
|
+
* OpenCode emits per-step `part.cost` sourced from models.dev (works across
|
|
256
|
+
* Anthropic, OpenAI, Google, xAI, DeepSeek, Moonshot, OpenRouter, etc.);
|
|
257
|
+
* Claude CLI emits `total_cost_usd` on its final `result` event. pass the
|
|
258
|
+
* accumulated value via `costUsd` to render the Cost column.
|
|
259
|
+
*/
|
|
260
|
+
export function logTokenTable(t: {
|
|
261
|
+
input: number;
|
|
262
|
+
cacheRead: number;
|
|
263
|
+
cacheWrite: number;
|
|
264
|
+
output: number;
|
|
265
|
+
costUsd?: number | undefined;
|
|
266
|
+
}): void {
|
|
267
|
+
const total = t.input + t.cacheRead + t.cacheWrite + t.output;
|
|
268
|
+
// narrow costUsd to a concrete number so the render path doesn't need a cast
|
|
269
|
+
const costUsd = typeof t.costUsd === "number" && t.costUsd > 0 ? t.costUsd : undefined;
|
|
270
|
+
|
|
271
|
+
const headerRow: Array<{ data: string; header: true }> = [
|
|
272
|
+
{ data: "Input", header: true },
|
|
273
|
+
{ data: "Cache Read", header: true },
|
|
274
|
+
{ data: "Cache Write", header: true },
|
|
275
|
+
{ data: "Output", header: true },
|
|
276
|
+
{ data: "Total", header: true },
|
|
277
|
+
];
|
|
278
|
+
const dataRow: string[] = [
|
|
279
|
+
String(t.input),
|
|
280
|
+
String(t.cacheRead),
|
|
281
|
+
String(t.cacheWrite),
|
|
282
|
+
String(t.output),
|
|
283
|
+
String(total),
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
if (costUsd !== undefined) {
|
|
287
|
+
headerRow.push({ data: "Cost ($)", header: true });
|
|
288
|
+
dataRow.push(formatCostUsd(costUsd));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
log.table([headerRow, dataRow]);
|
|
292
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { deriveSubagentModels } from "#app/agents/subagentModels";
|
|
3
|
+
|
|
4
|
+
describe("deriveSubagentModels", () => {
|
|
5
|
+
it("returns no override when orchestrator is undefined", () => {
|
|
6
|
+
expect(deriveSubagentModels(undefined)).toEqual({ reviewer: undefined });
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("returns no override when orchestrator slug isn't registered", () => {
|
|
10
|
+
expect(deriveSubagentModels("nonexistent/model")).toEqual({ reviewer: undefined });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("anthropic family — opus → sonnet", () => {
|
|
14
|
+
it("direct anthropic opus", () => {
|
|
15
|
+
expect(deriveSubagentModels("anthropic/claude-opus-4-8")).toEqual({
|
|
16
|
+
reviewer: "anthropic/claude-sonnet-4-6",
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
it("opencode-vendored opus stays on opencode prefix", () => {
|
|
20
|
+
expect(deriveSubagentModels("opencode/claude-opus-4-8")).toEqual({
|
|
21
|
+
reviewer: "opencode/claude-sonnet-4-6",
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
it("openrouter-anthropic-opus-via-anthropic-direct hits anthropic alias's openRouterResolve", () => {
|
|
25
|
+
// both the anthropic alias and the opencode alias have the same
|
|
26
|
+
// openRouterResolve. first-match-wins by alias declaration order
|
|
27
|
+
// (anthropic declared first in providers).
|
|
28
|
+
expect(deriveSubagentModels("openrouter/anthropic/claude-opus-4.8")).toEqual({
|
|
29
|
+
reviewer: "openrouter/anthropic/claude-sonnet-4.6",
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
it("sonnet has no further downshift", () => {
|
|
33
|
+
expect(deriveSubagentModels("anthropic/claude-sonnet-4-6")).toEqual({ reviewer: undefined });
|
|
34
|
+
expect(deriveSubagentModels("opencode/claude-sonnet-4-6")).toEqual({ reviewer: undefined });
|
|
35
|
+
});
|
|
36
|
+
it("haiku has no downshift", () => {
|
|
37
|
+
expect(deriveSubagentModels("anthropic/claude-haiku-4-5")).toEqual({ reviewer: undefined });
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("openai family", () => {
|
|
42
|
+
it("gpt-pro → gpt (direct)", () => {
|
|
43
|
+
expect(deriveSubagentModels("openai/gpt-5.5-pro")).toEqual({ reviewer: "openai/gpt-5.5" });
|
|
44
|
+
});
|
|
45
|
+
it("gpt → gpt-5.4 (direct)", () => {
|
|
46
|
+
expect(deriveSubagentModels("openai/gpt-5.5")).toEqual({ reviewer: "openai/gpt-5.4" });
|
|
47
|
+
});
|
|
48
|
+
it("gpt → gpt-5.4 (opencode-vendored)", () => {
|
|
49
|
+
expect(deriveSubagentModels("opencode/gpt-5.5")).toEqual({ reviewer: "opencode/gpt-5.4" });
|
|
50
|
+
});
|
|
51
|
+
it("gpt-pro → gpt (openrouter)", () => {
|
|
52
|
+
expect(deriveSubagentModels("openrouter/openai/gpt-5.5-pro")).toEqual({
|
|
53
|
+
reviewer: "openrouter/openai/gpt-5.5",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
it("gpt → gpt-5.4 (openrouter)", () => {
|
|
57
|
+
expect(deriveSubagentModels("openrouter/openai/gpt-5.5")).toEqual({
|
|
58
|
+
reviewer: "openrouter/openai/gpt-5.4",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
it("gpt-5.4 itself (the hidden subagent target) has no further downshift", () => {
|
|
62
|
+
expect(deriveSubagentModels("openai/gpt-5.4")).toEqual({ reviewer: undefined });
|
|
63
|
+
});
|
|
64
|
+
it("gpt-mini has no downshift", () => {
|
|
65
|
+
expect(deriveSubagentModels("openai/gpt-5.4-mini")).toEqual({ reviewer: undefined });
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("google (gemini) — inherit (Pro for both orchestrator and lenses)", () => {
|
|
70
|
+
// pro → flash was a meaningful capability cliff (Flash missed catastrophic
|
|
71
|
+
// cross-file bugs the v4 e2e test surfaced); Pro is cost-effective enough
|
|
72
|
+
// to keep on for lenses too. Google has no in-between tier.
|
|
73
|
+
it("direct google pro inherits", () => {
|
|
74
|
+
expect(deriveSubagentModels("google/gemini-3.1-pro-preview")).toEqual({
|
|
75
|
+
reviewer: undefined,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
it("opencode-vendored gemini-pro inherits", () => {
|
|
79
|
+
expect(deriveSubagentModels("opencode/gemini-3.1-pro")).toEqual({
|
|
80
|
+
reviewer: undefined,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
it("openrouter gemini-pro inherits", () => {
|
|
84
|
+
expect(deriveSubagentModels("openrouter/google/gemini-3.1-pro-preview")).toEqual({
|
|
85
|
+
reviewer: undefined,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
it("flash has no downshift", () => {
|
|
89
|
+
expect(deriveSubagentModels("google/gemini-3-flash-preview")).toEqual({
|
|
90
|
+
reviewer: undefined,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("providers / models without a subagentModel — inherit", () => {
|
|
96
|
+
it("xai grok (already cheap flagship)", () => {
|
|
97
|
+
expect(deriveSubagentModels("xai/grok-4.3")).toEqual({ reviewer: undefined });
|
|
98
|
+
});
|
|
99
|
+
it("deepseek", () => {
|
|
100
|
+
expect(deriveSubagentModels("deepseek/deepseek-v4-pro")).toEqual({ reviewer: undefined });
|
|
101
|
+
});
|
|
102
|
+
it("moonshot kimi", () => {
|
|
103
|
+
expect(deriveSubagentModels("moonshotai/kimi-k2.6")).toEqual({ reviewer: undefined });
|
|
104
|
+
});
|
|
105
|
+
it("opencode big-pickle", () => {
|
|
106
|
+
expect(deriveSubagentModels("opencode/big-pickle")).toEqual({ reviewer: undefined });
|
|
107
|
+
});
|
|
108
|
+
it("legacy fallback aliases (gpt-codex, deepseek-reasoner)", () => {
|
|
109
|
+
expect(deriveSubagentModels("openai/gpt-5.3-codex")).toEqual({ reviewer: undefined });
|
|
110
|
+
expect(deriveSubagentModels("deepseek/deepseek-reasoner")).toEqual({ reviewer: undefined });
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { modelAliases } from "#app/models";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Derive a cheaper subagent model override from the orchestrator's resolved
|
|
5
|
+
* model spec.
|
|
6
|
+
*
|
|
7
|
+
* This is a pure registry lookup: every alias in `action/models.ts` declares
|
|
8
|
+
* its own `subagentModel` (alias key in the same provider). At runtime we
|
|
9
|
+
* reverse-lookup the orchestrator's resolved slug to find the alias that
|
|
10
|
+
* produced it, follow the `subagentModel` pointer, and return the target
|
|
11
|
+
* alias's resolve / openRouterResolve depending on which route the
|
|
12
|
+
* orchestrator was using.
|
|
13
|
+
*
|
|
14
|
+
* Returns `{ reviewer: undefined }` when the orchestrator's alias has no
|
|
15
|
+
* `subagentModel` (e.g. it's already at a sufficiently cheap tier, or its
|
|
16
|
+
* provider doesn't have a clean cheaper-but-capable sibling). See models.ts
|
|
17
|
+
* for the wiring + per-provider rationale.
|
|
18
|
+
*/
|
|
19
|
+
export function deriveSubagentModels(orchestratorSpec: string | undefined): {
|
|
20
|
+
reviewer: string | undefined;
|
|
21
|
+
} {
|
|
22
|
+
if (!orchestratorSpec) return { reviewer: undefined };
|
|
23
|
+
|
|
24
|
+
// Reverse-lookup. The same resolve string appears in only one alias
|
|
25
|
+
// (within its provider), so first match wins. We track which field
|
|
26
|
+
// matched (resolve vs openRouterResolve) so we can pick the same field
|
|
27
|
+
// off the subagent target — keeping the orchestrator's route consistent.
|
|
28
|
+
for (const source of modelAliases) {
|
|
29
|
+
const matchedDirect = source.resolve === orchestratorSpec;
|
|
30
|
+
const matchedOR = source.openRouterResolve === orchestratorSpec;
|
|
31
|
+
if (!matchedDirect && !matchedOR) continue;
|
|
32
|
+
if (!source.subagentModel) return { reviewer: undefined };
|
|
33
|
+
const target = modelAliases.find((a) => a.slug === source.subagentModel);
|
|
34
|
+
if (!target) return { reviewer: undefined };
|
|
35
|
+
const reviewer = matchedOR ? target.openRouterResolve : target.resolve;
|
|
36
|
+
return { reviewer };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { reviewer: undefined };
|
|
40
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
const claudeSource = readFileSync(join(__dirname, "claude.ts"), "utf-8");
|
|
6
|
+
const opencodeSharedSource = readFileSync(join(__dirname, "opencodeShared.ts"), "utf-8");
|
|
7
|
+
const opencodeSource = readFileSync(join(__dirname, "opencode.ts"), "utf-8");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The Claude Code `--agents` JSON and OpenCode `agent` config block are the
|
|
11
|
+
* only places where per-subagent model overrides take effect. They're built
|
|
12
|
+
* by string-only helpers we don't export, so this test reads the source and
|
|
13
|
+
* asserts the literal model strings + agent names are wired in. A regression
|
|
14
|
+
* here means the next review run silently runs lenses on Opus instead of
|
|
15
|
+
* Sonnet.
|
|
16
|
+
*/
|
|
17
|
+
describe("subagent registration source asserts", () => {
|
|
18
|
+
describe("claude.ts buildAgentsJson", () => {
|
|
19
|
+
it("registers review with sonnet model", () => {
|
|
20
|
+
expect(claudeSource).toMatch(
|
|
21
|
+
/\[REVIEWER_AGENT_NAME\]:\s*\{[^}]*model:\s*"claude-sonnet-4-6"/s,
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
it("imports the reviewer name constant", () => {
|
|
25
|
+
expect(claudeSource).toMatch(/REVIEWER_AGENT_NAME/);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("opencodeShared.ts buildReviewerAgentConfig", () => {
|
|
30
|
+
it("registers review with mode: subagent", () => {
|
|
31
|
+
expect(opencodeSharedSource).toMatch(/\[REVIEWER_AGENT_NAME\]:[^}]*mode:\s*"subagent"/s);
|
|
32
|
+
});
|
|
33
|
+
it("uses deriveSubagentModels for the reviewer model override", () => {
|
|
34
|
+
expect(opencodeSharedSource).toMatch(/deriveSubagentModels\(/);
|
|
35
|
+
expect(opencodeSharedSource).toMatch(/overrides\.reviewer/);
|
|
36
|
+
});
|
|
37
|
+
it("v2 runner passes orchestrator model to buildReviewerAgentConfig", () => {
|
|
38
|
+
expect(opencodeSource).toMatch(/buildReviewerAgentConfig\(model\)/);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for MCP tools subagents are forbidden from calling.
|
|
3
|
+
*
|
|
4
|
+
* Subagents share the orchestrator's in-process git working tree, `toolState`,
|
|
5
|
+
* progress comment, and run-scoped pr/branch context. A subagent that calls
|
|
6
|
+
* `checkout_pr` switches the orchestrator's HEAD; one that calls `push_branch`
|
|
7
|
+
* pushes whatever the orchestrator happens to have committed. The 2026-05-18
|
|
8
|
+
* `zed-industries/cloud` incident hit exactly this: a `reviewfrog` lens
|
|
9
|
+
* dispatched `checkout_pr({2582})` mid-review, the orchestrator's next push
|
|
10
|
+
* clobbered an unrelated engineer's branch. PR #796 added runtime backstops
|
|
11
|
+
* inside `checkout_pr`/`push_branch`; this list is the upstream gate that
|
|
12
|
+
* stops the call from ever reaching MCP when it originates from a subagent.
|
|
13
|
+
*
|
|
14
|
+
* The gate is enforced at two pre-tool hooks:
|
|
15
|
+
* - opencode: `tool.execute.before` (action/agents/opencodePlugin.ts)
|
|
16
|
+
* - claude: `PreToolUse` settings hook (action/agents/claudePretoolGate.ts)
|
|
17
|
+
*
|
|
18
|
+
* Names are stored in their canonical bare form (the FastMCP tool `name`
|
|
19
|
+
* field). Each runtime presents them with a different prefix:
|
|
20
|
+
* - claude: `mcp__terramend__<name>`
|
|
21
|
+
* - opencode: `terramend_<name>`
|
|
22
|
+
* The hooks strip those prefixes before comparing.
|
|
23
|
+
*
|
|
24
|
+
* Read-only MCP tools (`get_*`, `list_*`, `git_fetch`, `get_check_suite_logs`,
|
|
25
|
+
* `await_dependency_installation`, etc.) and the `git`/`shell` tools stay off
|
|
26
|
+
* this list — denying them would make review work impossible. The reviewer system prompt
|
|
27
|
+
* (`action/agents/reviewer.ts`) already forbids state-changing shell/git
|
|
28
|
+
* subcommands as a prose constraint; this list is the belt-and-suspenders
|
|
29
|
+
* machine fence for the high-stakes mutations we can identify by name alone.
|
|
30
|
+
*
|
|
31
|
+
* When adding a state-changing MCP tool to `action/mcp/server.ts`, add its
|
|
32
|
+
* canonical name here too. Inclusions justified inline.
|
|
33
|
+
*/
|
|
34
|
+
export const SUBAGENT_DENIED_TOOLS = [
|
|
35
|
+
// working-tree mutation: switches HEAD onto pr-N and registers a push remote
|
|
36
|
+
"checkout_pr",
|
|
37
|
+
|
|
38
|
+
// remote mutation: pushes commits / branches / tags / deletes a branch
|
|
39
|
+
"push_branch",
|
|
40
|
+
"push_tags",
|
|
41
|
+
"delete_branch",
|
|
42
|
+
|
|
43
|
+
// GitHub PR state mutation
|
|
44
|
+
"create_pull_request",
|
|
45
|
+
"update_pull_request_body",
|
|
46
|
+
// §27 — closes a (remediation) PR; a state-changing PR mutation.
|
|
47
|
+
"close_pull_request",
|
|
48
|
+
|
|
49
|
+
// GitHub comment / issue mutation
|
|
50
|
+
"create_issue",
|
|
51
|
+
"create_issue_comment",
|
|
52
|
+
"edit_issue_comment",
|
|
53
|
+
"reply_to_review_comment",
|
|
54
|
+
|
|
55
|
+
// GitHub review state mutation
|
|
56
|
+
"create_pull_request_review",
|
|
57
|
+
"resolve_review_thread",
|
|
58
|
+
|
|
59
|
+
// GitHub label mutation
|
|
60
|
+
"add_labels",
|
|
61
|
+
|
|
62
|
+
// run-state mutation: workflow output, progress comment, run mode select
|
|
63
|
+
"set_output",
|
|
64
|
+
"report_progress",
|
|
65
|
+
"select_mode",
|
|
66
|
+
|
|
67
|
+
// process / filesystem mutation outside the agent's intended scope
|
|
68
|
+
"start_dependency_installation",
|
|
69
|
+
"kill_background",
|
|
70
|
+
"upload_file",
|
|
71
|
+
] as const;
|
|
72
|
+
|
|
73
|
+
export type SubagentDeniedTool = (typeof SUBAGENT_DENIED_TOOLS)[number];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Strip the runtime-specific MCP prefix from a tool name and return the
|
|
77
|
+
* canonical bare name (matching FastMCP's `name:` field). Returns the input
|
|
78
|
+
* unchanged if it doesn't carry a known prefix — keeping comparison simple
|
|
79
|
+
* for native (non-MCP) tools, which never appear on the deny list anyway.
|
|
80
|
+
*/
|
|
81
|
+
export function stripMcpPrefix(toolName: string): string {
|
|
82
|
+
// claude: `mcp__terramend__checkout_pr` → `checkout_pr`
|
|
83
|
+
if (toolName.startsWith("mcp__terramend__")) return toolName.slice("mcp__terramend__".length);
|
|
84
|
+
// opencode: `terramend_checkout_pr` → `checkout_pr`
|
|
85
|
+
if (toolName.startsWith("terramend_")) return toolName.slice("terramend_".length);
|
|
86
|
+
return toolName;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whether `toolName` (in any runtime's prefix style) names a tool that
|
|
91
|
+
* subagents must not call.
|
|
92
|
+
*/
|
|
93
|
+
export function isSubagentDeniedTool(toolName: string): boolean {
|
|
94
|
+
const bare = stripMcpPrefix(toolName);
|
|
95
|
+
return (SUBAGENT_DENIED_TOOLS as readonly string[]).includes(bare);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Human-readable refusal surfaced to the model when a denied tool is gated.
|
|
100
|
+
* Phrased so a halfway-attentive subagent realises (a) the tool is denied to
|
|
101
|
+
* it specifically, (b) why (shared in-process state with the orchestrator),
|
|
102
|
+
* and (c) what to do instead (report findings; the orchestrator can call the
|
|
103
|
+
* tool directly).
|
|
104
|
+
*/
|
|
105
|
+
export function buildSubagentDenyMessage(toolName: string): string {
|
|
106
|
+
const bare = stripMcpPrefix(toolName);
|
|
107
|
+
return (
|
|
108
|
+
`subagent attempted to call denied tool '${bare}'. ` +
|
|
109
|
+
`subagents share the orchestrator's in-process working tree and toolState; ` +
|
|
110
|
+
`state-changing MCP tools (checkout_pr, push_branch, create_pull_request_review, ` +
|
|
111
|
+
`report_progress, etc.) are reserved for the orchestrator. ` +
|
|
112
|
+
`report findings back to the orchestrator and let it perform the mutation.`
|
|
113
|
+
);
|
|
114
|
+
}
|