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,582 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import { assertTargetInScope } from "#app/mcp/scope";
|
|
3
|
+
import type { ToolContext } from "#app/mcp/server";
|
|
4
|
+
import { execute, tool } from "#app/mcp/shared";
|
|
5
|
+
import { getApiUrl } from "#app/utils/apiUrl";
|
|
6
|
+
import { buildTerramendFooter, stripExistingFooter } from "#app/utils/buildTerramendFooter";
|
|
7
|
+
import { log } from "#app/utils/cli";
|
|
8
|
+
import { fixDoubleEscapedString } from "#app/utils/fixDoubleEscapedString";
|
|
9
|
+
import { patchWorkflowRunFields } from "#app/utils/patchWorkflowRunFields";
|
|
10
|
+
import {
|
|
11
|
+
createLeapingProgressComment,
|
|
12
|
+
deleteProgressCommentApi,
|
|
13
|
+
updateProgressComment,
|
|
14
|
+
} from "#app/utils/progressComment";
|
|
15
|
+
|
|
16
|
+
// re-export for backward compat with anything importing the leaping helpers from mcp/comment
|
|
17
|
+
export {
|
|
18
|
+
isLeapingIntoActionCommentBody,
|
|
19
|
+
LEAPING_INTO_ACTION_PREFIX,
|
|
20
|
+
} from "#app/utils/leapingComment";
|
|
21
|
+
|
|
22
|
+
function isNotFoundError(error: unknown): boolean {
|
|
23
|
+
return error instanceof Error && error.message.includes("Not Found");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildCommentFooter(ctx: ToolContext, customParts?: string[]): string {
|
|
27
|
+
return buildTerramendFooter({
|
|
28
|
+
triggeredBy: true,
|
|
29
|
+
customParts,
|
|
30
|
+
model: ctx.toolState.model,
|
|
31
|
+
fallbackFrom: ctx.toolState.modelFallback?.from,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildImplementPlanLink(ctx: ToolContext, issueNumber: number, commentId: number): string {
|
|
36
|
+
const apiUrl = getApiUrl();
|
|
37
|
+
return `[Implement plan ➔](${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${issueNumber}?action=implement&comment_id=${commentId})`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function addFooter(ctx: ToolContext, body: string): string {
|
|
41
|
+
if (/<br\s*\/?>[ \t]*\n(?!\s*\n)/i.test(body)) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
"body contains <br/> followed by a non-blank line, which breaks GitHub markdown rendering. always add a blank line after <br/> tags.",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
|
|
47
|
+
const footer = buildCommentFooter(ctx);
|
|
48
|
+
return `${bodyWithoutFooter}${footer}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const Comment = type({
|
|
52
|
+
issueNumber: type.number.describe("the issue number to comment on"),
|
|
53
|
+
body: type.string.describe("the comment body content"),
|
|
54
|
+
type: type
|
|
55
|
+
.enumerated("Plan", "Comment")
|
|
56
|
+
.describe("Plan: record as the plan for this run. Comment: regular comment (default).")
|
|
57
|
+
.optional(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export function CreateCommentTool(ctx: ToolContext) {
|
|
61
|
+
return tool({
|
|
62
|
+
name: "create_issue_comment",
|
|
63
|
+
description:
|
|
64
|
+
"Create a comment on a GitHub issue or PR. " +
|
|
65
|
+
'Example: `create_issue_comment({ issueNumber: 1234, body: "Thanks for the report." })`. ' +
|
|
66
|
+
"For progress/plan updates on the current run use report_progress instead — plan output (initial post AND revisions) is always posted via report_progress, never via this tool.",
|
|
67
|
+
parameters: Comment,
|
|
68
|
+
execute: execute(async ({ issueNumber, body, type: commentType }) => {
|
|
69
|
+
assertTargetInScope(ctx, issueNumber, "comment on");
|
|
70
|
+
const bodyWithFooter = addFooter(ctx, body);
|
|
71
|
+
|
|
72
|
+
const result = await ctx.octokit.rest.issues.createComment({
|
|
73
|
+
owner: ctx.repo.owner,
|
|
74
|
+
repo: ctx.repo.name,
|
|
75
|
+
issue_number: issueNumber,
|
|
76
|
+
body: bodyWithFooter,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
ctx.toolState.wasUpdated = true;
|
|
80
|
+
log.info(`» created comment ${result.data.id}`);
|
|
81
|
+
|
|
82
|
+
if (commentType === "Plan") {
|
|
83
|
+
if (result.data.node_id) {
|
|
84
|
+
await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
|
|
85
|
+
}
|
|
86
|
+
// add "Implement plan" link (needs comment ID, so create-then-update)
|
|
87
|
+
const customParts = [buildImplementPlanLink(ctx, issueNumber, result.data.id)];
|
|
88
|
+
const footer = buildCommentFooter(ctx, customParts);
|
|
89
|
+
const bodyWithPlanLink = `${stripExistingFooter(body)}${footer}`;
|
|
90
|
+
|
|
91
|
+
const updateResult = await ctx.octokit.rest.issues.updateComment({
|
|
92
|
+
owner: ctx.repo.owner,
|
|
93
|
+
repo: ctx.repo.name,
|
|
94
|
+
comment_id: result.data.id,
|
|
95
|
+
body: bodyWithPlanLink,
|
|
96
|
+
});
|
|
97
|
+
log.info(`» updated comment ${updateResult.data.id}`);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
success: true,
|
|
101
|
+
commentId: updateResult.data.id,
|
|
102
|
+
url: updateResult.data.html_url,
|
|
103
|
+
body: updateResult.data.body,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
success: true,
|
|
109
|
+
commentId: result.data.id,
|
|
110
|
+
url: result.data.html_url,
|
|
111
|
+
body: result.data.body,
|
|
112
|
+
};
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const EditComment = type({
|
|
118
|
+
commentId: type.number.describe("the ID of the comment to edit"),
|
|
119
|
+
body: type.string.describe("the new comment body content"),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/** parse the issue/PR number a comment belongs to from its API `issue_url`
|
|
123
|
+
* (`https://api.github.com/repos/o/r/issues/123`). undefined when unparseable. */
|
|
124
|
+
function issueNumberFromUrl(url: string | undefined): number | undefined {
|
|
125
|
+
const m = url?.match(/\/issues\/(\d+)(?:$|[/?#])/);
|
|
126
|
+
return m ? Number(m[1]) : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* SECURITY: bind edit_issue_comment to the run's scoped issue/PR. A comment id
|
|
131
|
+
* is repo-global, so resolve it to the issue/PR it lives on first (an injected
|
|
132
|
+
* agent must not be able to overwrite a comment on an UNRELATED issue/PR).
|
|
133
|
+
* Skips for standalone runs and, deliberately, fails OPEN when the lookup can't
|
|
134
|
+
* determine the issue — editing a comment is reversible and low-risk, so a
|
|
135
|
+
* transient API/permission hiccup must not block a legitimate edit. A definite
|
|
136
|
+
* out-of-scope match still throws.
|
|
137
|
+
*/
|
|
138
|
+
async function assertCommentInScope(ctx: ToolContext, commentId: number): Promise<void> {
|
|
139
|
+
if (ctx.payload?.event?.issue_number === undefined) return;
|
|
140
|
+
let issueNumber: number | undefined;
|
|
141
|
+
try {
|
|
142
|
+
const { data } = await ctx.octokit.rest.issues.getComment({
|
|
143
|
+
owner: ctx.repo.owner,
|
|
144
|
+
repo: ctx.repo.name,
|
|
145
|
+
comment_id: commentId,
|
|
146
|
+
});
|
|
147
|
+
issueNumber = issueNumberFromUrl(data.issue_url);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
log.debug(
|
|
150
|
+
`edit_issue_comment scope lookup failed (allowing): ${e instanceof Error ? e.message : String(e)}`,
|
|
151
|
+
);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (issueNumber !== undefined) {
|
|
155
|
+
assertTargetInScope(ctx, issueNumber, "edit a comment on");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function EditCommentTool(ctx: ToolContext) {
|
|
160
|
+
return tool({
|
|
161
|
+
name: "edit_issue_comment",
|
|
162
|
+
description: "Edit a GitHub issue comment by its ID",
|
|
163
|
+
parameters: EditComment,
|
|
164
|
+
execute: execute(async ({ commentId, body }) => {
|
|
165
|
+
await assertCommentInScope(ctx, commentId);
|
|
166
|
+
const bodyWithFooter = addFooter(ctx, body);
|
|
167
|
+
|
|
168
|
+
const result = await ctx.octokit.rest.issues.updateComment({
|
|
169
|
+
owner: ctx.repo.owner,
|
|
170
|
+
repo: ctx.repo.name,
|
|
171
|
+
comment_id: commentId,
|
|
172
|
+
body: bodyWithFooter,
|
|
173
|
+
});
|
|
174
|
+
log.info(`» updated comment ${result.data.id}`);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
success: true,
|
|
178
|
+
commentId: result.data.id,
|
|
179
|
+
url: result.data.html_url,
|
|
180
|
+
body: result.data.body,
|
|
181
|
+
updatedAt: result.data.updated_at,
|
|
182
|
+
};
|
|
183
|
+
}),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const ReportProgress = type({
|
|
188
|
+
body: type.string.describe("the progress update content to share"),
|
|
189
|
+
"target_plan_comment?": type("boolean").describe(
|
|
190
|
+
"for revising an existing plan comment ONLY. set to true only when the PlanEdit checklist from select_mode tells you to (i.e. a prior plan comment was found for this issue). NEVER set on the initial plan post — the initial plan reuses the run's progress comment and is posted by calling report_progress without this flag.",
|
|
191
|
+
),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Report progress to a GitHub comment.
|
|
196
|
+
*
|
|
197
|
+
* progressComment has three states:
|
|
198
|
+
* - undefined: no comment yet — will create one if an issue/PR target exists
|
|
199
|
+
* - object: active comment — will update it in place via the right REST endpoint for its type
|
|
200
|
+
* - null: deliberately deleted (e.g. after submitting a PR review) — skips silently
|
|
201
|
+
*
|
|
202
|
+
* The body is tracked in lastProgressBody for the job summary regardless of comment state,
|
|
203
|
+
* EXCEPT for `liveProgress` (todo-tracker) writes — see the param note below.
|
|
204
|
+
*
|
|
205
|
+
* The "existing plan comment" path always targets a top-level issue comment (plan comments are
|
|
206
|
+
* created by create_issue_comment with type:"Plan", never as review-thread replies).
|
|
207
|
+
*/
|
|
208
|
+
export async function reportProgress(
|
|
209
|
+
ctx: ToolContext,
|
|
210
|
+
params: { body: string; target_plan_comment?: boolean; liveProgress?: boolean },
|
|
211
|
+
): Promise<{
|
|
212
|
+
commentId?: number;
|
|
213
|
+
url?: string;
|
|
214
|
+
body: string;
|
|
215
|
+
action: "created" | "updated" | "skipped";
|
|
216
|
+
}> {
|
|
217
|
+
const { body, target_plan_comment } = params;
|
|
218
|
+
// `liveProgress` marks the automatic todo-tracker checklist render — a live
|
|
219
|
+
// progress update, NOT the agent's deliberate final answer. such writes must
|
|
220
|
+
// not record `lastProgressBody` or flip `wasUpdated`: both signal "a real
|
|
221
|
+
// user-facing answer landed", and letting an auto checklist trip them masks
|
|
222
|
+
// the #868 salvage (it would post the checklist instead of the real output,
|
|
223
|
+
// or skip salvage entirely) and triggers stranded-comment deletion.
|
|
224
|
+
if (!params.liveProgress) {
|
|
225
|
+
ctx.toolState.lastProgressBody = body;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// silent events (e.g., auto-label, pr-summary Task) should never create or update progress comments.
|
|
229
|
+
// the body is still tracked above for the GitHub Actions job summary.
|
|
230
|
+
if (ctx.payload.event.silent) {
|
|
231
|
+
return { body, action: "skipped" };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const issueNumber = ctx.payload.event.issue_number ?? ctx.toolState.issueNumber;
|
|
235
|
+
const isPlanMode = ctx.toolState.selectedMode === "Plan";
|
|
236
|
+
const apiCtx = { octokit: ctx.octokit, owner: ctx.repo.owner, repo: ctx.repo.name };
|
|
237
|
+
|
|
238
|
+
// when editing existing plan: update the plan comment from tool state (set by select_mode)
|
|
239
|
+
if (target_plan_comment === true && ctx.toolState.existingPlanCommentId === undefined) {
|
|
240
|
+
log.warning("target_plan_comment requested but no existingPlanCommentId in tool state");
|
|
241
|
+
}
|
|
242
|
+
if (target_plan_comment === true && ctx.toolState.existingPlanCommentId !== undefined) {
|
|
243
|
+
const commentId = ctx.toolState.existingPlanCommentId;
|
|
244
|
+
const customParts =
|
|
245
|
+
issueNumber !== undefined ? [buildImplementPlanLink(ctx, issueNumber, commentId)] : undefined;
|
|
246
|
+
const bodyWithoutFooter = stripExistingFooter(body);
|
|
247
|
+
const footer = buildCommentFooter(ctx, customParts);
|
|
248
|
+
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
|
|
249
|
+
|
|
250
|
+
const result = await updateProgressComment(
|
|
251
|
+
apiCtx,
|
|
252
|
+
{ id: commentId, type: "issue" },
|
|
253
|
+
bodyWithFooter,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
if (!params.liveProgress) ctx.toolState.wasUpdated = true;
|
|
257
|
+
|
|
258
|
+
if (isPlanMode && result.node_id) {
|
|
259
|
+
await patchWorkflowRunFields(ctx, { planCommentNodeId: result.node_id });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
commentId: result.id,
|
|
264
|
+
url: result.html_url,
|
|
265
|
+
body: result.body || "",
|
|
266
|
+
action: "updated",
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const existingComment = ctx.toolState.progressComment;
|
|
271
|
+
|
|
272
|
+
// if we already have a progress comment, update it
|
|
273
|
+
if (existingComment) {
|
|
274
|
+
const customParts =
|
|
275
|
+
isPlanMode && issueNumber !== undefined
|
|
276
|
+
? [buildImplementPlanLink(ctx, issueNumber, existingComment.id)]
|
|
277
|
+
: undefined;
|
|
278
|
+
|
|
279
|
+
const bodyWithoutFooter = stripExistingFooter(body);
|
|
280
|
+
const footer = buildCommentFooter(ctx, customParts);
|
|
281
|
+
const bodyWithFooter = `${bodyWithoutFooter}${footer}`;
|
|
282
|
+
|
|
283
|
+
// a review-reply progress comment (seeded by the dispatch path) can become
|
|
284
|
+
// stale before final delivery — the thread is deleted or otherwise
|
|
285
|
+
// unreachable, so updateProgressComment 404s. rather than fail an
|
|
286
|
+
// already-completed run, fall back to a fresh top-level comment on the PR
|
|
287
|
+
// and retarget future writes there.
|
|
288
|
+
let result: Awaited<ReturnType<typeof updateProgressComment>>;
|
|
289
|
+
try {
|
|
290
|
+
result = await updateProgressComment(apiCtx, existingComment, bodyWithFooter);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
// only a deliberate write to a stale review-reply comment falls back. a
|
|
293
|
+
// liveProgress (todo-tracker) 404 rethrows — it must never create a
|
|
294
|
+
// user-facing comment, and the next deliberate report_progress recovers.
|
|
295
|
+
if (
|
|
296
|
+
params.liveProgress ||
|
|
297
|
+
existingComment.type !== "review" ||
|
|
298
|
+
!isNotFoundError(error) ||
|
|
299
|
+
issueNumber === undefined
|
|
300
|
+
) {
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
log.warning(
|
|
304
|
+
`progress review comment ${existingComment.id} is gone (404); posting a top-level comment on #${issueNumber} instead`,
|
|
305
|
+
);
|
|
306
|
+
const created = await createLeapingProgressComment(
|
|
307
|
+
apiCtx,
|
|
308
|
+
{ kind: "issue", issueNumber },
|
|
309
|
+
bodyWithFooter,
|
|
310
|
+
);
|
|
311
|
+
ctx.toolState.progressComment = created.comment;
|
|
312
|
+
if (!params.liveProgress) ctx.toolState.wasUpdated = true;
|
|
313
|
+
return {
|
|
314
|
+
commentId: created.comment.id,
|
|
315
|
+
url: created.html_url,
|
|
316
|
+
body: created.body || "",
|
|
317
|
+
action: "created",
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!params.liveProgress) ctx.toolState.wasUpdated = true;
|
|
322
|
+
|
|
323
|
+
if (isPlanMode && result.node_id) {
|
|
324
|
+
await patchWorkflowRunFields(ctx, { planCommentNodeId: result.node_id });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
commentId: result.id,
|
|
329
|
+
url: result.html_url,
|
|
330
|
+
body: result.body || "",
|
|
331
|
+
action: "updated",
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// null = progress comment was deleted by stranded-comment cleanup in main.ts
|
|
336
|
+
if (existingComment === null) {
|
|
337
|
+
return { body, action: "skipped" };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// no existing comment - need an issue/PR to create one on
|
|
341
|
+
// use fallback chain: dynamically set context > event payload
|
|
342
|
+
if (issueNumber === undefined) {
|
|
343
|
+
// no-op: no comment target (e.g., workflow_dispatch events)
|
|
344
|
+
// body is already tracked for job summary
|
|
345
|
+
return { body, action: "skipped" };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// for new comments, we need to create first, then update with Plan link if in Plan mode
|
|
349
|
+
// self-created progress comments are always top-level issue comments — review-reply
|
|
350
|
+
// progress comments only originate from the dispatch path and arrive pre-created.
|
|
351
|
+
const initialBody = addFooter(ctx, body);
|
|
352
|
+
const created = await createLeapingProgressComment(
|
|
353
|
+
apiCtx,
|
|
354
|
+
{ kind: "issue", issueNumber },
|
|
355
|
+
initialBody,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
ctx.toolState.progressComment = created.comment;
|
|
359
|
+
if (!params.liveProgress) ctx.toolState.wasUpdated = true;
|
|
360
|
+
|
|
361
|
+
// if Plan mode, update the comment to add the "Implement plan" link
|
|
362
|
+
if (isPlanMode) {
|
|
363
|
+
const customParts = [buildImplementPlanLink(ctx, issueNumber, created.comment.id)];
|
|
364
|
+
const bodyWithoutFooter = stripExistingFooter(body);
|
|
365
|
+
const footer = buildCommentFooter(ctx, customParts);
|
|
366
|
+
const bodyWithPlanLink = `${bodyWithoutFooter}${footer}`;
|
|
367
|
+
|
|
368
|
+
const updateResult = await updateProgressComment(apiCtx, created.comment, bodyWithPlanLink);
|
|
369
|
+
|
|
370
|
+
if (updateResult.node_id) {
|
|
371
|
+
await patchWorkflowRunFields(ctx, { planCommentNodeId: updateResult.node_id });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
commentId: updateResult.id,
|
|
376
|
+
url: updateResult.html_url,
|
|
377
|
+
body: updateResult.body || "",
|
|
378
|
+
action: "created",
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
commentId: created.comment.id,
|
|
384
|
+
url: created.html_url,
|
|
385
|
+
body: created.body || "",
|
|
386
|
+
action: "created",
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function ReportProgressTool(ctx: ToolContext) {
|
|
391
|
+
return tool({
|
|
392
|
+
name: "report_progress",
|
|
393
|
+
description:
|
|
394
|
+
"Share progress on the associated GitHub issue/PR. The first call creates a comment; subsequent calls update it in place. " +
|
|
395
|
+
'Example: `report_progress({ body: "Implemented the auth check and added tests." })`. ' +
|
|
396
|
+
"Call this at the end of every run with a brief final summary (1-3 sentences) unless the mode guidance instructs otherwise. The current task list is automatically appended in a collapsible section — do not restate individual steps.",
|
|
397
|
+
parameters: ReportProgress,
|
|
398
|
+
execute: execute(async (params) => {
|
|
399
|
+
let body = params.body;
|
|
400
|
+
|
|
401
|
+
// for non-plan calls: stop auto-updates, wait for in-flight writes to settle,
|
|
402
|
+
// then append completed task list collapsible
|
|
403
|
+
if (!params.target_plan_comment && ctx.toolState.todoTracker) {
|
|
404
|
+
ctx.toolState.todoTracker.cancel();
|
|
405
|
+
await ctx.toolState.todoTracker.settled();
|
|
406
|
+
const collapsible = ctx.toolState.todoTracker.renderCollapsible({
|
|
407
|
+
completeInProgress: true,
|
|
408
|
+
});
|
|
409
|
+
if (collapsible) {
|
|
410
|
+
body = `${body}\n\n${collapsible}`;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const reportParams: { body: string; target_plan_comment?: boolean } = { body };
|
|
415
|
+
if (params.target_plan_comment !== undefined) {
|
|
416
|
+
reportParams.target_plan_comment = params.target_plan_comment;
|
|
417
|
+
}
|
|
418
|
+
const result = await reportProgress(ctx, reportParams);
|
|
419
|
+
|
|
420
|
+
if (result.action === "skipped") {
|
|
421
|
+
return {
|
|
422
|
+
success: true,
|
|
423
|
+
message:
|
|
424
|
+
"progress recorded (no GitHub comment created - this may occur for workflow_dispatch events or when there is no associated issue/PR)",
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (result.commentId !== undefined) {
|
|
429
|
+
log.info(`» ${result.action} comment ${result.commentId}`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!params.target_plan_comment) {
|
|
433
|
+
ctx.toolState.finalSummaryWritten = true;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
success: true,
|
|
438
|
+
...result,
|
|
439
|
+
};
|
|
440
|
+
}),
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Delete the progress comment if it exists.
|
|
446
|
+
* Used by main.ts for stranded-comment cleanup (orphaned "Leaping into action" or
|
|
447
|
+
* checklist left by the todo tracker when the agent didn't call report_progress).
|
|
448
|
+
* Sets progressComment to null so subsequent report_progress calls are no-ops.
|
|
449
|
+
*/
|
|
450
|
+
export async function deleteProgressComment(ctx: ToolContext): Promise<boolean> {
|
|
451
|
+
const existing = ctx.toolState.progressComment;
|
|
452
|
+
if (!existing) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
await deleteProgressCommentApi(
|
|
458
|
+
{ octokit: ctx.octokit, owner: ctx.repo.owner, repo: ctx.repo.name },
|
|
459
|
+
existing,
|
|
460
|
+
);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
// ignore 404 - comment already deleted
|
|
463
|
+
if (!isNotFoundError(error)) throw error;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// set to null (not undefined) so report_progress skips instead of creating a new comment
|
|
467
|
+
ctx.toolState.progressComment = null;
|
|
468
|
+
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export const ReplyToReviewComment = type({
|
|
473
|
+
pull_number: type.number.describe("the pull request number"),
|
|
474
|
+
comment_id: type.number.describe("the ID of the review comment to reply to"),
|
|
475
|
+
body: type.string.describe(
|
|
476
|
+
"extremely brief reply (1 sentence max) explaining what was fixed, e.g. 'Fixed by renaming to X' or 'Added null check'",
|
|
477
|
+
),
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* decision returned by `duplicateReplyDecision` when a session has already
|
|
482
|
+
* posted an identical reply to the same parent review comment.
|
|
483
|
+
*/
|
|
484
|
+
export interface DuplicateReplyDecision {
|
|
485
|
+
kind: "already-replied";
|
|
486
|
+
commentId: number;
|
|
487
|
+
url: string | undefined;
|
|
488
|
+
reason: string;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* decide whether a second reply_to_review_comment call in the same session
|
|
493
|
+
* is a duplicate of an earlier reply to the same parent comment.
|
|
494
|
+
*
|
|
495
|
+
* the agent is instructed to call reply_to_review_comment exactly once per
|
|
496
|
+
* parent comment per AddressReviews session, but in practice it sometimes
|
|
497
|
+
* emits the same call twice. PR #610 reproduced this with Kimi K2:
|
|
498
|
+
* identical body posted 3 seconds apart, only one tool_use event in the
|
|
499
|
+
* agent log. the second post is always redundant and clutters the PR thread.
|
|
500
|
+
*
|
|
501
|
+
* we key on (comment_id, bodyWithFooter) so a legitimate follow-up reply
|
|
502
|
+
* with different content still goes through. within a single run the
|
|
503
|
+
* footer is constant (workflow run + model + jobId), so byte-equal bodies
|
|
504
|
+
* catch the stutter without blocking real follow-ups.
|
|
505
|
+
*
|
|
506
|
+
* mirrors the shape of `duplicateReviewDecision` in mcp/review.ts.
|
|
507
|
+
*/
|
|
508
|
+
export function duplicateReplyDecision(params: {
|
|
509
|
+
existing: { commentId: number; url: string | undefined; bodyWithFooter: string } | undefined;
|
|
510
|
+
bodyWithFooter: string;
|
|
511
|
+
}): DuplicateReplyDecision | null {
|
|
512
|
+
const existing = params.existing;
|
|
513
|
+
if (!existing) return null;
|
|
514
|
+
if (existing.bodyWithFooter !== params.bodyWithFooter) return null;
|
|
515
|
+
return {
|
|
516
|
+
kind: "already-replied",
|
|
517
|
+
commentId: existing.commentId,
|
|
518
|
+
url: existing.url,
|
|
519
|
+
reason: `reply ${existing.commentId} with identical body was already posted in this session; ignoring duplicate call`,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function ReplyToReviewCommentTool(ctx: ToolContext) {
|
|
524
|
+
return tool({
|
|
525
|
+
name: "reply_to_review_comment",
|
|
526
|
+
description:
|
|
527
|
+
"Reply to a PR review comment thread (NOT issue comments — this only works for inline review comments on PR diffs). " +
|
|
528
|
+
'Example: `reply_to_review_comment({ pull_number: 1234, comment_id: 567890, body: "Fixed by adding a null check." })`. ' +
|
|
529
|
+
"Call exactly ONCE per parent comment you address in AddressReviews mode — duplicate calls with the same body are a no-op. Keep replies extremely brief (1 sentence max).",
|
|
530
|
+
parameters: ReplyToReviewComment,
|
|
531
|
+
execute: execute(async ({ pull_number, comment_id, body }) => {
|
|
532
|
+
assertTargetInScope(ctx, pull_number, "reply to a review comment on");
|
|
533
|
+
const bodyWithFooter = addFooter(ctx, body);
|
|
534
|
+
|
|
535
|
+
// guard against duplicate reply submissions in the same session.
|
|
536
|
+
// see duplicateReplyDecision for the rationale.
|
|
537
|
+
const dup = duplicateReplyDecision({
|
|
538
|
+
existing: ctx.toolState.reviewReplies?.get(comment_id),
|
|
539
|
+
bodyWithFooter,
|
|
540
|
+
});
|
|
541
|
+
if (dup) {
|
|
542
|
+
log.info(`skipping duplicate review reply: ${dup.reason}`);
|
|
543
|
+
return {
|
|
544
|
+
success: true,
|
|
545
|
+
skipped: true,
|
|
546
|
+
reason: dup.reason,
|
|
547
|
+
commentId: dup.commentId,
|
|
548
|
+
url: dup.url,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const result = await ctx.octokit.rest.pulls.createReplyForReviewComment({
|
|
553
|
+
owner: ctx.repo.owner,
|
|
554
|
+
repo: ctx.repo.name,
|
|
555
|
+
pull_number,
|
|
556
|
+
comment_id,
|
|
557
|
+
body: bodyWithFooter,
|
|
558
|
+
});
|
|
559
|
+
log.info(`» created review comment ${result.data.id} (in reply to ${comment_id})`);
|
|
560
|
+
|
|
561
|
+
// mark progress as updated so error reporting + run-result handling know
|
|
562
|
+
// a substantive write happened (used by reportErrorToComment / handleAgentResult)
|
|
563
|
+
ctx.toolState.wasUpdated = true;
|
|
564
|
+
|
|
565
|
+
// record this reply for in-session dedupe of subsequent identical calls.
|
|
566
|
+
ctx.toolState.reviewReplies ??= new Map();
|
|
567
|
+
ctx.toolState.reviewReplies.set(comment_id, {
|
|
568
|
+
commentId: result.data.id,
|
|
569
|
+
url: result.data.html_url,
|
|
570
|
+
bodyWithFooter,
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
return {
|
|
574
|
+
success: true,
|
|
575
|
+
commentId: result.data.id,
|
|
576
|
+
url: result.data.html_url,
|
|
577
|
+
body: result.data.body,
|
|
578
|
+
in_reply_to_id: result.data.in_reply_to_id,
|
|
579
|
+
};
|
|
580
|
+
}, "reply_to_review_comment"),
|
|
581
|
+
});
|
|
582
|
+
}
|