infraweaver 0.3.12 → 0.3.13
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 +1 -1
- package/dist/agents/opencode.d.ts +6 -0
- package/dist/agents/quotaTokens.d.ts +6 -0
- package/dist/cli.mjs +554 -199
- package/dist/index.js +531 -176
- package/dist/internal.js +4 -4
- package/dist/mcp/git.d.ts +6 -4
- package/dist/mcp/localContext.d.ts +7 -0
- package/dist/mcp/reviewCommentLimit.d.ts +55 -5
- package/dist/mcp/reviewMarkers.d.ts +5 -0
- package/dist/mcp/staleFix.d.ts +36 -1
- package/dist/mcp/terraform/concernResult.d.ts +9 -0
- package/dist/mcp/terraform/deltaSummary.d.ts +3 -0
- package/dist/mcp/terraform/hardcodedSecrets.d.ts +24 -10
- package/dist/mcp/terraform/refactorScope.d.ts +14 -2
- package/dist/mcp/terraform/ruleContext.d.ts +3 -0
- package/dist/mcp/terraform/tree.d.ts +5 -0
- package/dist/mcp/terraform/types.d.ts +2 -1
- package/dist/mcp/terraform/verifySites.d.ts +11 -3
- package/dist/vendor/@infraweaver-io/models/tokenQuota.d.ts +7 -6
- package/package.json +2 -2
- package/src/agents/opencode.ts +8 -1
- package/src/agents/quotaTokens.ts +7 -1
- package/src/mcp/git.ts +64 -12
- package/src/mcp/localContext.ts +6 -1
- package/src/mcp/moduleExtraction.ts +5 -4
- package/src/mcp/review.ts +52 -3
- package/src/mcp/reviewCommentLimit.ts +169 -26
- package/src/mcp/reviewMarkers.ts +11 -0
- package/src/mcp/reviewProvenance.ts +1 -1
- package/src/mcp/staleFix.ts +73 -2
- package/src/mcp/terraform/concernResult.ts +17 -10
- package/src/mcp/terraform/deltaSummary.ts +4 -0
- package/src/mcp/terraform/hardcodedSecrets.ts +148 -27
- package/src/mcp/terraform/nativeRuleScanners.ts +2 -1
- package/src/mcp/terraform/nativeScan.ts +4 -0
- package/src/mcp/terraform/refactor/equivalence.ts +42 -9
- package/src/mcp/terraform/refactorScope.ts +90 -21
- package/src/mcp/terraform/ruleContext.ts +4 -0
- package/src/mcp/terraform/scanDelta.ts +67 -26
- package/src/mcp/terraform/tools/detectPlanDrift.ts +2 -0
- package/src/mcp/terraform/tools/ingestExternalFindings.ts +2 -0
- package/src/mcp/terraform/tools/normalizationCandidates.ts +5 -5
- package/src/mcp/terraform/tools/readFindings.ts +2 -0
- package/src/mcp/terraform/tools/scan.ts +3 -0
- package/src/mcp/terraform/tree.ts +8 -0
- package/src/mcp/terraform/types.ts +2 -1
- package/src/mcp/terraform/verifySites.ts +23 -5
- package/src/modes/refactor.ts +1 -1
- package/src/modes/remediate-and-refactor.ts +1 -1
- package/src/modes/remediate.ts +1 -1
- package/src/phases/runAgentWithWatchdogs.ts +3 -2
|
@@ -275,38 +275,79 @@ export function computeScanDelta(input: ScanDeltaInput): ScanDelta {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
/**
|
|
278
|
-
* Pair
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
278
|
+
* Pair a NEW resource with a vanished one — a resource whose findings resolved
|
|
279
|
+
* because it no longer exists where it was: renamed within its module, or moved
|
|
280
|
+
* under the same name to another directory (a resource extracted into a module
|
|
281
|
+
* without a `moved` block).
|
|
282
|
+
*
|
|
283
|
+
* Resources pair one-to-one, as `moved` blocks do: Terraform rejects two blocks
|
|
284
|
+
* with the same `from` or the same `to`, so a pairing made per finding — one new
|
|
285
|
+
* bucket matched to two old ones because each shared a different rule — would
|
|
286
|
+
* advise blocks that cannot be written. Each candidate pair scores the findings
|
|
287
|
+
* the two resources share (rule by rule, instance by instance), the best pairs
|
|
288
|
+
* are taken first, and ties fall to address order so the result is stable. Only
|
|
289
|
+
* the shared findings of a pair are labelled; a rule the vanished resource never
|
|
290
|
+
* broke is new on the renamed one too.
|
|
283
291
|
*/
|
|
284
292
|
function markPossibleRenames(
|
|
285
293
|
introduced: DeltaFinding[],
|
|
286
294
|
resolved: DeltaFinding[],
|
|
287
295
|
headAddresses: (dir: string) => ReadonlySet<string>,
|
|
288
296
|
): void {
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
297
|
+
const byResource = (findings: DeltaFinding[]) => {
|
|
298
|
+
const out = new Map<string, { dir: string; address: string; findings: DeltaFinding[] }>();
|
|
299
|
+
for (const f of [...findings].sort((a, b) => byLocation(a.concern, b.concern))) {
|
|
300
|
+
if (f.address === null) continue;
|
|
301
|
+
const dir = dirOf(f.concern.location.file);
|
|
302
|
+
const key = `${dir}|${f.address}`;
|
|
303
|
+
const entry = out.get(key);
|
|
304
|
+
if (entry) entry.findings.push(f);
|
|
305
|
+
else out.set(key, { dir, address: f.address, findings: [f] });
|
|
306
|
+
}
|
|
307
|
+
return [...out.values()];
|
|
308
|
+
};
|
|
309
|
+
const vanished = byResource(resolved).filter((r) => !headAddresses(r.dir).has(r.address));
|
|
310
|
+
const appeared = byResource(introduced);
|
|
311
|
+
|
|
312
|
+
// the findings of `to` that match one of `from`'s, each of `from`'s used once
|
|
313
|
+
const shared = (from: (typeof vanished)[number], to: (typeof appeared)[number]) => {
|
|
314
|
+
const left = new Map<string, number>();
|
|
315
|
+
for (const f of from.findings)
|
|
316
|
+
left.set(ruleKey(f.concern), (left.get(ruleKey(f.concern)) ?? 0) + 1);
|
|
317
|
+
return to.findings.filter((f) => {
|
|
318
|
+
const n = left.get(ruleKey(f.concern)) ?? 0;
|
|
319
|
+
if (n === 0) return false;
|
|
320
|
+
left.set(ruleKey(f.concern), n - 1);
|
|
321
|
+
return true;
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
const order = (a: { dir: string; address: string }, b: { dir: string; address: string }) =>
|
|
325
|
+
a.dir.localeCompare(b.dir) || a.address.localeCompare(b.address);
|
|
326
|
+
const candidates = vanished.flatMap((from) =>
|
|
327
|
+
appeared
|
|
328
|
+
.filter(
|
|
329
|
+
(to) =>
|
|
330
|
+
// a `moved` block cannot change a resource's type
|
|
331
|
+
typeOf(from.address) === typeOf(to.address) &&
|
|
332
|
+
// renamed in place, or moved elsewhere under its own name
|
|
333
|
+
(from.dir === to.dir) !== (from.address === to.address),
|
|
334
|
+
)
|
|
335
|
+
.map((to) => ({ from, to, findings: shared(from, to) }))
|
|
336
|
+
.filter((p) => p.findings.length > 0),
|
|
337
|
+
);
|
|
338
|
+
candidates.sort(
|
|
339
|
+
(a, b) => b.findings.length - a.findings.length || order(a.from, b.from) || order(a.to, b.to),
|
|
340
|
+
);
|
|
341
|
+
const used = new Set<object>();
|
|
342
|
+
for (const { from, to, findings } of candidates) {
|
|
343
|
+
if (used.has(from) || used.has(to)) continue;
|
|
344
|
+
used.add(from);
|
|
345
|
+
used.add(to);
|
|
346
|
+
const file = from.findings[0]?.concern.location.file as string;
|
|
347
|
+
for (const finding of findings) {
|
|
348
|
+
finding.possiblyPreexisting = true;
|
|
349
|
+
finding.renamedFrom = { address: from.address, file };
|
|
350
|
+
}
|
|
310
351
|
}
|
|
311
352
|
}
|
|
312
353
|
|
|
@@ -6,6 +6,7 @@ import { PRODUCT_NAME } from "#app/brand";
|
|
|
6
6
|
import type { LocalToolContext } from "#app/mcp/localContext";
|
|
7
7
|
import { walkTfFiles } from "#app/mcp/modules";
|
|
8
8
|
import { execute, tool, toolOk } from "#app/mcp/shared";
|
|
9
|
+
import { listOpenRemediationPrs } from "#app/mcp/staleFix";
|
|
9
10
|
import { buildConcernResult } from "#app/mcp/terraform/concernResult";
|
|
10
11
|
import { indexTfResources } from "#app/mcp/terraform/cspm";
|
|
11
12
|
import { docUrlsForGroup, ruleDocUrl } from "#app/mcp/terraform/decisions";
|
|
@@ -180,6 +181,7 @@ export function DetectPlanDriftTool(ctx: LocalToolContext) {
|
|
|
180
181
|
// drift is a narrow concern source — join any prior scan baseline so a
|
|
181
182
|
// later ✗→✓ verify doesn't read the whole workspace as regressions.
|
|
182
183
|
baselineMode: "merge",
|
|
184
|
+
openRemediationPrs: await listOpenRemediationPrs(ctx),
|
|
183
185
|
});
|
|
184
186
|
|
|
185
187
|
log.info(
|
|
@@ -7,6 +7,7 @@ import { PRODUCT_NAME } from "#app/brand";
|
|
|
7
7
|
import type { LocalToolContext } from "#app/mcp/localContext";
|
|
8
8
|
import { readTextCapped, walkTfFiles } from "#app/mcp/modules";
|
|
9
9
|
import { execute, tool, toolOk } from "#app/mcp/shared";
|
|
10
|
+
import { listOpenRemediationPrs } from "#app/mcp/staleFix";
|
|
10
11
|
import { buildConcernResult } from "#app/mcp/terraform/concernResult";
|
|
11
12
|
import { cappedSummary, SeedCapParams, seedCapsFrom } from "#app/mcp/terraform/coverage";
|
|
12
13
|
import {
|
|
@@ -220,6 +221,7 @@ export function IngestExternalFindingsTool(ctx: LocalToolContext) {
|
|
|
220
221
|
// baseline so a later ✗→✓ verify doesn't read the workspace as regressions.
|
|
221
222
|
baselineMode: "merge",
|
|
222
223
|
seedCaps: seedCapsFrom(input),
|
|
224
|
+
openRemediationPrs: await listOpenRemediationPrs(ctx),
|
|
223
225
|
});
|
|
224
226
|
const cappedBlock = cappedSummary(capped);
|
|
225
227
|
|
|
@@ -2,7 +2,7 @@ import { type } from "arktype";
|
|
|
2
2
|
import type { LocalToolContext } from "#app/mcp/localContext";
|
|
3
3
|
import { execute, tool, toolOk } from "#app/mcp/shared";
|
|
4
4
|
import { findNormalizationCandidates } from "#app/mcp/terraform/normalization";
|
|
5
|
-
import { isExamplePath,
|
|
5
|
+
import { isExamplePath, libraryScope, reportOnly } from "#app/mcp/terraform/refactorScope";
|
|
6
6
|
import { log } from "#app/utils/cli";
|
|
7
7
|
|
|
8
8
|
// --- the tool ----------------------------------------------------------------
|
|
@@ -29,15 +29,15 @@ export function NormalizationCandidatesTool(ctx: LocalToolContext) {
|
|
|
29
29
|
parameters: NormalizationCandidatesParams,
|
|
30
30
|
execute: execute(async () => {
|
|
31
31
|
const cwd = ctx.payload.cwd ?? process.cwd();
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
const library = libraryScope(cwd);
|
|
33
|
+
const files = findNormalizationCandidates(cwd)
|
|
34
|
+
.filter((f) => ctx.payload.refactorExamples || !isExamplePath(f.file))
|
|
35
|
+
.map((f) => ({ ...reportOnly(library(f.file)), ...f }));
|
|
35
36
|
const total = files.reduce((n, f) => n + f.count, 0);
|
|
36
37
|
log.info(
|
|
37
38
|
`» terraform_normalization_candidates: ${total} site(s) across ${files.length} file(s)`,
|
|
38
39
|
);
|
|
39
40
|
return toolOk({
|
|
40
|
-
...reportOnly(libraryRepoReason(cwd)),
|
|
41
41
|
file_count: files.length,
|
|
42
42
|
total,
|
|
43
43
|
files,
|
|
@@ -4,6 +4,7 @@ import { type } from "arktype";
|
|
|
4
4
|
import { PRODUCT_NAME } from "#app/brand";
|
|
5
5
|
import type { LocalToolContext } from "#app/mcp/localContext";
|
|
6
6
|
import { execute, tool, toolOk } from "#app/mcp/shared";
|
|
7
|
+
import { listOpenRemediationPrs } from "#app/mcp/staleFix";
|
|
7
8
|
import { buildConcernResult } from "#app/mcp/terraform/concernResult";
|
|
8
9
|
import { cappedSummary, SeedCapParams, seedCapsFrom } from "#app/mcp/terraform/coverage";
|
|
9
10
|
import { docUrlsForGroup, ruleDocUrl } from "#app/mcp/terraform/decisions";
|
|
@@ -102,6 +103,7 @@ export function ReadFindingsTool(ctx: LocalToolContext) {
|
|
|
102
103
|
grouping,
|
|
103
104
|
autonomyThreshold,
|
|
104
105
|
seedCaps: seedCapsFrom(input),
|
|
106
|
+
openRemediationPrs: await listOpenRemediationPrs(ctx),
|
|
105
107
|
});
|
|
106
108
|
const cappedBlock = cappedSummary(capped);
|
|
107
109
|
|
|
@@ -4,6 +4,7 @@ import { type } from "arktype";
|
|
|
4
4
|
import type { LocalToolContext } from "#app/mcp/localContext";
|
|
5
5
|
import { walkTfFiles } from "#app/mcp/modules";
|
|
6
6
|
import { execute, tool, toolOk } from "#app/mcp/shared";
|
|
7
|
+
import { listOpenRemediationPrs } from "#app/mcp/staleFix";
|
|
7
8
|
import { loadBaseline, loadResolvedLedger } from "#app/mcp/terraform/baseline";
|
|
8
9
|
import { buildConcernResult } from "#app/mcp/terraform/concernResult";
|
|
9
10
|
import {
|
|
@@ -148,6 +149,8 @@ export async function runScan(
|
|
|
148
149
|
autonomyThreshold,
|
|
149
150
|
inScope,
|
|
150
151
|
priority,
|
|
152
|
+
openRemediationPrs: await listOpenRemediationPrs(ctx),
|
|
153
|
+
linesMatchTree: true,
|
|
151
154
|
},
|
|
152
155
|
);
|
|
153
156
|
const { active, suppressed, groups, batchPlan, bySeverity: by_severity } = result;
|
|
@@ -176,6 +176,14 @@ export function readTofuTree(cwd: string, limits?: TreeReadLimits): SourceTree {
|
|
|
176
176
|
return readTree(cwd, ".tofu", limits);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
/**
|
|
180
|
+
* The `.tfvars` inputs (`.auto.tfvars` included). Not configuration, so no rule
|
|
181
|
+
* reads them as blocks; the hardcoded-secret rule reads their assignments.
|
|
182
|
+
*/
|
|
183
|
+
export function readTfvarsTree(cwd: string, limits?: TreeReadLimits): SourceTree {
|
|
184
|
+
return readTree(cwd, ".tfvars", limits);
|
|
185
|
+
}
|
|
186
|
+
|
|
179
187
|
/** How many oversize paths a gap names before it stops listing them. A gap is a
|
|
180
188
|
* sentence in a report, not a manifest; past a handful the count carries the
|
|
181
189
|
* fact and the list only buries it. */
|
|
@@ -399,7 +399,8 @@ export interface ConcernGroup {
|
|
|
399
399
|
* the file (by-file grouping) or the rule (by-rule grouping). */
|
|
400
400
|
id: string;
|
|
401
401
|
/** the branch its fix is pushed to: `remediate/<id>`, plus `--<base>` when the
|
|
402
|
-
* PR targets a branch other than the repository default
|
|
402
|
+
* PR targets a branch other than the repository default — or the head of the
|
|
403
|
+
* remediation PR already open for this group into the same base. */
|
|
403
404
|
branch?: string;
|
|
404
405
|
/** the group's primary file (by-file) or a human label like "3 files"
|
|
405
406
|
* (by-rule); `files` carries the full list for by-rule groups. */
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* a fix that moves the defect onto a new resource would look resolved — so a
|
|
9
9
|
* finding counts as resolved at its own site only when ALL of these hold:
|
|
10
10
|
*
|
|
11
|
-
* - it was placed on a resource when it was reported
|
|
11
|
+
* - it was placed on a resource when it was reported, by a scan of the tree
|
|
12
|
+
* as it then stood, and no later scan placed the same id elsewhere;
|
|
12
13
|
* - that resource is still declared in the file (a renamed or deleted one
|
|
13
14
|
* proves nothing);
|
|
14
15
|
* - the rule no longer fires on it;
|
|
@@ -48,16 +49,33 @@ export function siteOf(c: Concern, read: (file: string) => string | null): strin
|
|
|
48
49
|
return text === null ? null : resourceAddressAt(text, c.location.line);
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Remember where each reported concern sat, and the first sites per key.
|
|
54
|
+
*
|
|
55
|
+
* `linesMatchTree` says the concerns were scanned off the tree in `cwd` as it
|
|
56
|
+
* is now. Without it (a findings file or external report, which may describe
|
|
57
|
+
* another commit) their lines are not trusted to name today's resources: their
|
|
58
|
+
* ids get no site and their keys no first sites.
|
|
59
|
+
*/
|
|
60
|
+
export function recordConcernSites(
|
|
61
|
+
session: ScanSession,
|
|
62
|
+
concerns: Concern[],
|
|
63
|
+
cwd: string,
|
|
64
|
+
linesMatchTree: boolean,
|
|
65
|
+
): void {
|
|
53
66
|
const read = fileReader(cwd);
|
|
54
67
|
session.siteById ??= new Map();
|
|
55
68
|
session.firstSitesByKey ??= new Map();
|
|
56
69
|
const first = session.firstSitesByKey;
|
|
57
70
|
const fresh = new Map<string, Set<string | null>>();
|
|
58
71
|
for (const c of concerns) {
|
|
59
|
-
const site = siteOf(c, read);
|
|
60
|
-
|
|
72
|
+
const site = linesMatchTree ? siteOf(c, read) : null;
|
|
73
|
+
// an id is line-pinned, so after an edit a re-scan can report a different
|
|
74
|
+
// finding under it. It keeps the site it was first reported at; placed
|
|
75
|
+
// anywhere else later, it is ambiguous and gets none.
|
|
76
|
+
const prior = session.siteById.get(c.id);
|
|
77
|
+
session.siteById.set(c.id, prior === undefined || prior === site ? site : null);
|
|
78
|
+
if (!linesMatchTree) continue;
|
|
61
79
|
const key = concernKeyOf(c);
|
|
62
80
|
if (first.has(key)) continue;
|
|
63
81
|
const sites = fresh.get(key) ?? new Set();
|
package/src/modes/refactor.ts
CHANGED
|
@@ -18,7 +18,7 @@ This mode standardises STRUCTURE while preserving BEHAVIOUR — the equivalence
|
|
|
18
18
|
- \`${t("terraform_normalization_candidates")}\` → in-place idiomatic cleanups that change SYNTAX, not behaviour (redundant whole-string \`"\${expr}"\` interpolation; legacy HCL0.11 map-argument block syntax like \`vars {\` on a \`template_file\`). These relocate NOTHING — zero \`moved {}\` blocks — and are proven by the same equivalence check.
|
|
19
19
|
- \`${t("terraform_consolidation_candidates")}\` → the SAME resource shape declared in several root directories (\`env/dev\` + \`env/staging\` + \`env/prod\`), with the single parameterised module they could all call already derived: which attributes are identical (module content) and which differ (per-environment inputs). A member root of a group will ALSO show up as a zero-candidate cluster in \`${t("module_extraction_candidates")}\` — prefer the consolidation, which does that work once for every environment instead of once per environment. Its \`near_groups\` are the opposite case: roots recognisably the same stack that DIFFER (one carries a resource the other does not), where folding them together is a design decision this mode has no basis for. Do NOT extract a near-group root's shared resources into a module on its own either — a module one twin calls and the other does not widens exactly the drift the near-group reports. Leave those roots alone and name the near-group and its \`differences\` in your report.
|
|
20
20
|
|
|
21
|
-
**A
|
|
21
|
+
**A candidate marked \`report_only\` is an observation, not work.** It sits in the code of a module other configurations call (\`report_only_reason\` says why), and restructuring that is churn for every caller: name those candidates in your \`${t("report_progress")}\` summary and open no PR for them. \`examples/\` roots are left out of every candidate list unless the operator set \`refactor_examples\`.
|
|
22
22
|
|
|
23
23
|
**Before picking, check the interface.** \`${t("module_extraction_candidates")}\` reports \`unmapped_attributes\` per candidate and a top-level \`partial_interfaces\` list: arguments the raw resources SET that the target module does not. Adopting such a module DROPS them silently — and the equivalence check cannot catch it, because once the argument is gone it is absent from both sides. Two of these are unrecoverable rather than merely wrong (\`object_lock_enabled\` cannot be set after creation; \`force_destroy\` changes deletion semantics). So: **decline the candidate, or extend the module to expose the argument and say so in the PR body.** Deleting the argument to make the shapes match is never the fix. A candidate whose \`unmapped_attributes\` is \`null\` was NOT checked — the module is external and its code is not in this repo — which is not the same as clean.
|
|
24
24
|
|
|
@@ -20,7 +20,7 @@ This mode composes the **Remediate** and **Refactor** verbs into a single run. I
|
|
|
20
20
|
- **commit the fix** (\`git add\` only the changed \`*.tf\`/\`*.tfvars\`, a \`fix(tf): …\` message). Do NOT push yet. This committed state is the refactor's equivalence BASELINE.
|
|
21
21
|
|
|
22
22
|
3. **REFACTOR (phase 2) — modularise on top of the committed fix**: follow the **Refactor** mode's flow:
|
|
23
|
-
- \`${t("module_extraction_candidates")}\` / \`${t("terraform_normalization_candidates")}\` → pick one behaviour-preserving refactor; a
|
|
23
|
+
- \`${t("module_extraction_candidates")}\` / \`${t("terraform_normalization_candidates")}\` → pick one behaviour-preserving refactor; a candidate marked \`report_only\` (it sits in the code of a module others call) is named in the summary, never proposed. Respect a pinned \`refactor_source\` if one was supplied (it constrains which module source you may use; the behaviour-altering b.4 path is only available when the operator pinned \`third-party\`).
|
|
24
24
|
- resolve the module source, wire the \`module\` call against its real interface (\`${t("terraform_module_interface")}\` for a local module dir, \`${t("terraform_module_lookup")}\` for a registry-sourced one), and emit a \`moved {}\` block for EVERY relocated address (\`${t("terraform_generate_moved")}\`).
|
|
25
25
|
- \`terraform fmt\` + \`${t("terraform_validate")}\`, then — **with the refactor edits still UNCOMMITTED** — call \`${t("terraform_equivalence_check")}\`. In this mode it diffs the working tree against the COMMITTED fix (not the run-start commit), so it measures only the refactor. It must return \`equivalent: true\` (zero uncovered moves, resource set + arguments preserved, validate + fmt clean) before you proceed; \`${t("push_branch")}\` hard-blocks an unproven one — and, once this run has added a \`moved {}\` block, it also hard-blocks a push that never ran the check at all, so abandoning the refactor means REVERTING its edits rather than leaving them in with the fix. If it can't be proven equivalent, abandon the refactor and ship the fix alone.
|
|
26
26
|
- **document the module (only when the \`docs\` input is enabled)**: after the equivalence check passes, call \`${t("terraform_module_docs")}\` for the module you created/adopted (pass the relocated addresses as \`moves\`). Do not hand-write docs — the tool builds from the interface, moved blocks, and equivalence verdict.
|
package/src/modes/remediate.ts
CHANGED
|
@@ -34,7 +34,7 @@ export function remediateMode(t: ToolRef): Mode {
|
|
|
34
34
|
|
|
35
35
|
4. **for the chosen group**:
|
|
36
36
|
- **base branch**: this run's base branch is resolved deterministically — \`${t("create_pull_request")}\` targets the \`base_branch\` input if set, else the branch the run started on, else the repository's default branch (\`main\`, or \`master\`). You do not choose it; just **omit** the \`base\` argument when opening the PR (below) and it is filled in.
|
|
37
|
-
- **idempotency**: the remediation branch is the group's \`branch\` (\`remediate/<group-id>\`, with a \`--<base>\` suffix when the PR targets a non-default branch) — use it exactly, never a name of your own. Before doing anything, check whether that branch or an open PR for it already exists (\`${t("git")}\` / \`${t("get_pull_request")}\`). If one exists, update it rather than opening a duplicate.
|
|
37
|
+
- **idempotency**: the remediation branch is the group's \`branch\` (\`remediate/<group-id>\`, with a \`--<base>\` suffix when the PR targets a non-default branch, or the branch of the PR already open for this group) — use it exactly, never a name of your own. Before doing anything, check whether that branch or an open PR for it already exists (\`${t("git")}\` / \`${t("get_pull_request")}\`). If one exists, update it rather than opening a duplicate.
|
|
38
38
|
- **branch**: create the group's \`branch\` from the **current HEAD** (the checkout that was just scanned) via \`${t("git")}\` (\`git({ command: "checkout", args: ["-b", <branch>] })\`). Do NOT switch to a different base first — branching from the scanned checkout keeps the PR diff to exactly your fix.
|
|
39
39
|
- **honest refusal (decide BEFORE fixing)**: if the group's concerns appear in the scan's \`refusal_candidates\` (the fix needs a human decision — narrowing an IAM wildcard, a KMS key policy, a real ingress CIDR), do **not** guess a fix that could break the stack. Instead open a structured issue (\`${t("create_issue")}\`) describing the concern, why it isn't auto-fixed, and what a human should do, and skip the PR for that group. A proven fix or an honest refusal — never a guessed, unverifiable PR.
|
|
40
40
|
- **propose, then let me steer (when there's no single right fix)**: distinct from honest refusal (which refuses a fix a human must *decide*), this is for a finding with **2–3 genuinely distinct, defensible fixes** that differ in trade-offs, not correctness (e.g. encrypt with an AWS-managed key **vs** a customer-managed KMS key; a narrow security-group rule **vs** a prefix list **vs** a VPC endpoint). When such a fork exists **and the triggering comment did not already select a strategy**, do **not** silently pick for the reviewer: via \`${t("create_issue_comment")}\` post one short comment listing the options as **A / B / C** — each a single line (what it does + its trade-off) — and ask the reviewer to reply \`${COMMENT_COMMAND} fix #<concern-id> with strategy <A|B|C>\`. Then **skip the PR for this group** this run and note it in your final report (it resumes when the reviewer replies). When the comment **did** select one (\`fix #<id> with strategy B\`, or a bare \`strategy B\` reply on the proposal thread), apply **exactly** that strategy — don't second-guess it. Reserve this for real forks in the road; a fix with one obvious correct answer just gets made.
|
|
@@ -105,10 +105,11 @@ export async function runAgentWithWatchdogs(params: RunAgentParams): Promise<Age
|
|
|
105
105
|
const costQuota = reserved ? quotas.cost : createCostQuota(quotas.costLimit.full);
|
|
106
106
|
log.info(
|
|
107
107
|
quotas.tokenLimit.full > 0
|
|
108
|
-
? `» run token quota: ${quotas.tokenLimit.full.toLocaleString("en-GB")}
|
|
108
|
+
? `» run token quota: ${quotas.tokenLimit.full.toLocaleString("en-GB")} tokens` +
|
|
109
109
|
(reserved
|
|
110
110
|
? ` (${tokenQuota.limit.toLocaleString("en-GB")} for the run, the rest reserved to deliver findings if it runs out)`
|
|
111
|
-
: "")
|
|
111
|
+
: "") +
|
|
112
|
+
"; a token read from the prompt cache counts as a tenth"
|
|
112
113
|
: `» run token quota: disabled (${RUN_TOKEN_QUOTA_INPUT}: 0)`,
|
|
113
114
|
);
|
|
114
115
|
log.info(
|