infraweaver 0.3.7 → 0.3.9
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 +31908 -18736
- package/dist/external.d.ts +2 -1
- package/dist/index.js +31992 -18749
- package/dist/internal.js +133 -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 +41 -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 +7 -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
|
@@ -13,6 +13,7 @@ import { getTempDir } from "#app/utils/env";
|
|
|
13
13
|
import { resolveBodyAssets } from "#app/utils/github/body";
|
|
14
14
|
import { stripExistingFooter } from "#app/utils/github/buildInfraweaverFooter";
|
|
15
15
|
import { log } from "#app/utils/log";
|
|
16
|
+
import { type HunkHeader, parseDiffLines } from "#app/utils/unifiedDiff";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* A GraphQL review-thread fetch worth retrying: an HTTP 5xx / network blip
|
|
@@ -24,7 +25,7 @@ import { log } from "#app/utils/log";
|
|
|
24
25
|
*/
|
|
25
26
|
export function isTransientGraphqlError(error: unknown): boolean {
|
|
26
27
|
if (isTransientServerError(error)) return true;
|
|
27
|
-
const msg =
|
|
28
|
+
const msg = errorMessage(error);
|
|
28
29
|
return /something went wrong while executing your query/i.test(msg);
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -100,7 +101,7 @@ query ($owner: String!, $name: String!, $prNumber: Int!, $cursor: String) {
|
|
|
100
101
|
line
|
|
101
102
|
startLine
|
|
102
103
|
diffSide
|
|
103
|
-
comments(first: 1) { nodes { databaseId body author { login __typename } } }
|
|
104
|
+
comments(first: 1) { nodes { databaseId body originalCommit { oid } author { login __typename } } }
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
}
|
|
@@ -127,6 +128,7 @@ export type OutstandingThreadsResponse = {
|
|
|
127
128
|
| ({
|
|
128
129
|
databaseId?: number | null;
|
|
129
130
|
body?: string | null;
|
|
131
|
+
originalCommit?: { oid: string } | null;
|
|
130
132
|
author: { login: string; __typename?: string } | null;
|
|
131
133
|
} | null)[]
|
|
132
134
|
| null;
|
|
@@ -148,6 +150,10 @@ export type OpenInfraweaverThread = {
|
|
|
148
150
|
rootCommentId?: number;
|
|
149
151
|
/** the marker the root comment carries, when the engine wrote one */
|
|
150
152
|
marker?: CommentMarker;
|
|
153
|
+
/** the root comment's body */
|
|
154
|
+
rootBody?: string;
|
|
155
|
+
/** the commit the root comment was made on */
|
|
156
|
+
rootCommit?: string;
|
|
151
157
|
};
|
|
152
158
|
|
|
153
159
|
/**
|
|
@@ -202,6 +208,8 @@ export async function listOpenInfraweaverThreads(input: {
|
|
|
202
208
|
if (typeof root.databaseId === "number") entry.rootCommentId = root.databaseId;
|
|
203
209
|
const marker = parseCommentMarker(root.body);
|
|
204
210
|
if (marker) entry.marker = marker;
|
|
211
|
+
if (root.body) entry.rootBody = root.body;
|
|
212
|
+
if (root.originalCommit?.oid) entry.rootCommit = root.originalCommit.oid;
|
|
205
213
|
// a thread with no current `line` has no comparable span — outdated, or a
|
|
206
214
|
// file-level comment. counted, never matched against.
|
|
207
215
|
if (!entry.isOutdated && thread.path && thread.line != null) {
|
|
@@ -289,62 +297,26 @@ function extractCommentedLines(
|
|
|
289
297
|
endLine: number | null,
|
|
290
298
|
side: "LEFT" | "RIGHT",
|
|
291
299
|
): string {
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
const header = lines[0] ?? "";
|
|
296
|
-
const contentLines = lines.slice(1);
|
|
297
|
-
|
|
298
|
-
// parse header: @@ -old_start,old_count +new_start,new_count @@
|
|
299
|
-
const headerMatch = header.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
300
|
-
if (!headerMatch) return diffHunk;
|
|
301
|
-
|
|
302
|
-
// destructure with defaults instead of `!` — the two `(\d+)` groups are required,
|
|
303
|
-
// so the defaults never apply; this carries the non-null in the binding, not an assertion.
|
|
304
|
-
const [, oldStr = "", newStr = ""] = headerMatch;
|
|
305
|
-
const hunkOldStart = parseInt(oldStr, 10);
|
|
306
|
-
const hunkNewStart = parseInt(newStr, 10);
|
|
300
|
+
const [first, ...rest] = parseDiffLines(diffHunk);
|
|
301
|
+
if (rest.length === 0 || first?.kind !== "hunk") return diffHunk;
|
|
302
|
+
const header = first.raw;
|
|
307
303
|
|
|
308
304
|
// LEFT = old file (deletions), RIGHT = new file (additions)
|
|
309
|
-
const hunkStart = side === "LEFT" ?
|
|
305
|
+
const hunkStart = side === "LEFT" ? first.header.oldStart : first.header.newStart;
|
|
310
306
|
const commentStart = startLine ?? endLine ?? hunkStart;
|
|
311
307
|
const commentEnd = endLine ?? commentStart;
|
|
312
308
|
|
|
313
|
-
//
|
|
309
|
+
// number each diff line on the commented side only
|
|
314
310
|
// - lines: old file only (LEFT)
|
|
315
311
|
// + lines: new file only (RIGHT)
|
|
316
312
|
// context lines: both files
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (prefix === "-") {
|
|
325
|
-
// deletion - only has old line number
|
|
326
|
-
diffLines.push({
|
|
327
|
-
text: line,
|
|
328
|
-
lineNum: side === "LEFT" ? oldLineNum : null,
|
|
329
|
-
});
|
|
330
|
-
oldLineNum++;
|
|
331
|
-
} else if (prefix === "+") {
|
|
332
|
-
// addition - only has new line number
|
|
333
|
-
diffLines.push({
|
|
334
|
-
text: line,
|
|
335
|
-
lineNum: side === "RIGHT" ? newLineNum : null,
|
|
336
|
-
});
|
|
337
|
-
newLineNum++;
|
|
338
|
-
} else {
|
|
339
|
-
// context - has both line numbers
|
|
340
|
-
diffLines.push({
|
|
341
|
-
text: line,
|
|
342
|
-
lineNum: side === "LEFT" ? oldLineNum : newLineNum,
|
|
343
|
-
});
|
|
344
|
-
oldLineNum++;
|
|
345
|
-
newLineNum++;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
313
|
+
const diffLines = rest.map((line) => {
|
|
314
|
+
let lineNum: number | null = null;
|
|
315
|
+
if (line.kind === "remove") lineNum = side === "LEFT" ? line.oldLine : null;
|
|
316
|
+
else if (line.kind === "add") lineNum = side === "RIGHT" ? line.newLine : null;
|
|
317
|
+
else if (line.kind === "context") lineNum = side === "LEFT" ? line.oldLine : line.newLine;
|
|
318
|
+
return { text: line.raw, lineNum };
|
|
319
|
+
});
|
|
348
320
|
|
|
349
321
|
// find lines for comment range with context
|
|
350
322
|
const targetStart = commentStart - CONTEXT_PADDING;
|
|
@@ -373,44 +345,26 @@ function extractCommentedLines(
|
|
|
373
345
|
}
|
|
374
346
|
|
|
375
347
|
// parsed hunk from a unified diff
|
|
376
|
-
export type ParsedHunk = {
|
|
348
|
+
export type ParsedHunk = HunkHeader & {
|
|
377
349
|
header: string;
|
|
378
|
-
oldStart: number;
|
|
379
|
-
oldCount: number;
|
|
380
|
-
newStart: number;
|
|
381
|
-
newCount: number;
|
|
382
350
|
content: string[];
|
|
383
351
|
};
|
|
384
352
|
|
|
385
353
|
// parse a full file patch into individual hunks
|
|
386
354
|
export function parseFilePatches(patch: string): ParsedHunk[] {
|
|
387
355
|
const hunks: ParsedHunk[] = [];
|
|
388
|
-
const lines = patch.split("\n");
|
|
389
|
-
|
|
390
356
|
let currentHunk: ParsedHunk | null = null;
|
|
391
357
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (hunkMatch) {
|
|
358
|
+
// the hunk-header match is anchored to line start: an added, removed, or context
|
|
359
|
+
// line whose TEXT contains a hunk-header-shaped substring must not be mistaken for
|
|
360
|
+
// a real hunk boundary — that would mis-key downstream overlap math and show wrong
|
|
361
|
+
// context.
|
|
362
|
+
for (const line of parseDiffLines(patch)) {
|
|
363
|
+
if (line.kind === "hunk") {
|
|
399
364
|
if (currentHunk) hunks.push(currentHunk);
|
|
400
|
-
|
|
401
|
-
// "" default never applies); `oldC`/`newC` are the optional counts, defaulting to
|
|
402
|
-
// "1" exactly as the prior `?? "1"` did.
|
|
403
|
-
const [, oldS = "", oldC = "1", newS = "", newC = "1"] = hunkMatch;
|
|
404
|
-
currentHunk = {
|
|
405
|
-
header: line,
|
|
406
|
-
oldStart: parseInt(oldS, 10),
|
|
407
|
-
oldCount: parseInt(oldC, 10),
|
|
408
|
-
newStart: parseInt(newS, 10),
|
|
409
|
-
newCount: parseInt(newC, 10),
|
|
410
|
-
content: [],
|
|
411
|
-
};
|
|
365
|
+
currentHunk = { ...line.header, header: line.raw, content: [] };
|
|
412
366
|
} else if (currentHunk) {
|
|
413
|
-
currentHunk.content.push(line);
|
|
367
|
+
currentHunk.content.push(line.raw);
|
|
414
368
|
}
|
|
415
369
|
}
|
|
416
370
|
if (currentHunk) hunks.push(currentHunk);
|
|
@@ -1092,13 +1046,12 @@ export function ResolveReviewThreadTool(ctx: GitHubToolContext) {
|
|
|
1092
1046
|
};
|
|
1093
1047
|
} catch (error) {
|
|
1094
1048
|
// handle common error cases gracefully
|
|
1095
|
-
const
|
|
1096
|
-
const isResolved =
|
|
1097
|
-
errorMessage.includes("already resolved") || errorMessage.includes("isResolved");
|
|
1049
|
+
const msg = errorMessage(error);
|
|
1050
|
+
const isResolved = msg.includes("already resolved") || msg.includes("isResolved");
|
|
1098
1051
|
|
|
1099
1052
|
const message = isResolved
|
|
1100
1053
|
? `thread ${params.thread_id} was already resolved`
|
|
1101
|
-
: `failed to resolve thread ${params.thread_id}: ${
|
|
1054
|
+
: `failed to resolve thread ${params.thread_id}: ${msg}`;
|
|
1102
1055
|
log.info(message);
|
|
1103
1056
|
|
|
1104
1057
|
return {
|
package/src/mcp/reviewDedup.ts
CHANGED
|
@@ -75,7 +75,7 @@ export type DuplicateComment = {
|
|
|
75
75
|
/** how much of a withheld comment the note quotes back */
|
|
76
76
|
const GIST_MAX_CHARS = 120;
|
|
77
77
|
|
|
78
|
-
function gistOf(body: unknown): string | undefined {
|
|
78
|
+
export function gistOf(body: unknown): string | undefined {
|
|
79
79
|
if (typeof body !== "string") return undefined;
|
|
80
80
|
// The claim is prose. Skip fenced blocks WHOLE — a suggestion's contents are
|
|
81
81
|
// code, and `x = 1` as the summary of a withheld finding tells a reader
|
package/src/mcp/reviewMarkers.ts
CHANGED
|
@@ -16,16 +16,22 @@ import { isInfraweaver } from "#app/utils/config/payload";
|
|
|
16
16
|
|
|
17
17
|
export type CommentMarker =
|
|
18
18
|
| { kind: "finding"; key: string; proven: boolean }
|
|
19
|
-
| { kind: "judgement" }
|
|
19
|
+
| { kind: "judgement" }
|
|
20
|
+
/** a reply re-proving its thread's suggestion on a later head, at the
|
|
21
|
+
* thread's current lines — what lets the apply command take it there */
|
|
22
|
+
| { kind: "reproof"; head: string; startLine: number; line: number };
|
|
20
23
|
|
|
21
24
|
const PREFIX = `${PRODUCT_SLUG}:`;
|
|
22
|
-
const MARKER = new RegExp(`<!-- ${PREFIX}(finding|judgement)((?: [a-z]+=[^\\s]*)*) -->`);
|
|
25
|
+
const MARKER = new RegExp(`<!-- ${PREFIX}(finding|judgement|reproof)((?: [a-z]+=[^\\s]*)*) -->`);
|
|
23
26
|
|
|
24
27
|
// `-` is encoded too: `--` inside an HTML comment is not allowed to appear.
|
|
25
28
|
const enc = (s: string): string => encodeURIComponent(s).replace(/-/g, "%2D");
|
|
26
29
|
|
|
27
30
|
export function renderCommentMarker(marker: CommentMarker): string {
|
|
28
31
|
if (marker.kind === "judgement") return `<!-- ${PREFIX}judgement -->`;
|
|
32
|
+
if (marker.kind === "reproof") {
|
|
33
|
+
return `<!-- ${PREFIX}reproof head=${enc(marker.head)} start=${marker.startLine} line=${marker.line} -->`;
|
|
34
|
+
}
|
|
29
35
|
return `<!-- ${PREFIX}finding key=${enc(marker.key)}${marker.proven ? " proven=1" : ""} -->`;
|
|
30
36
|
}
|
|
31
37
|
|
|
@@ -43,6 +49,15 @@ export function parseCommentMarker(body: string | null | undefined): CommentMark
|
|
|
43
49
|
return [pair.slice(0, eq), pair.slice(eq + 1)] as const;
|
|
44
50
|
}),
|
|
45
51
|
);
|
|
52
|
+
if (m[1] === "reproof") {
|
|
53
|
+
const head = fields.get("head");
|
|
54
|
+
const startLine = Number(fields.get("start"));
|
|
55
|
+
const line = Number(fields.get("line"));
|
|
56
|
+
const valid = (n: number) => Number.isInteger(n) && n > 0;
|
|
57
|
+
if (!head || !/^[0-9a-f]{7,64}$/.test(head) || !valid(startLine) || !valid(line)) return null;
|
|
58
|
+
if (startLine > line) return null;
|
|
59
|
+
return { kind: "reproof", head, startLine, line };
|
|
60
|
+
}
|
|
46
61
|
const raw = fields.get("key");
|
|
47
62
|
if (!raw) return null;
|
|
48
63
|
try {
|
|
@@ -52,6 +67,14 @@ export function parseCommentMarker(body: string | null | undefined): CommentMark
|
|
|
52
67
|
}
|
|
53
68
|
}
|
|
54
69
|
|
|
70
|
+
const SUGGESTION = /```suggestion\r?\n([\s\S]*?)```/;
|
|
71
|
+
|
|
72
|
+
/** The text of a comment's `suggestion` block, without its closing newline. */
|
|
73
|
+
export function suggestionText(body: string): string | undefined {
|
|
74
|
+
const m = SUGGESTION.exec(body);
|
|
75
|
+
return m ? (m[1] ?? "").replace(/\r?\n$/, "") : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
/**
|
|
56
79
|
* A review comment the engine wrote: by the Infraweaver app, or by any Bot
|
|
57
80
|
* account when it carries the marker (the workflow-token path).
|
|
@@ -13,24 +13,24 @@
|
|
|
13
13
|
* one that is neither is named in the summary as not addressed, so a silently
|
|
14
14
|
* skipped finding is visible to the author rather than absent.
|
|
15
15
|
*/
|
|
16
|
-
import { type } from "arktype";
|
|
17
16
|
import { packControlsForConcern } from "#app/mcp/crosswalk";
|
|
18
17
|
import { claimsBlockingSeverity, type WithheldFinding } from "#app/mcp/findingAudit";
|
|
19
18
|
import {
|
|
20
19
|
controlsTouched,
|
|
21
20
|
type DeltaVerdict,
|
|
21
|
+
failsGate,
|
|
22
|
+
renderUndeclaredRenames,
|
|
22
23
|
reviewedFindings,
|
|
23
24
|
} from "#app/mcp/terraform/deltaSummary";
|
|
24
|
-
import
|
|
25
|
+
import { count } from "#app/mcp/terraform/prose";
|
|
26
|
+
import { type DeltaFinding, type GateDelta, undeclaredRenames } from "#app/mcp/terraform/scanDelta";
|
|
25
27
|
import { neutralizeMarkup } from "#app/mcp/untrustedContent";
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
});
|
|
33
|
-
export type Dismissal = typeof Dismissal.infer;
|
|
29
|
+
/** An introduced finding the reviewer decided needs no comment, and why. */
|
|
30
|
+
export interface Dismissal {
|
|
31
|
+
finding_id: string;
|
|
32
|
+
reason: string;
|
|
33
|
+
}
|
|
34
34
|
|
|
35
35
|
interface ProvenanceComment {
|
|
36
36
|
path: string;
|
|
@@ -103,15 +103,35 @@ const code = (text: string): string =>
|
|
|
103
103
|
.trim() || "—"
|
|
104
104
|
}\``;
|
|
105
105
|
|
|
106
|
-
/**
|
|
107
|
-
|
|
106
|
+
/**
|
|
107
|
+
* The provenance line closing an inline comment.
|
|
108
|
+
*
|
|
109
|
+
* A scanner-backed comment carries two severities, and the line says which one
|
|
110
|
+
* decides what. The marker it opens with (🚨 ⚠️ ℹ️) is the reviewer's reading of
|
|
111
|
+
* the risk in this configuration; the gate counts the scanner's severity and
|
|
112
|
+
* the required controls. When the two disagree, the line says so, so an ℹ️ that
|
|
113
|
+
* still blocks the merge — or a 🚨 that does not — reads as intended.
|
|
114
|
+
*/
|
|
115
|
+
export function provenanceLine(
|
|
116
|
+
kind: CommentKind,
|
|
117
|
+
opts: { requiredControls?: string[]; reviewerMarker?: string | null } = {},
|
|
118
|
+
): string {
|
|
108
119
|
if (kind.kind === "judgement") {
|
|
109
120
|
return "<sub>**Review judgement** — the scanners do not flag this; it is the reviewer's own reading.</sub>";
|
|
110
121
|
}
|
|
111
122
|
const c = kind.finding.concern;
|
|
112
123
|
const controls = packControlsForConcern(c);
|
|
124
|
+
const blocks = failsGate(kind.finding, opts.requiredControls ?? []);
|
|
125
|
+
const reviewerBlocks = opts.reviewerMarker === "🚨";
|
|
126
|
+
const disagreement = !opts.reviewerMarker
|
|
127
|
+
? ""
|
|
128
|
+
: blocks && !reviewerBlocks
|
|
129
|
+
? " — the reviewer rates the risk here lower, but the gate counts the scanner's severity"
|
|
130
|
+
: !blocks && reviewerBlocks
|
|
131
|
+
? " — the reviewer rates the risk here higher; the gate does not count it"
|
|
132
|
+
: "";
|
|
113
133
|
return (
|
|
114
|
-
`<sub>**Scanner:** ${code(c.rule_id)} (${c.severity})` +
|
|
134
|
+
`<sub>**Scanner:** ${code(c.rule_id)} (${c.severity} · ${blocks ? "fails the gate" : "below the gate"}${disagreement})` +
|
|
115
135
|
(controls.length ? ` · **Controls:** ${controls.map(code).join(", ")}` : "") +
|
|
116
136
|
"</sub>"
|
|
117
137
|
);
|
|
@@ -135,8 +155,6 @@ function where(f: DeltaFinding): string {
|
|
|
135
155
|
return code(c.location.line ? `${c.location.file}:${c.location.line}` : c.location.file);
|
|
136
156
|
}
|
|
137
157
|
|
|
138
|
-
const plural = (n: number, one: string, many = `${one}s`): string => `${n} ${n === 1 ? one : many}`;
|
|
139
|
-
|
|
140
158
|
/**
|
|
141
159
|
* The verdict opening a customer-PR review: whether the PR can merge as far as
|
|
142
160
|
* the gate goes, why, and what the author can do next — in plain words, from
|
|
@@ -151,24 +169,41 @@ export function renderReviewVerdict(input: {
|
|
|
151
169
|
proven: number;
|
|
152
170
|
applyCommand: string;
|
|
153
171
|
fixedThreads?: { resolved: number; unresolved: number } | undefined;
|
|
172
|
+
/** the run's `required_controls`, so findings on renamed resources can be told
|
|
173
|
+
* apart among the ones the gate fails on. */
|
|
174
|
+
requiredControls?: readonly string[];
|
|
154
175
|
}): string {
|
|
155
176
|
const { delta, verdict } = input;
|
|
156
177
|
const introduced = delta.introduced.length;
|
|
157
178
|
const blocking = verdict.gating + verdict.required;
|
|
179
|
+
const renamedBlocking = delta.introduced.filter(
|
|
180
|
+
(f) => f.possiblyPreexisting && failsGate(f, [...(input.requiredControls ?? [])]),
|
|
181
|
+
).length;
|
|
158
182
|
let lead: string;
|
|
159
|
-
if (!verdict.pass) {
|
|
183
|
+
if (!verdict.pass && renamedBlocking > 0 && renamedBlocking === blocking) {
|
|
184
|
+
// every blocker rides on a rename: one thing to do, not N findings to read
|
|
185
|
+
lead =
|
|
186
|
+
`**❌ Blocks merge** — the gate fails on ${count(blocking, "finding")}, all on resources ` +
|
|
187
|
+
"that look renamed without a `moved` block. Adding the blocks named below should clear " +
|
|
188
|
+
`${blocking === 1 ? "it" : "them"}.`;
|
|
189
|
+
} else if (!verdict.pass) {
|
|
160
190
|
lead =
|
|
161
|
-
`**❌ Blocks merge** — this pull request introduces ${
|
|
162
|
-
`the gate fails on${introduced > blocking ? `, and ${introduced - blocking} lesser` : ""}
|
|
191
|
+
`**❌ Blocks merge** — this pull request introduces ${count(blocking, "finding")} ` +
|
|
192
|
+
`the gate fails on${introduced > blocking ? `, and ${introduced - blocking} lesser` : ""}` +
|
|
193
|
+
`${renamedBlocking > 0 ? `; ${renamedBlocking} of the blocking ones ${renamedBlocking === 1 ? "sits" : "sit"} on resources that look renamed without a \`moved\` block` : ""}.`;
|
|
163
194
|
} else if (introduced > 0) {
|
|
164
|
-
lead = `**✅ Passes the gate** — ${
|
|
195
|
+
lead = `**✅ Passes the gate** — ${count(introduced, "new finding")}, none the gate fails on.`;
|
|
165
196
|
} else {
|
|
166
197
|
lead = "**✅ Passes the gate** — this pull request introduces no scanner findings.";
|
|
167
198
|
}
|
|
168
199
|
const lines = [lead];
|
|
200
|
+
// named whether or not the gate fails: a rename without `moved` is a destroy
|
|
201
|
+
// and re-create at apply, which no severity threshold speaks to.
|
|
202
|
+
const renames = renderUndeclaredRenames(undeclaredRenames(delta.introduced), code);
|
|
203
|
+
if (renames) lines.push(renames);
|
|
169
204
|
if (input.concerns > 0) {
|
|
170
205
|
lines.push(
|
|
171
|
-
`The review also raises ${
|
|
206
|
+
`The review also raises ${count(input.concerns, "concern")} the scanners cannot see — the gate does not count ${input.concerns === 1 ? "it" : "them"}, so read before merging.`,
|
|
172
207
|
);
|
|
173
208
|
}
|
|
174
209
|
if (input.proven > 0) {
|
|
@@ -179,12 +214,12 @@ export function renderReviewVerdict(input: {
|
|
|
179
214
|
const fixed = input.fixedThreads;
|
|
180
215
|
if (fixed?.resolved) {
|
|
181
216
|
lines.push(
|
|
182
|
-
`Resolved ${
|
|
217
|
+
`Resolved ${count(fixed.resolved, "earlier thread")} whose finding this push fixed.`,
|
|
183
218
|
);
|
|
184
219
|
}
|
|
185
220
|
if (fixed?.unresolved) {
|
|
186
221
|
lines.push(
|
|
187
|
-
`${
|
|
222
|
+
`${count(fixed.unresolved, "earlier thread")} ${fixed.unresolved === 1 ? "is" : "are"} fixed but still open — this workflow's token may not resolve threads (\`contents: write\`).`,
|
|
188
223
|
);
|
|
189
224
|
}
|
|
190
225
|
return `${lines.join("\n\n")}\n\n`;
|
package/src/mcp/roots.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type } from "arktype";
|
|
|
4
4
|
import type { LocalToolContext } from "#app/mcp/localContext";
|
|
5
5
|
import { parseModuleBlocks, walkTfFiles } from "#app/mcp/modules";
|
|
6
6
|
import { execute, tool } from "#app/mcp/shared";
|
|
7
|
+
import { BLOCK_LABEL } from "#app/mcp/terraform/hcl";
|
|
7
8
|
import type { SourceFile } from "#app/mcp/terraform/tree";
|
|
8
9
|
import { log } from "#app/utils/cli";
|
|
9
10
|
import { pushInto } from "#app/utils/collect";
|
|
@@ -33,7 +34,7 @@ export interface TerraformRoot {
|
|
|
33
34
|
// a provider CONFIGURATION block — `provider "aws" {`. Distinct from a
|
|
34
35
|
// `required_providers` block (which child modules also have). The negative
|
|
35
36
|
// lookbehind avoids matching `required_providers`.
|
|
36
|
-
const PROVIDER_CONFIG =
|
|
37
|
+
const PROVIDER_CONFIG = new RegExp(String.raw`(?:^|\n)\s*provider\s+${BLOCK_LABEL}\s*\{`);
|
|
37
38
|
const BACKEND_BLOCK = /(?:^|\n)\s*backend\s+"[^"]+"\s*\{/;
|
|
38
39
|
|
|
39
40
|
/** detect whether some concatenated HCL marks a root module. */
|
|
@@ -112,7 +113,7 @@ export interface RootClassification {
|
|
|
112
113
|
/** a dir that declares infrastructure — resources or module calls. Used only by
|
|
113
114
|
* the fallback, where the question is "is this a place someone runs terraform",
|
|
114
115
|
* not "what kind of module is it". */
|
|
115
|
-
const DECLARES_INFRA = /(?:^|\n)\s*(?:resource|module)\s+"/;
|
|
116
|
+
const DECLARES_INFRA = /(?:^|\n)\s*(?:resource|module)\s+(?:"|[A-Za-z_])/;
|
|
116
117
|
|
|
117
118
|
/** normalize a POSIX path, resolving `.`/`..` segments. */
|
|
118
119
|
function normalizeRel(path: string): string {
|
package/src/mcp/server.ts
CHANGED
|
@@ -47,7 +47,7 @@ import { PolicyCheckTool } from "#app/mcp/policy";
|
|
|
47
47
|
import { CreatePullRequestTool, UpdatePullRequestBodyTool } from "#app/mcp/pr";
|
|
48
48
|
import { PullRequestInfoTool } from "#app/mcp/prInfo";
|
|
49
49
|
import { TerraformProviderSchemaTool } from "#app/mcp/providerSchema";
|
|
50
|
-
import { CreatePullRequestReviewTool } from "#app/mcp/review";
|
|
50
|
+
import { CreatePullRequestReviewTool, StageReviewTool } from "#app/mcp/review";
|
|
51
51
|
import {
|
|
52
52
|
GetReviewCommentsTool,
|
|
53
53
|
ListPullRequestReviewsTool,
|
|
@@ -186,6 +186,7 @@ function buildCommonTools(ctx: ToolContext, outputSchema?: JsonSchema): Infrawea
|
|
|
186
186
|
GetIssueCommentsTool(ctx),
|
|
187
187
|
GetIssueEventsTool(ctx),
|
|
188
188
|
CreatePullRequestReviewTool(ctx),
|
|
189
|
+
StageReviewTool(ctx),
|
|
189
190
|
PullRequestInfoTool(ctx),
|
|
190
191
|
CommitInfoTool(ctx),
|
|
191
192
|
CheckoutPrTool(ctx),
|
package/src/mcp/shared.ts
CHANGED
|
@@ -246,10 +246,49 @@ function instrument(tool: InfraweaverTool): InfraweaverTool {
|
|
|
246
246
|
} as InfraweaverTool;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
/**
|
|
250
|
+
* How often a running tool tells the client it is still working. Claude Code
|
|
251
|
+
* aborts a call that sends neither a response nor a progress notification for
|
|
252
|
+
* five minutes, and a scan that downloads external modules, or a plan, can run
|
|
253
|
+
* longer than that while working normally.
|
|
254
|
+
*/
|
|
255
|
+
export const PROGRESS_HEARTBEAT_MS = 30_000;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Send a progress notification every {@link PROGRESS_HEARTBEAT_MS} while the
|
|
259
|
+
* tool runs, carrying the seconds elapsed (the spec requires `progress` to
|
|
260
|
+
* increase). It only reaches the client while the event loop is free, so a tool
|
|
261
|
+
* that blocks on a synchronous subprocess still looks idle — which is why the
|
|
262
|
+
* Terraform and scanner paths spawn asynchronously.
|
|
263
|
+
*/
|
|
264
|
+
export function withProgressHeartbeat(tool: InfraweaverTool): InfraweaverTool {
|
|
265
|
+
if (typeof tool.execute !== "function") return tool;
|
|
266
|
+
const inner = tool.execute.bind(tool);
|
|
267
|
+
return {
|
|
268
|
+
...tool,
|
|
269
|
+
execute: async (...args: Parameters<typeof inner>) => {
|
|
270
|
+
const [, context] = args;
|
|
271
|
+
const started = Date.now();
|
|
272
|
+
const timer = setInterval(() => {
|
|
273
|
+
const seconds = Math.round((Date.now() - started) / 1000);
|
|
274
|
+
void context.reportProgress({ progress: seconds, message: `${tool.name}: ${seconds}s` });
|
|
275
|
+
}, PROGRESS_HEARTBEAT_MS);
|
|
276
|
+
timer.unref?.();
|
|
277
|
+
try {
|
|
278
|
+
return await inner(...args);
|
|
279
|
+
} finally {
|
|
280
|
+
clearInterval(timer);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
} as InfraweaverTool;
|
|
284
|
+
}
|
|
285
|
+
|
|
249
286
|
export const addTools = (ctx: ToolContext, server: FastMCP<any>, tools: InfraweaverTool[]) => {
|
|
250
287
|
const shouldSanitize = isGeminiRouted(ctx);
|
|
251
288
|
for (const tool of tools) {
|
|
252
|
-
server.addTool(
|
|
289
|
+
server.addTool(
|
|
290
|
+
withProgressHeartbeat(instrument(shouldSanitize ? sanitizeToolForGemini(tool) : tool)),
|
|
291
|
+
);
|
|
253
292
|
}
|
|
254
293
|
return server;
|
|
255
294
|
};
|
package/src/mcp/shell.ts
CHANGED
|
@@ -185,16 +185,15 @@ const SOCKET_CLEANUP = [
|
|
|
185
185
|
// let the agent bypass the .git/* binds. resolveRepoRoot pins the actual
|
|
186
186
|
// repo root once at startup via $GITHUB_WORKSPACE or `git rev-parse`.
|
|
187
187
|
function buildFsMounts(repoDir: string): string {
|
|
188
|
-
// shell-escape via single-quote wrap
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
const escaped = repoDir.replace(/'/g, "'\\''");
|
|
188
|
+
// shell-escape via single-quote wrap. repoDir paths in practice are GHA
|
|
189
|
+
// workspace paths (no quotes), but the escape keeps us correct against
|
|
190
|
+
// arbitrary user-configured workspaces.
|
|
191
|
+
const gitDir = shSingleQuote(`${repoDir}/.git`);
|
|
193
192
|
return [
|
|
194
193
|
`mkdir -p /var/lib/infraweaver 2>/dev/null;`,
|
|
195
194
|
`mount -t tmpfs tmpfs /var/lib/infraweaver 2>/dev/null;`,
|
|
196
195
|
`[ -n "$RUNNER_TEMP" ] && [ -d "$RUNNER_TEMP/_runner_file_commands" ] && mount -t tmpfs tmpfs "$RUNNER_TEMP/_runner_file_commands" 2>/dev/null;`,
|
|
197
|
-
`[ -e
|
|
196
|
+
`[ -e ${gitDir} ] && mount --bind ${gitDir} ${gitDir} 2>/dev/null && mount -o remount,bind,ro ${gitDir} 2>/dev/null;`,
|
|
198
197
|
// the corepack shim dir sits FIRST on PATH (utils/setup/packageManager.ts)
|
|
199
198
|
// and PATH survives filterEnv, so a writable pm-bin lets sandboxed code
|
|
200
199
|
// plant a `git`/`pnpm` that a later UNSANDBOXED spawn resolves with the
|
|
@@ -215,8 +214,8 @@ function buildFsMounts(repoDir: string): string {
|
|
|
215
214
|
function shimMount(): string {
|
|
216
215
|
const tempDir = readTempDir();
|
|
217
216
|
if (!tempDir) return "";
|
|
218
|
-
const dir = packageManagerBinDir(tempDir)
|
|
219
|
-
return `[ -d
|
|
217
|
+
const dir = shSingleQuote(packageManagerBinDir(tempDir));
|
|
218
|
+
return `[ -d ${dir} ] && mount --bind ${dir} ${dir} 2>/dev/null && mount -o remount,bind,ro ${dir} 2>/dev/null;`;
|
|
220
219
|
}
|
|
221
220
|
|
|
222
221
|
/** locate the repo root once at action startup. process.cwd() is unreliable
|
|
@@ -313,7 +312,6 @@ function spawnShell(params: SpawnParams): ChildProcess {
|
|
|
313
312
|
params.timeoutMs && params.timeoutMs > 0
|
|
314
313
|
? `timeout --kill-after=5 --signal=TERM ${Math.ceil(params.timeoutMs / 1000)}s bash -c ${shSingleQuote(params.command)}`
|
|
315
314
|
: params.command;
|
|
316
|
-
const escaped = (pathRestore + userCommand).replace(/'/g, "'\\''");
|
|
317
315
|
envArgs.push(`SANDBOX_PATH=${params.env.PATH ?? ""}`);
|
|
318
316
|
return spawn(
|
|
319
317
|
"sudo",
|
|
@@ -326,7 +324,7 @@ function spawnShell(params: SpawnParams): ChildProcess {
|
|
|
326
324
|
"--mount-proc",
|
|
327
325
|
"bash",
|
|
328
326
|
"-c",
|
|
329
|
-
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} exec su -p -s /bin/bash ${username} -c
|
|
327
|
+
`${PROC_CLEANUP} ${SOCKET_CLEANUP} ${fsMounts} exec su -p -s /bin/bash ${username} -c ${shSingleQuote(pathRestore + userCommand)}`,
|
|
330
328
|
],
|
|
331
329
|
{ ...spawnOpts, env: {} },
|
|
332
330
|
);
|
|
@@ -352,13 +350,20 @@ function sudoKillGroup(pid: number, signal: "SIGTERM" | "SIGKILL"): void {
|
|
|
352
350
|
}
|
|
353
351
|
}
|
|
354
352
|
|
|
353
|
+
/** SIGTERM a process group, give it 200ms to exit cleanly, then SIGKILL it.
|
|
354
|
+
* `signal` delivers one signal to the group; a throw from it propagates. */
|
|
355
|
+
async function termThenKill(signal: (sig: "SIGTERM" | "SIGKILL") => void): Promise<void> {
|
|
356
|
+
signal("SIGTERM");
|
|
357
|
+
await sleep(200);
|
|
358
|
+
signal("SIGKILL");
|
|
359
|
+
}
|
|
360
|
+
|
|
355
361
|
/** kill process and its entire process group */
|
|
356
362
|
async function killProcessGroup(proc: ChildProcess): Promise<void> {
|
|
357
|
-
|
|
363
|
+
const pid = proc.pid;
|
|
364
|
+
if (!pid) return;
|
|
358
365
|
try {
|
|
359
|
-
process.kill(-
|
|
360
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
361
|
-
process.kill(-proc.pid, "SIGKILL");
|
|
366
|
+
await termThenKill((sig) => process.kill(-pid, sig));
|
|
362
367
|
} catch {
|
|
363
368
|
try {
|
|
364
369
|
proc.kill("SIGKILL");
|
|
@@ -652,21 +657,15 @@ export function KillBackgroundTool(ctx: ToolContext) {
|
|
|
652
657
|
// under sudo-unshare the group is root-owned — process.kill(-pid) EPERMs,
|
|
653
658
|
// so escalate straight through sudo. otherwise the unprivileged kill works.
|
|
654
659
|
if (getSandboxMethod() === "sudo-unshare") {
|
|
655
|
-
sudoKillGroup(proc.pid,
|
|
656
|
-
await sleep(200);
|
|
657
|
-
sudoKillGroup(proc.pid, "SIGKILL");
|
|
660
|
+
await termThenKill((sig) => sudoKillGroup(proc.pid, sig));
|
|
658
661
|
} else {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
process.kill(-proc.pid, "SIGKILL");
|
|
667
|
-
} catch {
|
|
668
|
-
// already dead
|
|
669
|
-
}
|
|
662
|
+
await termThenKill((sig) => {
|
|
663
|
+
try {
|
|
664
|
+
process.kill(-proc.pid, sig);
|
|
665
|
+
} catch {
|
|
666
|
+
// already dead
|
|
667
|
+
}
|
|
668
|
+
});
|
|
670
669
|
}
|
|
671
670
|
|
|
672
671
|
ctx.toolState.backgroundProcesses.delete(params.handle);
|
|
@@ -50,8 +50,9 @@ export interface SuggestionProof {
|
|
|
50
50
|
const MAX_SINGLE_PROOFS = 4;
|
|
51
51
|
|
|
52
52
|
export interface ProverDeps {
|
|
53
|
-
/** a gate-configured scan of the tree at `cwd`, ACTIVE concerns only
|
|
54
|
-
|
|
53
|
+
/** a gate-configured scan of the tree at `cwd`, ACTIVE concerns only, covering
|
|
54
|
+
* at least the root modules that `files` (relative to `cwd`) reach */
|
|
55
|
+
scan: (cwd: string, files: string[]) => Promise<Concern[]>;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
/**
|
|
@@ -87,14 +88,17 @@ export async function proveSuggestions(
|
|
|
87
88
|
return input.candidates.map((c) => out.get(c.index) as SuggestionProof);
|
|
88
89
|
}
|
|
89
90
|
const scanCwd = prefix ? join(worktree, prefix) : worktree;
|
|
91
|
+
// a suggestion can change findings only in the roots its file reaches, so
|
|
92
|
+
// both sides of each comparison scan those
|
|
93
|
+
const files = [...new Set(ready.map((c) => c.path.slice(prefix.length)))];
|
|
90
94
|
try {
|
|
91
|
-
const before = await deps.scan(scanCwd);
|
|
95
|
+
const before = await deps.scan(scanCwd, files);
|
|
92
96
|
// review paths are repo-relative; concern paths are relative to the scan dir
|
|
93
97
|
const pristine = snapshot(worktree, prefix, ready);
|
|
94
98
|
const judge = async (subset: SuggestionCandidate[]) => {
|
|
95
99
|
restore(worktree, prefix, pristine);
|
|
96
100
|
for (const [path, group] of groupByPath(subset)) apply(worktree, path, group);
|
|
97
|
-
const after = await deps.scan(scanCwd);
|
|
101
|
+
const after = await deps.scan(scanCwd, files);
|
|
98
102
|
return {
|
|
99
103
|
after,
|
|
100
104
|
delta: computeScanDelta({
|