infraweaver 0.3.7 → 0.3.8
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/README.md +2 -2
- package/dist/agents/claude.d.ts +1 -1
- package/dist/agents/claudePretoolGate.d.ts +0 -36
- package/dist/agents/toolPlaneWatch.d.ts +31 -0
- package/dist/cli.mjs +31903 -18736
- package/dist/external.d.ts +2 -1
- package/dist/index.js +31987 -18749
- package/dist/internal.js +132 -52
- package/dist/mcp/baseBranch.d.ts +41 -0
- package/dist/mcp/changeSummary.d.ts +1 -1
- package/dist/mcp/findingAudit.d.ts +2 -0
- package/dist/mcp/guardrails.d.ts +38 -0
- package/dist/mcp/localContext.d.ts +1 -1
- package/dist/mcp/moduleExtraction.d.ts +5 -0
- package/dist/mcp/pr.d.ts +0 -31
- package/dist/mcp/reproveSuggestions.d.ts +27 -0
- package/dist/mcp/review.d.ts +126 -2
- package/dist/mcp/reviewCommentLimit.d.ts +17 -0
- package/dist/mcp/reviewComments.d.ts +9 -5
- package/dist/mcp/reviewDedup.d.ts +1 -0
- package/dist/mcp/reviewMarkers.d.ts +10 -0
- package/dist/mcp/reviewProvenance.d.ts +20 -6
- package/dist/mcp/shared.d.ts +15 -0
- package/dist/mcp/suggestionProver.d.ts +3 -2
- package/dist/mcp/terraform/checkovSeverities.d.ts +7 -0
- package/dist/mcp/terraform/consolidation.d.ts +26 -0
- package/dist/mcp/terraform/cost.d.ts +2 -2
- package/dist/mcp/terraform/currency.d.ts +11 -0
- package/dist/mcp/terraform/deltaSummary.d.ts +11 -1
- package/dist/mcp/terraform/emittedArtefacts.d.ts +11 -0
- package/dist/mcp/terraform/evidence.d.ts +10 -0
- package/dist/mcp/terraform/gateScan.d.ts +2 -16
- package/dist/mcp/terraform/hcl.d.ts +15 -0
- package/dist/mcp/terraform/initArtefacts.d.ts +21 -0
- package/dist/mcp/terraform/localModules.d.ts +29 -0
- package/dist/mcp/terraform/moduleFindings.d.ts +38 -0
- package/dist/mcp/terraform/refactor/attribution.d.ts +1 -1
- package/dist/mcp/terraform/refactor/equivalence.d.ts +7 -0
- package/dist/mcp/terraform/refactor/expressions.d.ts +62 -0
- package/dist/mcp/terraform/refactor/references.d.ts +15 -0
- package/dist/mcp/terraform/refactor/resources.d.ts +11 -0
- package/dist/mcp/terraform/refactor/substitution.d.ts +55 -37
- package/dist/mcp/terraform/scanDelta.d.ts +28 -0
- package/dist/mcp/terraform/scanSession.d.ts +13 -0
- package/dist/mcp/terraform/scanners.d.ts +67 -18
- package/dist/mcp/terraform/secretScan.d.ts +19 -0
- package/dist/mcp/terraform/standardsReport.d.ts +1 -1
- package/dist/mcp/terraform/tools/plan.d.ts +1 -1
- package/dist/mcp/terraform/tools/scan.d.ts +7 -7
- package/dist/mcp/terraform/tools/validate.d.ts +7 -2
- package/dist/mcp/terraform/tools/verifyRemediation.d.ts +1 -1
- package/dist/mcp/terraform/treeLayout.d.ts +57 -0
- package/dist/mcp/terraform/types.d.ts +1 -1
- package/dist/phases/applyPlan.d.ts +0 -2
- package/dist/toolState.d.ts +3 -0
- package/dist/utils/agent/codexOAuth.d.ts +1 -1
- package/dist/utils/cli.d.ts +1 -7
- package/dist/utils/cloud/cloudReport.d.ts +1 -1
- package/dist/utils/config/baseRefConfig.d.ts +1 -1
- package/dist/utils/config/infraweaverConfig.d.ts +1 -1
- package/dist/utils/config/payload.d.ts +5 -0
- package/dist/utils/markdownTable.d.ts +6 -0
- package/dist/utils/setup/toolLicensing.d.ts +1 -1
- package/dist/utils/unifiedDiff.d.ts +69 -0
- package/package.json +34 -28
- package/src/agents/claude.ts +48 -13
- package/src/agents/claudePretoolGate.ts +9 -17
- package/src/agents/sessionLabeler.ts +3 -3
- package/src/agents/toolPlaneWatch.ts +71 -0
- package/src/external.ts +3 -1
- package/src/mcp/assess.ts +8 -8
- package/src/mcp/baseBranch.ts +96 -0
- package/src/mcp/changeSummary.ts +10 -11
- package/src/mcp/checkSuite.ts +7 -10
- package/src/mcp/checkout.ts +13 -40
- package/src/mcp/crosswalk.ts +5 -0
- package/src/mcp/findingAudit.ts +1 -1
- package/src/mcp/git.ts +44 -2
- package/src/mcp/guardrails.ts +188 -36
- package/src/mcp/localContext.ts +2 -0
- package/src/mcp/localServer.ts +1 -0
- package/src/mcp/moduleExtraction.ts +17 -5
- package/src/mcp/moduleTests.ts +9 -2
- package/src/mcp/modules.ts +10 -7
- package/src/mcp/pr.ts +17 -65
- package/src/mcp/reproveSuggestions.ts +86 -0
- package/src/mcp/review.ts +249 -75
- package/src/mcp/reviewAnchor.ts +11 -23
- package/src/mcp/reviewCommentLimit.ts +79 -0
- package/src/mcp/reviewComments.ts +34 -81
- package/src/mcp/reviewDedup.ts +1 -1
- package/src/mcp/reviewMarkers.ts +25 -2
- package/src/mcp/reviewProvenance.ts +56 -21
- package/src/mcp/roots.ts +3 -2
- package/src/mcp/server.ts +2 -1
- package/src/mcp/shared.ts +40 -1
- package/src/mcp/shell.ts +27 -28
- package/src/mcp/suggestionProver.ts +8 -4
- package/src/mcp/terraform/checkovSeverities.ts +1156 -0
- package/src/mcp/terraform/concernResult.ts +2 -1
- package/src/mcp/terraform/consolidation.ts +232 -19
- package/src/mcp/terraform/cost.ts +16 -7
- package/src/mcp/terraform/cspm.ts +8 -4
- package/src/mcp/terraform/currency.ts +28 -12
- package/src/mcp/terraform/decisions.ts +1 -1
- package/src/mcp/terraform/deltaSummary.ts +50 -1
- package/src/mcp/terraform/emittedArtefacts.ts +63 -0
- package/src/mcp/terraform/evidence.ts +23 -3
- package/src/mcp/terraform/gateScan.ts +191 -128
- package/src/mcp/terraform/hcl.ts +27 -2
- package/src/mcp/terraform/idleFloor.ts +2 -1
- package/src/mcp/terraform/initArtefacts.ts +76 -0
- package/src/mcp/terraform/localModules.ts +112 -0
- package/src/mcp/terraform/moduleDocs.ts +2 -5
- package/src/mcp/terraform/moduleFindings.ts +102 -0
- package/src/mcp/terraform/moduleVersionConstraints.ts +1 -1
- package/src/mcp/terraform/planPairs.ts +2 -20
- package/src/mcp/terraform/policyGate.ts +0 -15
- package/src/mcp/terraform/refactor/attribution.ts +39 -5
- package/src/mcp/terraform/refactor/equivalence.ts +49 -10
- package/src/mcp/terraform/refactor/expressions.ts +323 -0
- package/src/mcp/terraform/refactor/references.ts +101 -0
- package/src/mcp/terraform/refactor/resources.ts +79 -7
- package/src/mcp/terraform/refactor/substitution.ts +852 -256
- package/src/mcp/terraform/scanDelta.ts +34 -0
- package/src/mcp/terraform/scanSession.ts +30 -0
- package/src/mcp/terraform/scannerCache.ts +4 -0
- package/src/mcp/terraform/scanners.ts +553 -233
- package/src/mcp/terraform/secretScan.ts +131 -0
- package/src/mcp/terraform/standardsReport.ts +14 -13
- package/src/mcp/terraform/tools/consolidationCandidates.ts +3 -0
- package/src/mcp/terraform/tools/emitEvidence.ts +13 -5
- package/src/mcp/terraform/tools/emitOscal.ts +3 -2
- package/src/mcp/terraform/tools/emitVex.ts +10 -4
- package/src/mcp/terraform/tools/equivalenceCheck.ts +139 -21
- package/src/mcp/terraform/tools/infracostDiff.ts +2 -2
- package/src/mcp/terraform/tools/plan.ts +30 -17
- package/src/mcp/terraform/tools/reviewScanDelta.ts +3 -0
- package/src/mcp/terraform/tools/scan.ts +5 -3
- package/src/mcp/terraform/tools/standardsReport.ts +1 -1
- package/src/mcp/terraform/tools/validate.ts +20 -5
- package/src/mcp/terraform/tools/verifyRemediation.ts +14 -7
- package/src/mcp/terraform/tools/versionCurrency.ts +3 -1
- package/src/mcp/terraform/treeLayout.ts +378 -0
- package/src/mcp/terraform/types.ts +4 -1
- package/src/mcp/terraform/versionRequirements.ts +11 -2
- package/src/modes/__snapshots__/assemblePrompt.test.ts.snap +2 -0
- package/src/modes/modernize-deprecated.ts +2 -2
- package/src/modes/refactor.ts +9 -9
- package/src/modes/remediate.ts +1 -1
- package/src/modes/terraform-code-review.ts +6 -3
- package/src/modes/update-dependencies.ts +6 -6
- package/src/phases/applyPlan.ts +33 -14
- package/src/phases/applySuggestions.ts +2 -2
- package/src/phases/runAgentWithWatchdogs.ts +2 -1
- package/src/toolState.ts +7 -0
- package/src/utils/agent/codexOAuth.ts +7 -7
- package/src/utils/agent/openCodeModels.ts +4 -6
- package/src/utils/cli.ts +1 -31
- package/src/utils/cloud/runContext.ts +1 -8
- package/src/utils/config/baseRefConfig.ts +6 -0
- package/src/utils/config/infraweaverConfig.ts +2 -0
- package/src/utils/config/payload.ts +41 -0
- package/src/utils/github/assets.ts +1 -6
- package/src/utils/github/github.ts +42 -150
- package/src/utils/github/token.ts +6 -11
- package/src/utils/log.ts +4 -5
- package/src/utils/markdownTable.ts +11 -0
- package/src/utils/prompt/changeImpact.ts +18 -26
- package/src/utils/prompt/instructions.ts +1 -1
- package/src/utils/prompt/promptDirectives.ts +1 -1
- package/src/utils/prompt/toolSelection.ts +11 -2
- package/src/utils/setup/toolLicensing.ts +7 -0
- package/src/utils/telemetry.ts +2 -1
- package/src/utils/unifiedDiff.ts +120 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { formatMcpToolRef, infraweaverMcpName } from "#app/external";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Notices when the agent has lost the infraweaver tool server, so the run stops
|
|
5
|
+
* instead of spending its budget without it.
|
|
6
|
+
*
|
|
7
|
+
* Every deliverable — the push, the pull request, the review, the final
|
|
8
|
+
* progress report — goes through that server. Once Claude Code drops it, each
|
|
9
|
+
* call fails at once with "not connected", and a model told to keep trying
|
|
10
|
+
* retries in a tight loop until the token quota ends the run: a refactor did
|
|
11
|
+
* exactly that for 41 minutes and 8.5M tokens.
|
|
12
|
+
*
|
|
13
|
+
* One lost call is not enough to stop on: Claude Code reconnects, and a call
|
|
14
|
+
* that lands mid-reconnect fails the same way. The run stops only after
|
|
15
|
+
* {@link LOST_CALLS} such failures in a row spanning at least
|
|
16
|
+
* {@link LOST_WINDOW_MS}, with no infraweaver call succeeding in between.
|
|
17
|
+
*/
|
|
18
|
+
export const LOST_CALLS = 3;
|
|
19
|
+
export const LOST_WINDOW_MS = 30_000;
|
|
20
|
+
|
|
21
|
+
/** Claude Code's wordings for a call to a server it no longer has. */
|
|
22
|
+
const LOST = new RegExp(
|
|
23
|
+
[
|
|
24
|
+
`MCP server ["']${infraweaverMcpName}["'] is not connected`,
|
|
25
|
+
`MCP server ["']${infraweaverMcpName}["'] has disconnected`,
|
|
26
|
+
`MCP server ["']${infraweaverMcpName}["'] transport dropped mid-call`,
|
|
27
|
+
`MCP tool call for ["']${infraweaverMcpName}["'] timed out connecting`,
|
|
28
|
+
].join("|"),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const TOOL_PREFIX = formatMcpToolRef("claude", "");
|
|
32
|
+
|
|
33
|
+
export class ToolPlaneLostError extends Error {
|
|
34
|
+
constructor(calls: number, seconds: number) {
|
|
35
|
+
super(
|
|
36
|
+
`the agent lost the ${infraweaverMcpName} tool server: ${calls} calls in a row over ${seconds}s ` +
|
|
37
|
+
"failed because it was not connected, so the run was stopped rather than continuing " +
|
|
38
|
+
"without the tools that deliver its result",
|
|
39
|
+
);
|
|
40
|
+
this.name = "ToolPlaneLostError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ToolResult {
|
|
45
|
+
/** the tool that was called, when known. */
|
|
46
|
+
toolName: string | undefined;
|
|
47
|
+
isError: boolean;
|
|
48
|
+
text: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createToolPlaneWatch(now: () => number = Date.now) {
|
|
52
|
+
let streak = 0;
|
|
53
|
+
let since = 0;
|
|
54
|
+
return {
|
|
55
|
+
/** Record one tool result; returns the error to stop with once the server is lost. */
|
|
56
|
+
observe(result: ToolResult): ToolPlaneLostError | null {
|
|
57
|
+
if (result.isError && LOST.test(result.text)) {
|
|
58
|
+
if (streak === 0) since = now();
|
|
59
|
+
streak += 1;
|
|
60
|
+
const elapsed = now() - since;
|
|
61
|
+
return streak >= LOST_CALLS && elapsed >= LOST_WINDOW_MS
|
|
62
|
+
? new ToolPlaneLostError(streak, Math.round(elapsed / 1000))
|
|
63
|
+
: null;
|
|
64
|
+
}
|
|
65
|
+
if (!result.isError && result.toolName?.startsWith(TOOL_PREFIX)) streak = 0;
|
|
66
|
+
return null;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type ToolPlaneWatch = ReturnType<typeof createToolPlaneWatch>;
|
package/src/external.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* Other files in action/ re-export from this file for backward compatibility.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type {
|
|
7
|
+
import type { components } from "@octokit/openapi-webhooks-types";
|
|
8
|
+
|
|
9
|
+
type PullRequest = components["schemas"]["pull-request"];
|
|
8
10
|
|
|
9
11
|
// mcp name constant
|
|
10
12
|
export const infraweaverMcpName = "infraweaver";
|
package/src/mcp/assess.ts
CHANGED
|
@@ -20,9 +20,10 @@ import {
|
|
|
20
20
|
languageOfConcern,
|
|
21
21
|
resolveLanguageScope,
|
|
22
22
|
} from "#app/mcp/terraform/iacLanguages";
|
|
23
|
+
import { count } from "#app/mcp/terraform/prose";
|
|
23
24
|
import { runScannersCached } from "#app/mcp/terraform/scannerCache";
|
|
24
25
|
import { attachSourceSnippets } from "#app/mcp/terraform/scanners";
|
|
25
|
-
import { scanSessionOf } from "#app/mcp/terraform/scanSession";
|
|
26
|
+
import { recordScanConcerns, scanSessionOf } from "#app/mcp/terraform/scanSession";
|
|
26
27
|
import { suppressConcerns } from "#app/mcp/terraform/suppressions";
|
|
27
28
|
import {
|
|
28
29
|
type Concern,
|
|
@@ -290,21 +291,20 @@ const BANNER_RECONCILE =
|
|
|
290
291
|
*/
|
|
291
292
|
function renderSeverityBanner(s: AssessmentScorecard): string {
|
|
292
293
|
const highBand = s.by_severity.critical + s.by_severity.high;
|
|
293
|
-
const plural = (n: number) => (n === 1 ? "" : "s");
|
|
294
294
|
switch (s.posture) {
|
|
295
295
|
case "clean":
|
|
296
296
|
return "> [!NOTE]\n> ✅ **0 concerns found** at the configured threshold.";
|
|
297
297
|
case "advisory":
|
|
298
298
|
return (
|
|
299
299
|
"> [!WARNING]\n" +
|
|
300
|
-
`> ⚠️ **${s.total
|
|
300
|
+
`> ⚠️ **${count(s.total, "concern")} found** — none critical/high. ` +
|
|
301
301
|
"Plan to address them.\n>\n" +
|
|
302
302
|
`> ${BANNER_RECONCILE}`
|
|
303
303
|
);
|
|
304
304
|
default:
|
|
305
305
|
return (
|
|
306
306
|
"> [!CAUTION]\n" +
|
|
307
|
-
`> 🚨 **${highBand
|
|
307
|
+
`> 🚨 **${count(highBand, "critical/high concern")} found** ` +
|
|
308
308
|
`(${s.total} open in total). Review before relying on this Terraform.\n>\n` +
|
|
309
309
|
`> ${BANNER_RECONCILE}`
|
|
310
310
|
);
|
|
@@ -449,9 +449,7 @@ export function renderAssessmentMarkdown(
|
|
|
449
449
|
.filter((sev) => s.by_severity[sev] > 0)
|
|
450
450
|
.map((sev) => `\`${sev}: ${s.by_severity[sev]}\``);
|
|
451
451
|
lines.push(
|
|
452
|
-
`**${s.total}
|
|
453
|
-
sevCells.length ? sevCells.join(" · ") : "none"
|
|
454
|
-
}`,
|
|
452
|
+
`**${count(s.total, "concern")}** — ${sevCells.length ? sevCells.join(" · ") : "none"}`,
|
|
455
453
|
);
|
|
456
454
|
if (ctx.delta) {
|
|
457
455
|
lines.push("");
|
|
@@ -624,6 +622,8 @@ export async function runAssessmentPipeline(
|
|
|
624
622
|
// the SAME threshold everywhere a scan happens. A verifier running on a
|
|
625
623
|
// different one would report a finding resolved because the knob moved.
|
|
626
624
|
repeatedLiteralThreshold: ctx.payload.repeatedLiteralThreshold,
|
|
625
|
+
// and the same variable files, which decide what trivy and checkov see
|
|
626
|
+
varFiles: ctx.payload.varFiles,
|
|
627
627
|
});
|
|
628
628
|
// Re-check the scope against what ACTUALLY ran: a language whose only covering
|
|
629
629
|
// scanner was skipped/disabled this run must report as a coverage gap, never
|
|
@@ -716,7 +716,7 @@ export function TerraformAssessTool(ctx: LocalToolContext) {
|
|
|
716
716
|
// so finalizeSuccessRun's emitFindingsOutputs + reportToCloud fire — assess IS
|
|
717
717
|
// the report path, but lastScanConcerns was only ever set by terraform_scan
|
|
718
718
|
// (Remediate), so Cloud reporting silently no-opped in Assess.
|
|
719
|
-
scanSessionOf(ctx.toolState)
|
|
719
|
+
recordScanConcerns(scanSessionOf(ctx.toolState), concerns);
|
|
720
720
|
const markdown = renderAssessmentMarkdown(scorecard, { delta });
|
|
721
721
|
|
|
722
722
|
const ran = outcomes.filter((o) => o.ran).map((o) => o.source);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { $ } from "@infraweaver-io/sandbox/shell";
|
|
2
|
+
import type { ToolState } from "#app/toolState";
|
|
3
|
+
import type { RunContextData } from "#app/utils/cloud/runContextData";
|
|
4
|
+
import type { ResolvedPayload } from "#app/utils/config/payload";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The branch a run's change is proposed into. Its own module because three
|
|
8
|
+
* places need the same answer and must never disagree: the PR tools (where the
|
|
9
|
+
* PR goes), `terraform_change_summary` (what the change is measured against)
|
|
10
|
+
* and the push guardrail that refuses a change an open PR already proposes.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The slice `resolveBaseBranch` reads: the declared base-branch input, the
|
|
15
|
+
* branch the run started on and the repository's default branch.
|
|
16
|
+
*/
|
|
17
|
+
export interface BaseBranchContext {
|
|
18
|
+
payload: ResolvedPayload;
|
|
19
|
+
repo: RunContextData["repo"];
|
|
20
|
+
toolState: Pick<ToolState, "initialHead">;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pure base-branch picker. Precedence: an explicit `base_branch` declaration
|
|
25
|
+
* always wins; then the branch the run started on, so a run dispatched on
|
|
26
|
+
* `develop` proposes into `develop`; then the repository's default branch
|
|
27
|
+
* (GitHub reports it — normally `main`, sometimes `master`); otherwise prefer
|
|
28
|
+
* `main`, then `master`; final fallback `main`. Split out from git/ctx so it's
|
|
29
|
+
* exhaustively testable.
|
|
30
|
+
*/
|
|
31
|
+
export function pickBaseBranch(opts: {
|
|
32
|
+
declared?: string | undefined;
|
|
33
|
+
startBranch?: string | undefined;
|
|
34
|
+
defaultBranch?: string | undefined;
|
|
35
|
+
mainExists: boolean;
|
|
36
|
+
masterExists: boolean;
|
|
37
|
+
}): string {
|
|
38
|
+
if (opts.declared) return opts.declared;
|
|
39
|
+
if (opts.startBranch) return opts.startBranch;
|
|
40
|
+
if (opts.defaultBranch) return opts.defaultBranch;
|
|
41
|
+
if (opts.mainExists) return "main";
|
|
42
|
+
if (opts.masterExists) return "master";
|
|
43
|
+
return "main";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** true when `branch` exists as a remote-tracking or local ref. */
|
|
47
|
+
function branchExists(branch: string): boolean {
|
|
48
|
+
for (const ref of [`refs/remotes/origin/${branch}`, `refs/heads/${branch}`]) {
|
|
49
|
+
try {
|
|
50
|
+
$("git", ["rev-parse", "--verify", "--quiet", ref], { log: false });
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
// ref absent — try the next form
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The branch the run started on, while HEAD is somewhere else. A run that is
|
|
61
|
+
* still on its starting branch is proposing or summarising that branch itself,
|
|
62
|
+
* so it cannot be the base; the run started detached (a pull request's merge
|
|
63
|
+
* ref) has no branch to offer.
|
|
64
|
+
*/
|
|
65
|
+
function startBranchOf(ctx: BaseBranchContext): string | undefined {
|
|
66
|
+
const head = ctx.toolState.initialHead;
|
|
67
|
+
if (head?.kind !== "branch") return undefined;
|
|
68
|
+
let current: string;
|
|
69
|
+
try {
|
|
70
|
+
current = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
|
71
|
+
} catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
return current === head.name ? undefined : head.name;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Deterministically resolve the branch a PR targets, so the base is never left
|
|
79
|
+
* to the agent's guess: the `base_branch` input, else the branch the run
|
|
80
|
+
* started on, else the repository's default branch (`main`, or `master`). Git
|
|
81
|
+
* is only probed for main/master in the last-resort case where none of those is
|
|
82
|
+
* available (essentially never).
|
|
83
|
+
*/
|
|
84
|
+
export function resolveBaseBranch(ctx: BaseBranchContext): string {
|
|
85
|
+
const declared = ctx.payload.baseBranch?.trim() || undefined;
|
|
86
|
+
const startBranch = declared ? undefined : startBranchOf(ctx);
|
|
87
|
+
const defaultBranch = ctx.repo.data.default_branch?.trim() || undefined;
|
|
88
|
+
const needsProbe = !declared && !startBranch && !defaultBranch;
|
|
89
|
+
return pickBaseBranch({
|
|
90
|
+
declared,
|
|
91
|
+
startBranch,
|
|
92
|
+
defaultBranch,
|
|
93
|
+
mainExists: needsProbe && branchExists("main"),
|
|
94
|
+
masterExists: needsProbe && branchExists("master"),
|
|
95
|
+
});
|
|
96
|
+
}
|
package/src/mcp/changeSummary.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { $ } from "@infraweaver-io/sandbox/shell";
|
|
2
2
|
import { type } from "arktype";
|
|
3
|
-
import type { BaseBranchContext } from "#app/mcp/
|
|
4
|
-
import { resolveBaseBranch } from "#app/mcp/
|
|
3
|
+
import type { BaseBranchContext } from "#app/mcp/baseBranch";
|
|
4
|
+
import { resolveBaseBranch } from "#app/mcp/baseBranch";
|
|
5
5
|
import { execute, tool, toolOk } from "#app/mcp/shared";
|
|
6
6
|
import { skipResult } from "#app/mcp/terraform/types";
|
|
7
7
|
import { log } from "#app/utils/cli";
|
|
8
|
+
import { parseDiffLines } from "#app/utils/unifiedDiff";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* AI PR summaries — the deterministic Terraform-change anchor. A PR summary
|
|
@@ -71,19 +72,17 @@ export function summarizeTerraformResourceDiff(diff: string): TerraformChangeSum
|
|
|
71
72
|
const files = new Set<string>();
|
|
72
73
|
let file = "";
|
|
73
74
|
let inTf = false;
|
|
74
|
-
for (const
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
file = path === "/dev/null" ? "" : path;
|
|
75
|
+
for (const line of parseDiffLines(diff)) {
|
|
76
|
+
if (line.kind === "file") {
|
|
77
|
+
file = line.path === "/dev/null" ? "" : line.path;
|
|
78
78
|
inTf = !!file && isTfFile(file);
|
|
79
79
|
continue;
|
|
80
80
|
}
|
|
81
|
-
if (raw.startsWith("--- ") || raw.startsWith("diff --git") || raw.startsWith("@@")) continue;
|
|
82
81
|
if (!inTf) continue;
|
|
83
|
-
if (
|
|
82
|
+
if (line.kind === "add" || line.kind === "remove") {
|
|
84
83
|
files.add(file);
|
|
85
|
-
const addr = parseBlockAddress(
|
|
86
|
-
if (addr) (
|
|
84
|
+
const addr = parseBlockAddress(line.text);
|
|
85
|
+
if (addr) (line.kind === "add" ? added : removed).add(addr);
|
|
87
86
|
}
|
|
88
87
|
}
|
|
89
88
|
const sort = (s: Set<string>): string[] => [...s].sort();
|
|
@@ -97,7 +96,7 @@ export function summarizeTerraformResourceDiff(diff: string): TerraformChangeSum
|
|
|
97
96
|
|
|
98
97
|
const TerraformChangeSummaryParams = type({
|
|
99
98
|
"base?": type.string.describe(
|
|
100
|
-
"base branch to diff against (default: the run's resolved base branch —
|
|
99
|
+
"base branch to diff against (default: the run's resolved base branch — the base_branch input, else the branch the run started on, else main/master).",
|
|
101
100
|
),
|
|
102
101
|
});
|
|
103
102
|
|
package/src/mcp/checkSuite.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { stripVTControlCharacters } from "node:util";
|
|
3
4
|
import { type } from "arktype";
|
|
4
5
|
import type { GitHubToolContext } from "#app/mcp/capabilityContext";
|
|
5
6
|
import { execute, tool } from "#app/mcp/shared";
|
|
@@ -29,8 +30,7 @@ type LogAnalysis = {
|
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
export function analyzeLog(logs: string, excerptLines = 80): LogAnalysis {
|
|
32
|
-
|
|
33
|
-
const clean = logs.replace(/\x1b\[[0-9;]*m/g, "");
|
|
33
|
+
const clean = stripVTControlCharacters(logs);
|
|
34
34
|
const lines = clean.split("\n");
|
|
35
35
|
const totalLines = lines.length;
|
|
36
36
|
|
|
@@ -192,19 +192,16 @@ export function GetCheckSuiteLogsTool(ctx: GitHubToolContext) {
|
|
|
192
192
|
failedJobs,
|
|
193
193
|
LOG_FETCH_CONCURRENCY,
|
|
194
194
|
async (job): Promise<JobLogResult> => {
|
|
195
|
-
|
|
195
|
+
// the endpoint 302s to the log blob; Octokit follows it and returns the
|
|
196
|
+
// body — a string for a text/* content type, an ArrayBuffer otherwise.
|
|
197
|
+
const { data } = await ctx.octokit.rest.actions.downloadJobLogsForWorkflowRun({
|
|
196
198
|
owner: ctx.repo.owner,
|
|
197
199
|
repo: ctx.repo.name,
|
|
198
200
|
job_id: job.id,
|
|
199
201
|
request: { signal: AbortSignal.timeout(10_000) },
|
|
200
202
|
});
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const logsResult = await fetch(logsUrl, { signal: AbortSignal.timeout(10_000) });
|
|
204
|
-
if (!logsResult.ok) {
|
|
205
|
-
throw new Error(`failed to fetch logs: ${logsResult.status} ${logsResult.statusText}`);
|
|
206
|
-
}
|
|
207
|
-
const logsText = await logsResult.text();
|
|
203
|
+
const logsText =
|
|
204
|
+
typeof data === "string" ? data : Buffer.from(data as ArrayBuffer).toString("utf8");
|
|
208
205
|
|
|
209
206
|
// write full log to disk
|
|
210
207
|
const logPath = join(logsDir, `job-${job.id}.log`);
|
package/src/mcp/checkout.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { getTempDir } from "#app/utils/env";
|
|
|
18
18
|
import { computeIncrementalDiff } from "#app/utils/github/rangeDiff";
|
|
19
19
|
import { createChangeImpactArtifact } from "#app/utils/prompt/changeImpact";
|
|
20
20
|
import { executeLifecycleHook } from "#app/utils/run/lifecycle";
|
|
21
|
+
import { parseDiffLines } from "#app/utils/unifiedDiff";
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* checkout_pr genuinely spans two capabilities: it reads the PR + its files over
|
|
@@ -78,46 +79,19 @@ export function formatFilesWithLineNumbers(files: PullFile[]): FormatFilesResult
|
|
|
78
79
|
continue;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
//
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
let newLine = 0;
|
|
85
|
-
|
|
86
|
-
for (const line of lines) {
|
|
87
|
-
// hunk header: @@ -OLD,COUNT +NEW,COUNT @@ optional context
|
|
88
|
-
const hunkMatch = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
89
|
-
if (hunkMatch) {
|
|
90
|
-
oldLine = parseInt(hunkMatch[1]!, 10);
|
|
91
|
-
newLine = parseInt(hunkMatch[2]!, 10);
|
|
92
|
-
output.push(line); // pass through unchanged
|
|
93
|
-
currentLine++;
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// code lines within hunks
|
|
98
|
-
const changeType = line[0] || " ";
|
|
99
|
-
const code = line.slice(1);
|
|
100
|
-
|
|
101
|
-
if (changeType === "-") {
|
|
82
|
+
// format the patch with line numbers
|
|
83
|
+
for (const line of parseDiffLines(file.patch)) {
|
|
84
|
+
if (line.kind === "remove") {
|
|
102
85
|
// removed line: show old line number, no new line number
|
|
103
|
-
output.push(`| ${padNum(oldLine)} | | - | ${
|
|
104
|
-
|
|
105
|
-
} else if (changeType === "+") {
|
|
86
|
+
output.push(`| ${padNum(line.oldLine)} | | - | ${line.text}`);
|
|
87
|
+
} else if (line.kind === "add") {
|
|
106
88
|
// added line: no old line number, show new line number
|
|
107
|
-
output.push(`| | ${padNum(newLine)} | + | ${
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// context line or ""
|
|
111
|
-
if (changeType === "\\") {
|
|
112
|
-
output.push(line); // pass through as-is
|
|
113
|
-
} else {
|
|
114
|
-
output.push(`| ${padNum(oldLine)} | ${padNum(newLine)} | | ${code}`);
|
|
115
|
-
oldLine++;
|
|
116
|
-
newLine++;
|
|
117
|
-
}
|
|
89
|
+
output.push(`| | ${padNum(line.newLine)} | + | ${line.text}`);
|
|
90
|
+
} else if (line.kind === "context") {
|
|
91
|
+
output.push(`| ${padNum(line.oldLine)} | ${padNum(line.newLine)} | | ${line.text}`);
|
|
118
92
|
} else {
|
|
119
|
-
//
|
|
120
|
-
output.push(line);
|
|
93
|
+
// hunk header, "" or unknown: pass through unchanged
|
|
94
|
+
output.push(line.raw);
|
|
121
95
|
}
|
|
122
96
|
currentLine++;
|
|
123
97
|
}
|
|
@@ -481,7 +455,7 @@ export async function checkoutPrBranch(
|
|
|
481
455
|
// dispatched on. if it's been closed/merged or the head SHA moved,
|
|
482
456
|
// no amount of retrying will populate the expected ref — surface a
|
|
483
457
|
// clean abort error instead of burning the full retry budget.
|
|
484
|
-
const msg =
|
|
458
|
+
const msg = errorMessage(e);
|
|
485
459
|
if (PULL_REF_MISSING_PATTERN.test(msg)) {
|
|
486
460
|
await abortIfPullRequestMoved({ octokit, owner, repo: name, pr });
|
|
487
461
|
}
|
|
@@ -491,8 +465,7 @@ export async function checkoutPrBranch(
|
|
|
491
465
|
{
|
|
492
466
|
delaysMs: PULL_REF_RETRY_DELAYS_MS,
|
|
493
467
|
label: `pull/${pr.number}/head fetch`,
|
|
494
|
-
shouldRetry: (e) =>
|
|
495
|
-
PULL_REF_MISSING_PATTERN.test(e instanceof Error ? e.message : String(e)),
|
|
468
|
+
shouldRetry: (e) => PULL_REF_MISSING_PATTERN.test(errorMessage(e)),
|
|
496
469
|
},
|
|
497
470
|
);
|
|
498
471
|
|
package/src/mcp/crosswalk.ts
CHANGED
|
@@ -960,6 +960,11 @@ export function mapConcernToControls(concern: {
|
|
|
960
960
|
category?: string;
|
|
961
961
|
}): { themes: string[]; controls: ControlRef[] } {
|
|
962
962
|
assertCrosswalkIntegrity();
|
|
963
|
+
// A style finding is about the form of the code, and no control framework has
|
|
964
|
+
// anything to say about it. Its evidence can still quote the code — a
|
|
965
|
+
// repeated literal that happens to be `0.0.0.0/0` — so it must not reach the
|
|
966
|
+
// signal match at all.
|
|
967
|
+
if (concern.category === "style") return { themes: [], controls: [] };
|
|
963
968
|
const haystack = `${concern.rule_id} ${concern.evidence}`.toLowerCase();
|
|
964
969
|
const locked = THEME_LOCKED.get(bareRuleId(concern.rule_id));
|
|
965
970
|
const themes: string[] = [];
|
package/src/mcp/findingAudit.ts
CHANGED
|
@@ -294,7 +294,7 @@ function severityMarker(severity: Severity): "🚨" | "⚠️" | "ℹ️" {
|
|
|
294
294
|
const LEADING_MARKER = /^(\s*)(🚨|⚠️|⚠|ℹ️|ℹ)️?\s*/u;
|
|
295
295
|
|
|
296
296
|
/** The severity marker a comment body opens with, if any. */
|
|
297
|
-
function leadingMarker(body: string): string | null {
|
|
297
|
+
export function leadingMarker(body: string): string | null {
|
|
298
298
|
return LEADING_MARKER.exec(body)?.[2] ?? null;
|
|
299
299
|
}
|
|
300
300
|
|
package/src/mcp/git.ts
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
assertThirdPartyAdoptionOptIn,
|
|
20
20
|
enforceProtectedPaths,
|
|
21
21
|
enforceRemediationPaths,
|
|
22
|
+
isBranchPerChangeMode,
|
|
23
|
+
isGuardedModeName,
|
|
22
24
|
withChangedFilesMemo,
|
|
23
25
|
} from "#app/mcp/guardrails";
|
|
24
26
|
import { execute, tool } from "#app/mcp/shared";
|
|
@@ -302,7 +304,7 @@ async function pushWithRetry(
|
|
|
302
304
|
return;
|
|
303
305
|
} catch (err) {
|
|
304
306
|
lastErr = err;
|
|
305
|
-
const msg =
|
|
307
|
+
const msg = errorMessage(err);
|
|
306
308
|
const kind = classifyPushError(msg);
|
|
307
309
|
|
|
308
310
|
// last attempt, or a non-retryable error (concurrent-push / unknown /
|
|
@@ -411,6 +413,17 @@ export function PushBranchTool(ctx: GitToolContext) {
|
|
|
411
413
|
// can't slip past the default-branch guard below.
|
|
412
414
|
rejectSpecialRef(branch, "branch");
|
|
413
415
|
|
|
416
|
+
// every guardrail below judges HEAD, so a write mode pushes the branch it
|
|
417
|
+
// checked: naming another one would push commits nothing here inspected.
|
|
418
|
+
if (branchName && isGuardedModeName(ctx.toolState.selectedMode)) {
|
|
419
|
+
const current = $("git", ["rev-parse", "--abbrev-ref", "HEAD"], { log: false });
|
|
420
|
+
if (current !== branchName) {
|
|
421
|
+
throw new Error(
|
|
422
|
+
`push blocked: '${branchName}' is not the checked-out branch ('${current}'). In this mode push_branch pushes the current branch, because that is what the guardrails check — check out '${branchName}' first, or omit branchName.`,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
414
427
|
// reject push if working tree is dirty — forces agent to commit or discard before pushing
|
|
415
428
|
const status = $("git", ["status", "--porcelain"], { log: false });
|
|
416
429
|
if (status) {
|
|
@@ -526,6 +539,19 @@ export function PushBranchTool(ctx: GitToolContext) {
|
|
|
526
539
|
);
|
|
527
540
|
}
|
|
528
541
|
|
|
542
|
+
// git never rejects a force push, so the another-change check after the
|
|
543
|
+
// push never sees one: refuse up front to overwrite a branch this run
|
|
544
|
+
// did not push.
|
|
545
|
+
if (
|
|
546
|
+
force &&
|
|
547
|
+
isBranchPerChangeMode(ctx.toolState.selectedMode) &&
|
|
548
|
+
!ctx.toolState.pushedHeads?.has(pushDest.remoteBranch)
|
|
549
|
+
) {
|
|
550
|
+
throw new Error(
|
|
551
|
+
`push blocked: force-pushing '${pushDest.remoteBranch}' would overwrite a branch this run did not push — if it exists, it belongs to another change. Push this change to a NEW branch name, without force.`,
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
529
555
|
// use refspec when local and remote branch names differ
|
|
530
556
|
const refspec =
|
|
531
557
|
branch === pushDest.remoteBranch ? branch : `${branch}:${pushDest.remoteBranch}`;
|
|
@@ -580,8 +606,22 @@ export function PushBranchTool(ctx: GitToolContext) {
|
|
|
580
606
|
ctx.refreshGitToken,
|
|
581
607
|
);
|
|
582
608
|
} catch (err) {
|
|
583
|
-
const msg =
|
|
609
|
+
const msg = errorMessage(err);
|
|
584
610
|
if (classifyPushError(msg) === "concurrent-push") {
|
|
611
|
+
// In a mode that names a fresh branch per change, a remote branch this
|
|
612
|
+
// run never pushed is another change's branch, not a concurrent push
|
|
613
|
+
// of this one: integrating it would fold two changes into one PR, and
|
|
614
|
+
// adopting it would propose work this run never checked. Remediate
|
|
615
|
+
// names its branch after the concern group, and a re-run updates it.
|
|
616
|
+
if (
|
|
617
|
+
isBranchPerChangeMode(ctx.toolState.selectedMode) &&
|
|
618
|
+
!ctx.toolState.pushedHeads?.has(pushDest.remoteBranch)
|
|
619
|
+
) {
|
|
620
|
+
throw new Error(
|
|
621
|
+
`push rejected: a branch named '${pushDest.remoteBranch}' already exists on the remote, and this run did not push it — it belongs to another change (an earlier run's, or a person's).\n\n` +
|
|
622
|
+
`do not fetch, merge, rebase onto or check out that branch. create a NEW branch name for this change from your current commit (git({ command: "checkout", args: ["-b", "${pushDest.remoteBranch}-2"] })) and push_branch again.`,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
585
625
|
// git rebase is blocked through the MCP tool when shell is disabled
|
|
586
626
|
// (rebase --exec can execute arbitrary code). merge always works and
|
|
587
627
|
// integrates remote changes cleanly, so suggest it as the default.
|
|
@@ -602,6 +642,8 @@ export function PushBranchTool(ctx: GitToolContext) {
|
|
|
602
642
|
}
|
|
603
643
|
|
|
604
644
|
const pushedSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
|
645
|
+
ctx.toolState.pushedHeads ??= new Map();
|
|
646
|
+
ctx.toolState.pushedHeads.set(pushDest.remoteBranch, pushedSha);
|
|
605
647
|
log.info(
|
|
606
648
|
`» pushed branch ${branch} to ${pushDest.remoteName}/${pushDest.remoteBranch} (sha ${pushedSha})`,
|
|
607
649
|
);
|