finitemonkey 0.1.0-next.14 → 0.1.0-next.15
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/dist/finitemonkey.mjs +529 -194
- package/package.json +2 -1
package/dist/finitemonkey.mjs
CHANGED
|
@@ -1620,8 +1620,8 @@ var shannonEntropy = (text6) => {
|
|
|
1620
1620
|
const counts = /* @__PURE__ */ new Map();
|
|
1621
1621
|
for (const char of text6) counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
1622
1622
|
let entropy = 0;
|
|
1623
|
-
for (const
|
|
1624
|
-
const p =
|
|
1623
|
+
for (const count4 of counts.values()) {
|
|
1624
|
+
const p = count4 / text6.length;
|
|
1625
1625
|
entropy -= p * Math.log2(p);
|
|
1626
1626
|
}
|
|
1627
1627
|
return entropy;
|
|
@@ -1670,6 +1670,35 @@ var defineRule = (spec2) => ({
|
|
|
1670
1670
|
skipTestPaths: spec2.skipTestPaths ?? false,
|
|
1671
1671
|
placeholders: spec2.placeholders ?? true
|
|
1672
1672
|
});
|
|
1673
|
+
var isWarning = (finding3) => finding3.severity === "warning";
|
|
1674
|
+
var blocking = (findings) => findings.filter((finding3) => !isWarning(finding3));
|
|
1675
|
+
|
|
1676
|
+
// src/secrets/rules/exposure.ts
|
|
1677
|
+
var ARGUMENT_CONTEXT = String.raw`(?:^|\n)[ \t-]*(?:command|entrypoint|CMD|ENTRYPOINT|RUN|ExecStart(?:Pre|Post)?|ExecReload)\b[^\n]*?`;
|
|
1678
|
+
var SECRET_FLAG = String.raw`(?<=[\s"'\[,])--?[A-Za-z0-9_-]*(?:password|passwd|pass|secret|token|requirepass|api-?key)`;
|
|
1679
|
+
var EXPOSURE_RULES = [
|
|
1680
|
+
defineRule({
|
|
1681
|
+
id: "secret-in-process-args",
|
|
1682
|
+
summary: "Literal password, token or key passed as a command-line argument in compose, a Dockerfile or a systemd unit (visible in ps and docker inspect)",
|
|
1683
|
+
kind: "practice",
|
|
1684
|
+
severity: "warning",
|
|
1685
|
+
keywords: ["command", "entrypoint", "cmd", "run", "execstart", "execreload"],
|
|
1686
|
+
source: String.raw`${ARGUMENT_CONTEXT}${SECRET_FLAG}(?:=["']?|["'][ \t]*,[ \t]*["']|[ \t]+["']?)(?<secret>[^\s"',\]$\x60/][^\s"',\]\x60]{3,127})`,
|
|
1687
|
+
flags: "i",
|
|
1688
|
+
minLength: 4
|
|
1689
|
+
}),
|
|
1690
|
+
defineRule({
|
|
1691
|
+
id: "tls-disabled",
|
|
1692
|
+
summary: "TLS verification turned off in code: sslmode set to disable, rejectUnauthorized or InsecureSkipVerify set to skip checks, Python verify set to False, NODE_TLS_REJECT_UNAUTHORIZED set to 0",
|
|
1693
|
+
kind: "practice",
|
|
1694
|
+
severity: "warning",
|
|
1695
|
+
keywords: ["sslmode", "rejectunauthorized", "verify", "insecureskipverify", "node_tls_reject_unauthorized"],
|
|
1696
|
+
source: String.raw`(?<secret>\bsslmode[ \t]*=[ \t]*["']?disable\b|\brejectUnauthorized["']?[ \t]*:[ \t]*false\b|\bverify[ \t]*=[ \t]*False\b|\bInsecureSkipVerify[ \t]*:[ \t]*true\b|\bNODE_TLS_REJECT_UNAUTHORIZED["']?[ \t]*(?:=|:)[ \t]*["']?0\b)`,
|
|
1697
|
+
minLength: 1,
|
|
1698
|
+
placeholders: false,
|
|
1699
|
+
skipTestPaths: true
|
|
1700
|
+
})
|
|
1701
|
+
];
|
|
1673
1702
|
|
|
1674
1703
|
// src/secrets/rules/providers.ts
|
|
1675
1704
|
var PROVIDER_RULES = [
|
|
@@ -1893,7 +1922,7 @@ var STRUCTURAL_RULES = [
|
|
|
1893
1922
|
];
|
|
1894
1923
|
|
|
1895
1924
|
// src/secrets/rules/catalog.ts
|
|
1896
|
-
var RULES = [...PROVIDER_RULES, ...STRUCTURAL_RULES];
|
|
1925
|
+
var RULES = [...PROVIDER_RULES, ...STRUCTURAL_RULES, ...EXPOSURE_RULES];
|
|
1897
1926
|
var CREDENTIAL_RULES = RULES.filter((rule) => rule.kind === "credential");
|
|
1898
1927
|
var ruleById = (id) => RULES.find((rule) => rule.id === id);
|
|
1899
1928
|
|
|
@@ -3162,9 +3191,9 @@ var skipOptions = (args, wrapper) => {
|
|
|
3162
3191
|
}
|
|
3163
3192
|
return index + wrapper.positionals;
|
|
3164
3193
|
};
|
|
3165
|
-
var shift = (stage,
|
|
3194
|
+
var shift = (stage, count4, name) => ({
|
|
3166
3195
|
...stage,
|
|
3167
|
-
argv: stage.argv.slice(
|
|
3196
|
+
argv: stage.argv.slice(count4),
|
|
3168
3197
|
via: [...stage.via, name]
|
|
3169
3198
|
});
|
|
3170
3199
|
var envCommand = (stage) => {
|
|
@@ -3948,7 +3977,93 @@ var implicitContract = (files) => {
|
|
|
3948
3977
|
};
|
|
3949
3978
|
|
|
3950
3979
|
// src/gate/prepare.ts
|
|
3980
|
+
import { join as join14 } from "node:path";
|
|
3981
|
+
|
|
3982
|
+
// src/git/project-config.ts
|
|
3951
3983
|
import { join as join12 } from "node:path";
|
|
3984
|
+
var CONFIG_FILE3 = ".finitemonkey/config.yml";
|
|
3985
|
+
var unquote = (value) => value.trim().replace(/^(["'])(.*)\1$/s, "$2");
|
|
3986
|
+
var stripComment = (line2) => {
|
|
3987
|
+
let quote;
|
|
3988
|
+
for (let index = 0; index < line2.length; index += 1) {
|
|
3989
|
+
const char = line2.charAt(index);
|
|
3990
|
+
if (quote === void 0 && (char === '"' || char === "'")) quote = char;
|
|
3991
|
+
else if (char === quote) quote = void 0;
|
|
3992
|
+
else if (quote === void 0 && char === "#" && (index === 0 || /\s/.test(line2.charAt(index - 1)))) {
|
|
3993
|
+
return line2.slice(0, index);
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
return line2;
|
|
3997
|
+
};
|
|
3998
|
+
var flowList = (value) => {
|
|
3999
|
+
const trimmed = value.trim();
|
|
4000
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return void 0;
|
|
4001
|
+
return trimmed.slice(1, -1).split(",").map(unquote).filter((item) => item !== "");
|
|
4002
|
+
};
|
|
4003
|
+
var keyValue = (body2) => {
|
|
4004
|
+
const match = /^("[^"]*"|'[^']*'|[^:]+?)\s*:(?:\s+(.*))?$/.exec(body2);
|
|
4005
|
+
if (match === null) return void 0;
|
|
4006
|
+
return { key: unquote(match[1] ?? ""), value: match[2] ?? "" };
|
|
4007
|
+
};
|
|
4008
|
+
var addItem = (frame, item) => {
|
|
4009
|
+
if (frame.key === void 0) return;
|
|
4010
|
+
const parent = frame.tree[frame.key];
|
|
4011
|
+
if (Array.isArray(parent)) parent.push(item);
|
|
4012
|
+
else frame.tree[frame.key] = [item];
|
|
4013
|
+
};
|
|
4014
|
+
var placeLine = (stack, indent, body2) => {
|
|
4015
|
+
while (stack.length > 1 && (stack[stack.length - 1]?.indent ?? 0) >= indent && !body2.startsWith("- ")) stack.pop();
|
|
4016
|
+
const top = stack[stack.length - 1];
|
|
4017
|
+
if (top === void 0) return;
|
|
4018
|
+
if (body2.startsWith("- ")) {
|
|
4019
|
+
addItem(top, unquote(body2.slice(2)));
|
|
4020
|
+
return;
|
|
4021
|
+
}
|
|
4022
|
+
const pair = keyValue(body2);
|
|
4023
|
+
if (pair === void 0) return;
|
|
4024
|
+
const parentTree = top.key === void 0 ? top.tree : ensureTree(top.tree, top.key);
|
|
4025
|
+
if (pair.value === "") {
|
|
4026
|
+
stack.push({ indent, tree: parentTree, key: pair.key });
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
4029
|
+
parentTree[pair.key] = flowList(pair.value) ?? unquote(pair.value);
|
|
4030
|
+
};
|
|
4031
|
+
var ensureTree = (tree, key) => {
|
|
4032
|
+
const existing = tree[key];
|
|
4033
|
+
if (existing !== void 0 && typeof existing !== "string" && !Array.isArray(existing)) return existing;
|
|
4034
|
+
const fresh3 = {};
|
|
4035
|
+
tree[key] = fresh3;
|
|
4036
|
+
return fresh3;
|
|
4037
|
+
};
|
|
4038
|
+
var parseConfigYaml = (text6) => {
|
|
4039
|
+
const root = {};
|
|
4040
|
+
const stack = [{ indent: -1, tree: root, key: void 0 }];
|
|
4041
|
+
for (const raw of text6.split(/\r?\n/)) {
|
|
4042
|
+
const line2 = stripComment(raw).trimEnd();
|
|
4043
|
+
if (line2.trim() === "" || line2.trim() === "---") continue;
|
|
4044
|
+
const indent = line2.length - line2.trimStart().length;
|
|
4045
|
+
placeLine(stack, indent, line2.trim());
|
|
4046
|
+
}
|
|
4047
|
+
return root;
|
|
4048
|
+
};
|
|
4049
|
+
var isTree = (node) => typeof node === "object" && !Array.isArray(node);
|
|
4050
|
+
var configAt = (tree, ...path) => path.reduce((node, key) => isTree(node) ? node[key] : void 0, tree);
|
|
4051
|
+
var configList = (tree, ...path) => {
|
|
4052
|
+
const node = configAt(tree, ...path);
|
|
4053
|
+
if (typeof node === "string") return node === "" ? [] : [node];
|
|
4054
|
+
return Array.isArray(node) ? node.filter((item) => typeof item === "string") : [];
|
|
4055
|
+
};
|
|
4056
|
+
var configMap = (tree, ...path) => {
|
|
4057
|
+
const node = configAt(tree, ...path);
|
|
4058
|
+
if (!isTree(node)) return {};
|
|
4059
|
+
return Object.fromEntries(
|
|
4060
|
+
Object.entries(node).filter((entry2) => typeof entry2[1] === "string")
|
|
4061
|
+
);
|
|
4062
|
+
};
|
|
4063
|
+
var loadProjectConfig = (runtime, root) => {
|
|
4064
|
+
const text6 = runtime.readText(toAbsolutePath(join12(root, CONFIG_FILE3)));
|
|
4065
|
+
return text6.ok ? parseConfigYaml(text6.value) : {};
|
|
4066
|
+
};
|
|
3952
4067
|
|
|
3953
4068
|
// src/core/yaml/scalar.ts
|
|
3954
4069
|
var ESCAPES = {
|
|
@@ -4096,7 +4211,7 @@ var parseValue = (text6) => {
|
|
|
4096
4211
|
|
|
4097
4212
|
// src/core/yaml/parse.ts
|
|
4098
4213
|
var BOM = new RegExp(`^${String.fromCharCode(65279)}`);
|
|
4099
|
-
var
|
|
4214
|
+
var stripComment2 = (text6) => {
|
|
4100
4215
|
for (let index = 0; index < text6.length; index += 1) {
|
|
4101
4216
|
const char = text6[index];
|
|
4102
4217
|
if (char === '"' || char === "'") {
|
|
@@ -4111,7 +4226,7 @@ var toLines = (source) => {
|
|
|
4111
4226
|
const lines3 = [];
|
|
4112
4227
|
const raw = source.replace(BOM, "").split(/\r?\n/);
|
|
4113
4228
|
for (const [index, full] of raw.entries()) {
|
|
4114
|
-
const text6 =
|
|
4229
|
+
const text6 = stripComment2(full).trimEnd();
|
|
4115
4230
|
const body2 = text6.trimStart();
|
|
4116
4231
|
if (body2 === "" || body2 === "---" && lines3.length === 0) continue;
|
|
4117
4232
|
const indentText = text6.slice(0, text6.length - body2.length);
|
|
@@ -4219,7 +4334,8 @@ var CHECK_KINDS = [
|
|
|
4219
4334
|
"worktree",
|
|
4220
4335
|
"merge",
|
|
4221
4336
|
"prod",
|
|
4222
|
-
"prose"
|
|
4337
|
+
"prose",
|
|
4338
|
+
"review"
|
|
4223
4339
|
];
|
|
4224
4340
|
var GIT_REQUIREMENTS = ["committed", "pushed", "pr_open"];
|
|
4225
4341
|
var SIZES = ["S", "M", "L"];
|
|
@@ -4425,6 +4541,9 @@ var readBaseline = (runtime, paths) => {
|
|
|
4425
4541
|
};
|
|
4426
4542
|
var writeBaseline = (runtime, paths, treeHash2, checks) => writeJsonFile(runtime, paths.cache(BASELINE_FILE), { treeHash: treeHash2, checks: { ...checks } }).ok;
|
|
4427
4543
|
|
|
4544
|
+
// src/gate/diff-shape.ts
|
|
4545
|
+
import { join as join13 } from "node:path";
|
|
4546
|
+
|
|
4428
4547
|
// src/gate/git.ts
|
|
4429
4548
|
var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
4430
4549
|
var GIT_TIMEOUT_MS = 15e3;
|
|
@@ -4524,10 +4643,20 @@ var numstat = (git, base) => {
|
|
|
4524
4643
|
};
|
|
4525
4644
|
var dirtyPaths = (git) => nulFields(git.out(["status", "--porcelain", "-z", "--", ".", EXCLUDE_FM]) ?? "").map((entry2) => entry2.slice(3));
|
|
4526
4645
|
var aheadOfUpstream = (git) => {
|
|
4527
|
-
const
|
|
4528
|
-
return
|
|
4646
|
+
const count4 = git.out(["rev-list", "--count", "@{u}..HEAD"])?.trim();
|
|
4647
|
+
return count4 === void 0 ? void 0 : Number(count4);
|
|
4529
4648
|
};
|
|
4530
4649
|
|
|
4650
|
+
// src/gate/diff-shape.ts
|
|
4651
|
+
var untrackedLines = (runtime, root, changes) => changes.filter((change) => change.status === "A").reduce((sum, change) => {
|
|
4652
|
+
const text6 = runtime.readText(toAbsolutePath(join13(root, change.path)));
|
|
4653
|
+
return sum + (text6.ok ? text6.value.split("\n").length : 0);
|
|
4654
|
+
}, 0);
|
|
4655
|
+
var diffShape = (runtime, git, base, changes) => ({
|
|
4656
|
+
files: changes.map((change) => change.path),
|
|
4657
|
+
lines: numstat(git, base).lines + untrackedLines(runtime, git.cwd, changes)
|
|
4658
|
+
});
|
|
4659
|
+
|
|
4531
4660
|
// src/gate/pr-parse.ts
|
|
4532
4661
|
var FAILED = /* @__PURE__ */ new Set(["FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE", "ERROR"]);
|
|
4533
4662
|
var PENDING = /* @__PURE__ */ new Set(["IN_PROGRESS", "QUEUED", "PENDING", "WAITING", "REQUESTED", "EXPECTED"]);
|
|
@@ -4618,13 +4747,118 @@ var conflictFiles = (git, base) => {
|
|
|
4618
4747
|
return [];
|
|
4619
4748
|
};
|
|
4620
4749
|
|
|
4750
|
+
// src/core/glob.ts
|
|
4751
|
+
var escape = (char) => /[.+^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
|
|
4752
|
+
var globToRegExp = (glob) => {
|
|
4753
|
+
const pattern = glob.replace(/^\.\//, "");
|
|
4754
|
+
let out = "";
|
|
4755
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
4756
|
+
const char = pattern[index] ?? "";
|
|
4757
|
+
if (char === "*" && pattern[index + 1] === "*") {
|
|
4758
|
+
const slash = pattern[index + 2] === "/";
|
|
4759
|
+
out += slash ? "(?:.*/)?" : ".*";
|
|
4760
|
+
index += slash ? 2 : 1;
|
|
4761
|
+
} else if (char === "*") out += "[^/]*";
|
|
4762
|
+
else if (char === "?") out += "[^/]";
|
|
4763
|
+
else out += escape(char);
|
|
4764
|
+
}
|
|
4765
|
+
return new RegExp(`^${out}${pattern.endsWith("/") ? ".*" : ""}$`);
|
|
4766
|
+
};
|
|
4767
|
+
var matchesAny = (path, globs) => {
|
|
4768
|
+
const normalized = toPosixPath(path);
|
|
4769
|
+
return globs.some((glob) => globToRegExp(glob).test(normalized));
|
|
4770
|
+
};
|
|
4771
|
+
|
|
4772
|
+
// src/gate/size.ts
|
|
4773
|
+
var DEFAULT_SENSITIVE = [
|
|
4774
|
+
"**/*payment*",
|
|
4775
|
+
"**/*payment*/**",
|
|
4776
|
+
"**/*billing*",
|
|
4777
|
+
"**/*billing*/**",
|
|
4778
|
+
"**/*checkout*/**",
|
|
4779
|
+
"**/auth/**",
|
|
4780
|
+
"**/*auth.*",
|
|
4781
|
+
"**/oauth*",
|
|
4782
|
+
"**/migrations/**",
|
|
4783
|
+
"**/migration/**",
|
|
4784
|
+
"**/*.sql",
|
|
4785
|
+
"**/openapi*.*",
|
|
4786
|
+
"**/swagger*.*",
|
|
4787
|
+
"**/*.proto",
|
|
4788
|
+
"**/*.graphql",
|
|
4789
|
+
"**/*stripe*",
|
|
4790
|
+
"**/*openai*",
|
|
4791
|
+
"**/*anthropic*"
|
|
4792
|
+
];
|
|
4793
|
+
var DEFAULT_SIZE_CONFIG = {
|
|
4794
|
+
m: { files: 5, lines: 150 },
|
|
4795
|
+
l: { files: 20, lines: 600 },
|
|
4796
|
+
sensitive: DEFAULT_SENSITIVE
|
|
4797
|
+
};
|
|
4798
|
+
var count2 = (tree, fallback, ...path) => {
|
|
4799
|
+
const value = configAt(tree, ...path);
|
|
4800
|
+
const parsed = typeof value === "string" ? Number(value) : Number.NaN;
|
|
4801
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
4802
|
+
};
|
|
4803
|
+
var threshold = (tree, key, fallback) => ({
|
|
4804
|
+
files: count2(tree, fallback.files, "size", key, "files"),
|
|
4805
|
+
lines: count2(tree, fallback.lines, "size", key, "lines")
|
|
4806
|
+
});
|
|
4807
|
+
var sizeConfig = (tree) => {
|
|
4808
|
+
const declared = configAt(tree, "size", "sensitive");
|
|
4809
|
+
return {
|
|
4810
|
+
m: threshold(tree, "m", DEFAULT_SIZE_CONFIG.m),
|
|
4811
|
+
l: threshold(tree, "l", DEFAULT_SIZE_CONFIG.l),
|
|
4812
|
+
sensitive: declared === void 0 ? DEFAULT_SENSITIVE : configList(tree, "size", "sensitive")
|
|
4813
|
+
};
|
|
4814
|
+
};
|
|
4815
|
+
var reaches = (shape, limit) => shape.files.length >= limit.files || shape.lines >= limit.lines;
|
|
4816
|
+
var rank = (size) => SIZES.indexOf(size);
|
|
4817
|
+
var larger = (a, b) => rank(a) >= rank(b) ? a : b;
|
|
4818
|
+
var computeSize = (shape, config) => {
|
|
4819
|
+
const sensitive = shape.files.filter((file) => matchesAny(file, config.sensitive));
|
|
4820
|
+
const byVolume = reaches(shape, config.l) ? "L" : reaches(shape, config.m) ? "M" : "S";
|
|
4821
|
+
const computed = sensitive.length > 0 ? larger(byVolume, "M") : byVolume;
|
|
4822
|
+
return { computed, files: shape.files.length, lines: shape.lines, sensitive };
|
|
4823
|
+
};
|
|
4824
|
+
var sizeVerdict = (declared, shape, config) => {
|
|
4825
|
+
const result = computeSize(shape, config);
|
|
4826
|
+
return { ...result, declared, effective: larger(declared, result.computed) };
|
|
4827
|
+
};
|
|
4828
|
+
var upgraded = (verdict) => rank(verdict.effective) > rank(verdict.declared);
|
|
4829
|
+
|
|
4830
|
+
// src/gate/size-contract.ts
|
|
4831
|
+
var REVIEW_CHECK = "size-review";
|
|
4832
|
+
var INVENTORY_CHECK = "size-inventory";
|
|
4833
|
+
var DECISION_CHECK = "size-decision";
|
|
4834
|
+
var INVENTORY = "inventory of the files, behaviours and callers the change touches";
|
|
4835
|
+
var DECISION = "a recorded decision on the approach for a change this large";
|
|
4836
|
+
var reviewChecks = (verdict) => verdict.effective === "S" ? [] : [{ id: REVIEW_CHECK, kind: "review" }];
|
|
4837
|
+
var largeChecks = (verdict) => verdict.effective === "L" && verdict.declared !== "S" ? [
|
|
4838
|
+
{ id: INVENTORY_CHECK, kind: "manual", description: INVENTORY },
|
|
4839
|
+
{ id: DECISION_CHECK, kind: "manual", description: DECISION }
|
|
4840
|
+
] : [];
|
|
4841
|
+
var sizeRequirements = (verdict) => [
|
|
4842
|
+
...reviewChecks(verdict),
|
|
4843
|
+
...largeChecks(verdict)
|
|
4844
|
+
];
|
|
4845
|
+
var applySize = (contract, verdict) => {
|
|
4846
|
+
if (contract.implicit) return contract;
|
|
4847
|
+
const declaredIds = new Set(contract.acceptance.map((check2) => check2.id));
|
|
4848
|
+
const declaredKinds = new Set(contract.acceptance.map((check2) => check2.kind));
|
|
4849
|
+
const extra = sizeRequirements(verdict).filter(
|
|
4850
|
+
(check2) => !declaredIds.has(check2.id) && !(check2.kind === "review" && declaredKinds.has("review"))
|
|
4851
|
+
);
|
|
4852
|
+
return extra.length === 0 && !upgraded(verdict) ? contract : { ...contract, size: verdict.effective, acceptance: [...contract.acceptance, ...extra] };
|
|
4853
|
+
};
|
|
4854
|
+
|
|
4621
4855
|
// src/gate/prepare.ts
|
|
4622
4856
|
var diskFiles = (runtime, root) => ({
|
|
4623
4857
|
read: (relative9) => {
|
|
4624
|
-
const text6 = runtime.readText(toAbsolutePath(
|
|
4858
|
+
const text6 = runtime.readText(toAbsolutePath(join14(root, relative9)));
|
|
4625
4859
|
return text6.ok ? text6.value : void 0;
|
|
4626
4860
|
},
|
|
4627
|
-
exists: (relative9) => runtime.stat(toAbsolutePath(
|
|
4861
|
+
exists: (relative9) => runtime.stat(toAbsolutePath(join14(root, relative9))) !== void 0
|
|
4628
4862
|
});
|
|
4629
4863
|
var baseFiles = (git, base, disk) => ({
|
|
4630
4864
|
read: (relative9) => base === EMPTY_TREE ? disk.read(relative9) : fileAt(git, base, relative9),
|
|
@@ -4656,6 +4890,7 @@ var skipReason = (runtime, paths, loaded, changes, tree, sessionId) => {
|
|
|
4656
4890
|
const session = readSession(runtime, paths);
|
|
4657
4891
|
return session !== void 0 && session.sessionId === sessionId && session.startTree === tree ? "no_session_edits" : void 0;
|
|
4658
4892
|
};
|
|
4893
|
+
var measureSize = (runtime, git, base, changes, contract) => sizeVerdict(contract.size, diffShape(runtime, git, base, changes), sizeConfig(loadProjectConfig(runtime, git.cwd)));
|
|
4659
4894
|
var prepareGate = (options) => {
|
|
4660
4895
|
const probe = gitAt(options.runtime, options.cwd);
|
|
4661
4896
|
const root = gitRoot(probe);
|
|
@@ -4675,11 +4910,13 @@ var prepareGate = (options) => {
|
|
|
4675
4910
|
const skip = options.noSkip === true ? void 0 : skipReason(options.runtime, paths, loaded.value, changes, tree, options.sessionId);
|
|
4676
4911
|
if (skip !== void 0) return { kind: "skipped", reason: skip };
|
|
4677
4912
|
const pr = memo(() => prStatus(options.runtime, git, paths));
|
|
4913
|
+
const size = measureSize(options.runtime, git, base, changes, loaded.value.contract);
|
|
4678
4914
|
const ctx = {
|
|
4679
4915
|
runtime: options.runtime,
|
|
4680
4916
|
git,
|
|
4681
4917
|
paths,
|
|
4682
|
-
contract: loaded.value.contract,
|
|
4918
|
+
contract: applySize(loaded.value.contract, size),
|
|
4919
|
+
size,
|
|
4683
4920
|
trusted: loaded.value.trusted,
|
|
4684
4921
|
treeHash: tree,
|
|
4685
4922
|
base,
|
|
@@ -5458,7 +5695,18 @@ var en_default = {
|
|
|
5458
5695
|
"history.pattern.no_verify": "{count, plural, one {# git command} other {# git commands}} with --no-verify",
|
|
5459
5696
|
"history.pattern.force_push": "{count, plural, one {# force push} other {# force pushes}}",
|
|
5460
5697
|
"history.pattern.test_skip": "{count, plural, one {# test edit} other {# test edits}} that added .skip, .only or a skip marker",
|
|
5461
|
-
"history.estimate": "These are estimates from pattern matching, not evidence; nothing was saved and nothing left this machine."
|
|
5698
|
+
"history.estimate": "These are estimates from pattern matching, not evidence; nothing was saved and nothing left this machine.",
|
|
5699
|
+
"gate.require.review": "a reviewer subagent read the change at its current state",
|
|
5700
|
+
"gate.size.declared": "size {effective}: {files, plural, one {# file} other {# files}}, {lines, plural, one {# line} other {# lines}}",
|
|
5701
|
+
"gate.size.grew": "declared {declared}, the diff makes it {effective}: {files, plural, one {# file} other {# files}}, {lines, plural, one {# line} other {# lines}}",
|
|
5702
|
+
"gate.size.sensitive": "sensitive path touched: {file}",
|
|
5703
|
+
"gate.review.missing": "no review of the current state: run a reviewer subagent on the diff, or the person writes `fm: approve {check} <reason>` in the chat",
|
|
5704
|
+
"gate.review.done": "reviewed by {agent} at {at}",
|
|
5705
|
+
"secrets.warn.write": "Finite Monkey warning (the write went through): {found}. A secret passed as a command-line argument shows in ps and docker inspect; disabled TLS verification lets anyone on the path read the traffic. Move the value to an environment variable or a secret file, and keep TLS checks on outside tests.",
|
|
5706
|
+
"gate.secrets.warning": "warning: {rule} at {file}:{line}",
|
|
5707
|
+
"doctor.secrets.remote_credential": "git remote {remotes} has a credential in its URL, stored in plain text in .git/config",
|
|
5708
|
+
"doctor.secrets.fix_remote_credential": "run `git remote set-url {remote} <url without user:token@>`, use a credential helper or SSH, and rotate the token",
|
|
5709
|
+
"secrets.session.remote_credential": "git remote {remotes} has a credential in its URL (plain text in .git/config). Ask the person to run `git remote set-url {remote} <url without user:token@>` and rotate the token; do not print the URL."
|
|
5462
5710
|
};
|
|
5463
5711
|
|
|
5464
5712
|
// src/i18n/es.json
|
|
@@ -5971,7 +6219,18 @@ var es_default = {
|
|
|
5971
6219
|
"history.pattern.no_verify": "{count, plural, one {# comando git} other {# comandos git}} con --no-verify",
|
|
5972
6220
|
"history.pattern.force_push": "{count, plural, one {# push forzado} other {# push forzados}}",
|
|
5973
6221
|
"history.pattern.test_skip": "{count, plural, one {# edición} other {# ediciones}} de pruebas que añadieron .skip, .only o una marca para saltar",
|
|
5974
|
-
"history.estimate": "Son estimaciones por patrones de texto, no evidencia; no se guardó nada y nada salió de esta máquina."
|
|
6222
|
+
"history.estimate": "Son estimaciones por patrones de texto, no evidencia; no se guardó nada y nada salió de esta máquina.",
|
|
6223
|
+
"gate.require.review": "un subagente revisor leyó el cambio en su estado actual",
|
|
6224
|
+
"gate.size.declared": "tamaño {effective}: {files, plural, one {# archivo} other {# archivos}}, {lines, plural, one {# línea} other {# líneas}}",
|
|
6225
|
+
"gate.size.grew": "declarada {declared}, el diff la vuelve {effective}: {files, plural, one {# archivo} other {# archivos}}, {lines, plural, one {# línea} other {# líneas}}",
|
|
6226
|
+
"gate.size.sensitive": "ruta sensible tocada: {file}",
|
|
6227
|
+
"gate.review.missing": "ninguna revisión del estado actual: ejecuta un subagente revisor sobre el diff, o la persona escribe `fm: approve {check} <motivo>` en el chat",
|
|
6228
|
+
"gate.review.done": "revisado por {agent} en {at}",
|
|
6229
|
+
"secrets.warn.write": "Aviso de Finite Monkey (la escritura se hizo): {found}. Un secreto pasado como argumento de línea de comandos aparece en ps y en docker inspect; la verificación TLS desactivada deja que cualquiera en el camino lea el tráfico. Lleva el valor a una variable de entorno o a un archivo de secreto y mantén el TLS verificado fuera de las pruebas.",
|
|
6230
|
+
"gate.secrets.warning": "aviso: {rule} en {file}:{line}",
|
|
6231
|
+
"doctor.secrets.remote_credential": "el remoto git {remotes} tiene una credencial en la URL, guardada en texto plano en .git/config",
|
|
6232
|
+
"doctor.secrets.fix_remote_credential": "ejecuta `git remote set-url {remote} <url sin usuario:token@>`, usa un credential helper o SSH y rota el token",
|
|
6233
|
+
"secrets.session.remote_credential": "el remoto git {remotes} tiene una credencial en la URL (texto plano en .git/config). Pide a la persona que ejecute `git remote set-url {remote} <url sin usuario:token@>` y rote el token; no imprimas la URL."
|
|
5975
6234
|
};
|
|
5976
6235
|
|
|
5977
6236
|
// src/i18n/pt-BR.json
|
|
@@ -6484,7 +6743,18 @@ var pt_BR_default = {
|
|
|
6484
6743
|
"history.pattern.no_verify": "{count, plural, one {# comando git} other {# comandos git}} com --no-verify",
|
|
6485
6744
|
"history.pattern.force_push": "{count, plural, one {# push forçado} other {# pushes forçados}}",
|
|
6486
6745
|
"history.pattern.test_skip": "{count, plural, one {# edição} other {# edições}} de teste que acrescentaram .skip, .only ou marcador de pular",
|
|
6487
|
-
"history.estimate": "São estimativas por padrão de texto, não evidência; nada foi salvo e nada saiu desta máquina."
|
|
6746
|
+
"history.estimate": "São estimativas por padrão de texto, não evidência; nada foi salvo e nada saiu desta máquina.",
|
|
6747
|
+
"gate.require.review": "um subagente revisor leu a mudança no estado atual",
|
|
6748
|
+
"gate.size.declared": "tamanho {effective}: {files, plural, one {# arquivo} other {# arquivos}}, {lines, plural, one {# linha} other {# linhas}}",
|
|
6749
|
+
"gate.size.grew": "declarada {declared}, o diff a torna {effective}: {files, plural, one {# arquivo} other {# arquivos}}, {lines, plural, one {# linha} other {# linhas}}",
|
|
6750
|
+
"gate.size.sensitive": "caminho sensível alterado: {file}",
|
|
6751
|
+
"gate.review.missing": "nenhuma revisão do estado atual: rode um subagente revisor no diff, ou a pessoa escreve `fm: approve {check} <motivo>` no chat",
|
|
6752
|
+
"gate.review.done": "revisado por {agent} em {at}",
|
|
6753
|
+
"secrets.warn.write": "Aviso do Finite Monkey (a escrita foi feita): {found}. Segredo passado como argumento de linha de comando aparece no ps e no docker inspect; verificação TLS desligada deixa qualquer um no caminho ler o tráfego. Leve o valor para uma variável de ambiente ou um arquivo de segredo e mantenha o TLS verificado fora dos testes.",
|
|
6754
|
+
"gate.secrets.warning": "aviso: {rule} em {file}:{line}",
|
|
6755
|
+
"doctor.secrets.remote_credential": "o remoto git {remotes} tem uma credencial na URL, guardada em texto puro no .git/config",
|
|
6756
|
+
"doctor.secrets.fix_remote_credential": "rode `git remote set-url {remote} <url sem usuario:token@>`, use um credential helper ou SSH e rotacione o token",
|
|
6757
|
+
"secrets.session.remote_credential": "o remoto git {remotes} tem uma credencial na URL (texto puro no .git/config). Peça para a pessoa rodar `git remote set-url {remote} <url sem usuario:token@>` e rotacionar o token; não imprima a URL."
|
|
6488
6758
|
};
|
|
6489
6759
|
|
|
6490
6760
|
// src/i18n/review.json
|
|
@@ -6998,7 +7268,18 @@ var review_default = {
|
|
|
6998
7268
|
"history.pattern.no_verify": "machine",
|
|
6999
7269
|
"history.pattern.force_push": "machine",
|
|
7000
7270
|
"history.pattern.test_skip": "machine",
|
|
7001
|
-
"history.estimate": "machine"
|
|
7271
|
+
"history.estimate": "machine",
|
|
7272
|
+
"gate.require.review": "machine",
|
|
7273
|
+
"gate.size.declared": "machine",
|
|
7274
|
+
"gate.size.grew": "machine",
|
|
7275
|
+
"gate.size.sensitive": "machine",
|
|
7276
|
+
"gate.review.missing": "machine",
|
|
7277
|
+
"gate.review.done": "machine",
|
|
7278
|
+
"secrets.warn.write": "machine",
|
|
7279
|
+
"gate.secrets.warning": "machine",
|
|
7280
|
+
"doctor.secrets.remote_credential": "machine",
|
|
7281
|
+
"doctor.secrets.fix_remote_credential": "machine",
|
|
7282
|
+
"secrets.session.remote_credential": "machine"
|
|
7002
7283
|
},
|
|
7003
7284
|
es: {
|
|
7004
7285
|
"command.init.summary": "machine",
|
|
@@ -7509,7 +7790,18 @@ var review_default = {
|
|
|
7509
7790
|
"history.pattern.no_verify": "machine",
|
|
7510
7791
|
"history.pattern.force_push": "machine",
|
|
7511
7792
|
"history.pattern.test_skip": "machine",
|
|
7512
|
-
"history.estimate": "machine"
|
|
7793
|
+
"history.estimate": "machine",
|
|
7794
|
+
"gate.require.review": "machine",
|
|
7795
|
+
"gate.size.declared": "machine",
|
|
7796
|
+
"gate.size.grew": "machine",
|
|
7797
|
+
"gate.size.sensitive": "machine",
|
|
7798
|
+
"gate.review.missing": "machine",
|
|
7799
|
+
"gate.review.done": "machine",
|
|
7800
|
+
"secrets.warn.write": "machine",
|
|
7801
|
+
"gate.secrets.warning": "machine",
|
|
7802
|
+
"doctor.secrets.remote_credential": "machine",
|
|
7803
|
+
"doctor.secrets.fix_remote_credential": "machine",
|
|
7804
|
+
"secrets.session.remote_credential": "machine"
|
|
7513
7805
|
}
|
|
7514
7806
|
};
|
|
7515
7807
|
|
|
@@ -7632,7 +7924,7 @@ var buildReport = (input) => {
|
|
|
7632
7924
|
);
|
|
7633
7925
|
return fromEvidence(check2.id, check2.kind, entry2, hasStale(ledger, check2.id, treeHash2));
|
|
7634
7926
|
});
|
|
7635
|
-
const
|
|
7927
|
+
const count4 = (status) => items.filter((item) => item.status === status).length;
|
|
7636
7928
|
return {
|
|
7637
7929
|
schema: GATE_SCHEMA,
|
|
7638
7930
|
task: contract.id,
|
|
@@ -7643,7 +7935,7 @@ var buildReport = (input) => {
|
|
|
7643
7935
|
source: input.source,
|
|
7644
7936
|
contractEdited: input.contractEdited,
|
|
7645
7937
|
verdict: verdictOf(items),
|
|
7646
|
-
counts: { passed:
|
|
7938
|
+
counts: { passed: count4("passed"), failed: count4("failed"), unverified: count4("unverified") },
|
|
7647
7939
|
items,
|
|
7648
7940
|
decisionsNeeded: contract.decisionsNeeded,
|
|
7649
7941
|
fastPath: input.fastPath
|
|
@@ -7767,6 +8059,27 @@ var preCompact = (input) => {
|
|
|
7767
8059
|
return { kind: "allow" };
|
|
7768
8060
|
};
|
|
7769
8061
|
|
|
8062
|
+
// src/gate/review.ts
|
|
8063
|
+
var REVIEWS_FILE = "reviews.json";
|
|
8064
|
+
var REVIEWER_AGENT = /(^|:)[\w-]*reviewer$/i;
|
|
8065
|
+
var KEEP = 50;
|
|
8066
|
+
var toReview = (value) => {
|
|
8067
|
+
if (!isRecord(value)) return void 0;
|
|
8068
|
+
const { treeHash: treeHash2, agent, at } = value;
|
|
8069
|
+
return typeof treeHash2 === "string" && typeof agent === "string" && typeof at === "string" ? { treeHash: treeHash2, agent, at } : void 0;
|
|
8070
|
+
};
|
|
8071
|
+
var readReviews = (runtime, paths) => {
|
|
8072
|
+
const raw = readJsonFile(runtime, paths.cache(REVIEWS_FILE))?.["reviews"];
|
|
8073
|
+
return Array.isArray(raw) ? raw.flatMap((item) => {
|
|
8074
|
+
const review2 = toReview(item);
|
|
8075
|
+
return review2 === void 0 ? [] : [review2];
|
|
8076
|
+
}) : [];
|
|
8077
|
+
};
|
|
8078
|
+
var recordReview = (runtime, paths, treeHash2, agent) => {
|
|
8079
|
+
const reviews = [...readReviews(runtime, paths), { treeHash: treeHash2, agent, at: runtime.now() }].slice(-KEEP);
|
|
8080
|
+
return writeJsonFile(runtime, paths.cache(REVIEWS_FILE), { reviews: reviews.map((review2) => ({ ...review2 })) }).ok;
|
|
8081
|
+
};
|
|
8082
|
+
|
|
7770
8083
|
// src/gate/test-summary.ts
|
|
7771
8084
|
var ANSI2 = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
7772
8085
|
var COUNTERS = [
|
|
@@ -7906,44 +8219,14 @@ var runShellCheck = (check2, run4, ctx) => {
|
|
|
7906
8219
|
};
|
|
7907
8220
|
var commandCheck = (check2, ctx) => check2.run === void 0 ? unverified("not_implemented") : runShellCheck(check2, check2.run, ctx);
|
|
7908
8221
|
|
|
7909
|
-
// src/gate/checks/diff.ts
|
|
7910
|
-
import { join as join13 } from "node:path";
|
|
7911
|
-
|
|
7912
|
-
// src/core/glob.ts
|
|
7913
|
-
var escape = (char) => /[.+^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
|
|
7914
|
-
var globToRegExp = (glob) => {
|
|
7915
|
-
const pattern = glob.replace(/^\.\//, "");
|
|
7916
|
-
let out = "";
|
|
7917
|
-
for (let index = 0; index < pattern.length; index += 1) {
|
|
7918
|
-
const char = pattern[index] ?? "";
|
|
7919
|
-
if (char === "*" && pattern[index + 1] === "*") {
|
|
7920
|
-
const slash = pattern[index + 2] === "/";
|
|
7921
|
-
out += slash ? "(?:.*/)?" : ".*";
|
|
7922
|
-
index += slash ? 2 : 1;
|
|
7923
|
-
} else if (char === "*") out += "[^/]*";
|
|
7924
|
-
else if (char === "?") out += "[^/]";
|
|
7925
|
-
else out += escape(char);
|
|
7926
|
-
}
|
|
7927
|
-
return new RegExp(`^${out}${pattern.endsWith("/") ? ".*" : ""}$`);
|
|
7928
|
-
};
|
|
7929
|
-
var matchesAny = (path, globs) => {
|
|
7930
|
-
const normalized = toPosixPath(path);
|
|
7931
|
-
return globs.some((glob) => globToRegExp(glob).test(normalized));
|
|
7932
|
-
};
|
|
7933
|
-
|
|
7934
8222
|
// src/gate/checks/diff.ts
|
|
7935
8223
|
var MAX_LISTED = 10;
|
|
7936
|
-
var untrackedLines = (ctx) => ctx.changes.filter((change) => change.status === "A").reduce((sum, change) => {
|
|
7937
|
-
const text6 = ctx.runtime.readText(toAbsolutePath(join13(ctx.paths.root, change.path)));
|
|
7938
|
-
return sum + (text6.ok ? text6.value.split("\n").length : 0);
|
|
7939
|
-
}, 0);
|
|
7940
8224
|
var diffCheck = (_check, ctx) => {
|
|
7941
8225
|
const { paths, diffBudget } = ctx.contract.scope;
|
|
7942
8226
|
if (paths.length === 0 && diffBudget === void 0) return passed([note("gate.diff.no_scope")]);
|
|
7943
8227
|
const outside = paths.length === 0 ? [] : ctx.changes.filter((change) => !matchesAny(change.path, paths));
|
|
7944
|
-
const stat = numstat(ctx.git, ctx.base);
|
|
7945
8228
|
const files = ctx.changes.length;
|
|
7946
|
-
const lines3 =
|
|
8229
|
+
const lines3 = diffShape(ctx.runtime, ctx.git, ctx.base, ctx.changes).lines;
|
|
7947
8230
|
const notes2 = [
|
|
7948
8231
|
...outside.slice(0, MAX_LISTED).map((change) => note("gate.diff.outside", { file: change.path })),
|
|
7949
8232
|
...diffBudget?.files !== void 0 && files > diffBudget.files ? [note("gate.diff.too_many_files", { count: files, max: diffBudget.files })] : [],
|
|
@@ -7993,13 +8276,13 @@ var manualCheck = (check2, ctx) => {
|
|
|
7993
8276
|
};
|
|
7994
8277
|
|
|
7995
8278
|
// src/gate/checks/placeholders.ts
|
|
7996
|
-
import { join as
|
|
8279
|
+
import { join as join15 } from "node:path";
|
|
7997
8280
|
var MAX_LISTED2 = 10;
|
|
7998
8281
|
var SKIP_FILES = /\.(md|mdx|txt|rst|lock|snap|svg|png|jpg|gif|json)$|(^|\/)(CHANGELOG|LICENSE)/i;
|
|
7999
8282
|
var PLACEHOLDER = /\b(TODO|FIXME|XXX)\b|not implemented|NotImplementedError|unimplemented!|todo!\(|lorem ipsum/i;
|
|
8000
8283
|
var findPlaceholders = (lines3) => lines3.filter((line2) => !isTestFile(line2.path) && !SKIP_FILES.test(line2.path) && PLACEHOLDER.test(line2.text));
|
|
8001
8284
|
var untrackedAdded = (ctx) => ctx.changes.filter((change) => change.status === "A" && !SKIP_FILES.test(change.path)).flatMap((change) => {
|
|
8002
|
-
const text6 = ctx.runtime.readText(toAbsolutePath(
|
|
8285
|
+
const text6 = ctx.runtime.readText(toAbsolutePath(join15(ctx.paths.root, change.path)));
|
|
8003
8286
|
return text6.ok ? text6.value.split("\n").map((line2, index) => ({ path: change.path, line: index + 1, text: line2 })) : [];
|
|
8004
8287
|
});
|
|
8005
8288
|
var placeholdersCheck = (_check, ctx) => {
|
|
@@ -8511,10 +8794,10 @@ var hit = (rule, units, index, weight) => {
|
|
|
8511
8794
|
return { rule, unit: index, line: unit?.line ?? 0, weight, excerpt: unit?.text ?? "" };
|
|
8512
8795
|
};
|
|
8513
8796
|
var isProse = (unit) => unit?.kind === "sentence";
|
|
8514
|
-
var samePara = (units, from,
|
|
8797
|
+
var samePara = (units, from, count4) => {
|
|
8515
8798
|
const first = units[from];
|
|
8516
8799
|
if (!isProse(first)) return false;
|
|
8517
|
-
return units.slice(from, from +
|
|
8800
|
+
return units.slice(from, from + count4).every((unit) => isProse(unit) && unit.paragraph === first.paragraph);
|
|
8518
8801
|
};
|
|
8519
8802
|
var runsOf = (units, size, matches) => {
|
|
8520
8803
|
const starts = [];
|
|
@@ -8571,13 +8854,13 @@ var emoji = (units) => units.flatMap(
|
|
|
8571
8854
|
var dashes = (units) => {
|
|
8572
8855
|
const perParagraph = /* @__PURE__ */ new Map();
|
|
8573
8856
|
for (const unit of units) {
|
|
8574
|
-
const
|
|
8575
|
-
perParagraph.set(unit.paragraph, (perParagraph.get(unit.paragraph) ?? 0) +
|
|
8857
|
+
const count4 = unit.text.split("—").length - 1;
|
|
8858
|
+
perParagraph.set(unit.paragraph, (perParagraph.get(unit.paragraph) ?? 0) + count4);
|
|
8576
8859
|
}
|
|
8577
|
-
return [...perParagraph].flatMap(([paragraph,
|
|
8578
|
-
if (
|
|
8860
|
+
return [...perParagraph].flatMap(([paragraph, count4]) => {
|
|
8861
|
+
if (count4 === 0) return [];
|
|
8579
8862
|
const index = units.findIndex((unit) => unit.paragraph === paragraph && unit.text.includes("—"));
|
|
8580
|
-
const weight =
|
|
8863
|
+
const weight = count4 >= 2 ? STRUCTURE_WEIGHTS.dashSeries : STRUCTURE_WEIGHTS.dash;
|
|
8581
8864
|
return [hit("em-dash-series", units, index, weight)];
|
|
8582
8865
|
});
|
|
8583
8866
|
};
|
|
@@ -8605,11 +8888,11 @@ var windowEnd = (units, from, size) => {
|
|
|
8605
8888
|
return to;
|
|
8606
8889
|
};
|
|
8607
8890
|
var windowScore = (hits, window) => hits.filter((item) => item.unit >= window.from && item.unit <= window.to).reduce((sum, item) => sum + item.weight, 0);
|
|
8608
|
-
var hotWindows = (hits, units, size,
|
|
8891
|
+
var hotWindows = (hits, units, size, threshold2) => {
|
|
8609
8892
|
const windows = [];
|
|
8610
8893
|
for (let from = 0; from < units.length; from += 1) {
|
|
8611
8894
|
const window = { from, to: windowEnd(units, from, size) };
|
|
8612
|
-
if (windowScore(hits, window) >=
|
|
8895
|
+
if (windowScore(hits, window) >= threshold2) windows.push(window);
|
|
8613
8896
|
}
|
|
8614
8897
|
return windows;
|
|
8615
8898
|
};
|
|
@@ -8636,10 +8919,10 @@ var byPosition = (left, right) => left.unit - right.unit || left.line - right.li
|
|
|
8636
8919
|
var scoreProse = (text6, options = {}) => {
|
|
8637
8920
|
const { units, allowed: allowed2 } = extractProse(text6);
|
|
8638
8921
|
const size = options.window ?? DENSITY_WINDOW;
|
|
8639
|
-
const
|
|
8922
|
+
const threshold2 = options.threshold ?? DENSITY_THRESHOLD;
|
|
8640
8923
|
const catalog = options.context === "record" ? [...phraseRules(), ...recordRules()] : phraseRules();
|
|
8641
8924
|
const hits = [...phraseHits(units, catalog), ...structureHits(units)].sort(byPosition);
|
|
8642
|
-
const spans = mergeSpans(hits, hotWindows(hits, units, size,
|
|
8925
|
+
const spans = mergeSpans(hits, hotWindows(hits, units, size, threshold2));
|
|
8643
8926
|
return { hits, spans, allowed: allowed2 };
|
|
8644
8927
|
};
|
|
8645
8928
|
var isFlagged = (report2) => report2.spans.length > 0;
|
|
@@ -8689,22 +8972,55 @@ var MAX_LISTED3 = 10;
|
|
|
8689
8972
|
var rulesOf = (span) => [...new Set(span.hits.map((hit3) => hit3.rule))].join(", ");
|
|
8690
8973
|
var proseOutcome = (status) => {
|
|
8691
8974
|
if (!status.ok) return unverified("tool_missing", [note("gate.prose.unavailable")]);
|
|
8692
|
-
const { files, flagged, blocking } = status.value;
|
|
8693
|
-
const key =
|
|
8975
|
+
const { files, flagged, blocking: blocking2 } = status.value;
|
|
8976
|
+
const key = blocking2 ? "gate.prose.finding" : "gate.prose.warning";
|
|
8694
8977
|
const notes2 = flagged.flatMap((item) => item.report.spans.map((span) => ({ file: item.file, span }))).slice(0, MAX_LISTED3).map(({ file, span }) => note(key, { file, line: span.startLine, rules: rulesOf(span) }));
|
|
8695
8978
|
if (notes2.length === 0) return passed([note("gate.prose.clean", { count: files.length })]);
|
|
8696
|
-
return
|
|
8979
|
+
return blocking2 ? failed(notes2) : passed(notes2);
|
|
8697
8980
|
};
|
|
8698
8981
|
var proseCheck = (_check, ctx) => proseOutcome(proseGateStatus(ctx.runtime, ctx.paths.root));
|
|
8699
8982
|
|
|
8983
|
+
// src/gate/checks/review.ts
|
|
8984
|
+
var MAX_SENSITIVE = 5;
|
|
8985
|
+
var sizeNotes = (ctx) => {
|
|
8986
|
+
const size = ctx.size;
|
|
8987
|
+
if (size === void 0) return [];
|
|
8988
|
+
return [
|
|
8989
|
+
note(size.declared === size.effective ? "gate.size.declared" : "gate.size.grew", {
|
|
8990
|
+
declared: size.declared,
|
|
8991
|
+
effective: size.effective,
|
|
8992
|
+
files: size.files,
|
|
8993
|
+
lines: size.lines
|
|
8994
|
+
}),
|
|
8995
|
+
...size.sensitive.slice(0, MAX_SENSITIVE).map((file) => note("gate.size.sensitive", { file }))
|
|
8996
|
+
];
|
|
8997
|
+
};
|
|
8998
|
+
var reviewCheck = (check2, ctx) => {
|
|
8999
|
+
const review2 = readReviews(ctx.runtime, ctx.paths).findLast((entry2) => entry2.treeHash === ctx.treeHash);
|
|
9000
|
+
if (review2 !== void 0) {
|
|
9001
|
+
return { status: "passed", notes: [note("gate.review.done", { agent: review2.agent, at: review2.at })] };
|
|
9002
|
+
}
|
|
9003
|
+
const approval = readApprovals(ctx.runtime, ctx.paths).findLast(
|
|
9004
|
+
(item) => item.task === ctx.contract.id && item.check === check2.id
|
|
9005
|
+
);
|
|
9006
|
+
if (approval !== void 0) {
|
|
9007
|
+
return {
|
|
9008
|
+
status: "passed",
|
|
9009
|
+
notes: [note("gate.manual.approved", { at: approval.at, reason: approval.reason })],
|
|
9010
|
+
releasedBy: `person@${approval.at}`
|
|
9011
|
+
};
|
|
9012
|
+
}
|
|
9013
|
+
return failed([...sizeNotes(ctx), note("gate.review.missing", { check: check2.id })]);
|
|
9014
|
+
};
|
|
9015
|
+
|
|
8700
9016
|
// src/secrets/baseline.ts
|
|
8701
|
-
import { join as
|
|
9017
|
+
import { join as join16 } from "node:path";
|
|
8702
9018
|
var BASELINE_FILE2 = ".finitemonkey/findings/baseline.json";
|
|
8703
9019
|
var BASELINE_SCHEMA = "finitemonkey/secrets-baseline@1";
|
|
8704
9020
|
var BASELINE_STATES = ["aceito", "falso-positivo"];
|
|
8705
9021
|
var EXCEPTION_MAX_DAYS = 90;
|
|
8706
9022
|
var EMPTY_BASELINE = { schema: BASELINE_SCHEMA, entries: [] };
|
|
8707
|
-
var baselinePath = (root) => toAbsolutePath(
|
|
9023
|
+
var baselinePath = (root) => toAbsolutePath(join16(root, BASELINE_FILE2));
|
|
8708
9024
|
var text5 = (record2, key) => {
|
|
8709
9025
|
const value = record2[key];
|
|
8710
9026
|
return typeof value === "string" && value !== "" ? value : void 0;
|
|
@@ -8764,14 +9080,14 @@ var withoutFixed = (baseline, fixed) => {
|
|
|
8764
9080
|
var acceptedFingerprints = (baseline, now) => new Set(baseline.entries.filter((entry2) => entry2.expiresAt >= now).map((entry2) => entry2.fingerprint));
|
|
8765
9081
|
|
|
8766
9082
|
// src/secrets/incidents.ts
|
|
8767
|
-
import { join as
|
|
9083
|
+
import { join as join17 } from "node:path";
|
|
8768
9084
|
var INCIDENTS_DIR = ".finitemonkey/incidents";
|
|
8769
9085
|
var INCIDENT_STATES = ["aberto", "exposicao-local", "rotacionado", "fechado"];
|
|
8770
9086
|
var INCIDENT_SOURCES = ["tool-output", "prompt", "history", "scan"];
|
|
8771
9087
|
var ID = /^inc-[0-9a-f]{12}$/;
|
|
8772
9088
|
var incidentId = (rule, fingerprint) => `inc-${sha256Hex(`${rule}\0${fingerprint}`).slice(0, 12)}`;
|
|
8773
|
-
var incidentsDir = (root) => toAbsolutePath(
|
|
8774
|
-
var incidentFile = (root, id) => toAbsolutePath(
|
|
9089
|
+
var incidentsDir = (root) => toAbsolutePath(join17(root, INCIDENTS_DIR));
|
|
9090
|
+
var incidentFile = (root, id) => toAbsolutePath(join17(incidentsDir(root), `${id}.json`));
|
|
8775
9091
|
var pick2 = (options, value) => options.find((option) => option === value);
|
|
8776
9092
|
var str2 = (record2, key) => {
|
|
8777
9093
|
const value = record2[key];
|
|
@@ -8826,7 +9142,7 @@ var listIncidents = (runtime, root) => {
|
|
|
8826
9142
|
return names.value.filter((name) => name.endsWith(".json")).map((name) => readIncident(runtime, root, name.slice(0, -".json".length))).flatMap((parsed) => parsed.ok ? [parsed.value] : []).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
8827
9143
|
};
|
|
8828
9144
|
var saveIncident = (runtime, root, incident) => {
|
|
8829
|
-
const ignore = runtime.writeText(toAbsolutePath(
|
|
9145
|
+
const ignore = runtime.writeText(toAbsolutePath(join17(incidentsDir(root), ".gitignore")), "*\n");
|
|
8830
9146
|
if (!ignore.ok) return ignore;
|
|
8831
9147
|
return runtime.writeText(incidentFile(root, incident.id), `${JSON.stringify(incident, null, 2)}
|
|
8832
9148
|
`);
|
|
@@ -8873,7 +9189,7 @@ var moveIncident = (incident, to, now, by) => ({
|
|
|
8873
9189
|
var isRotated = (incident) => incident.state === "rotacionado" || incident.state === "fechado";
|
|
8874
9190
|
|
|
8875
9191
|
// src/secrets/scan.ts
|
|
8876
|
-
import { join as
|
|
9192
|
+
import { join as join18, relative as relative4 } from "node:path";
|
|
8877
9193
|
|
|
8878
9194
|
// src/secrets/located.ts
|
|
8879
9195
|
var findingId = (ruleId, file, fingerprint) => `fm-${sha256Hex(`${ruleId}\0${file}\0${fingerprint}`).slice(0, 16)}`;
|
|
@@ -8973,7 +9289,7 @@ var listed2 = (runtime, root, target) => {
|
|
|
8973
9289
|
const base = toAbsolutePath(target, root);
|
|
8974
9290
|
const walked = runtime.walkFiles(base);
|
|
8975
9291
|
if (!walked.ok) return walked;
|
|
8976
|
-
return accept(walked.value.map((name) => toPosixPath(relative4(root,
|
|
9292
|
+
return accept(walked.value.map((name) => toPosixPath(relative4(root, join18(base, toPosixPath(name))))));
|
|
8977
9293
|
};
|
|
8978
9294
|
var scanFile = (runtime, root, file) => {
|
|
8979
9295
|
const text6 = runtime.readText(toAbsolutePath(file, root));
|
|
@@ -9033,15 +9349,17 @@ var secretsCheck = (_check, ctx) => {
|
|
|
9033
9349
|
const status = secretsGateStatus(ctx.runtime, ctx.paths.root, sinceOf(ctx));
|
|
9034
9350
|
if (!status.ok) return unverified("tool_missing", [note("gate.secrets.unavailable")]);
|
|
9035
9351
|
const { findings, openIncidents } = status.value;
|
|
9352
|
+
const warnings = findings.filter(isWarning).slice(0, MAX_LISTED4).map((finding3) => note("gate.secrets.warning", { file: finding3.file, line: finding3.line, rule: finding3.ruleId }));
|
|
9036
9353
|
const notes2 = [
|
|
9037
|
-
...findings.slice(0, MAX_LISTED4).map((finding3) => note("gate.secrets.finding", { file: finding3.file, line: finding3.line, rule: finding3.ruleId })),
|
|
9354
|
+
...blocking(findings).slice(0, MAX_LISTED4).map((finding3) => note("gate.secrets.finding", { file: finding3.file, line: finding3.line, rule: finding3.ruleId })),
|
|
9038
9355
|
...openIncidents.slice(0, MAX_LISTED4).map((incident) => note("gate.secrets.incident", { id: incident.id, state: incident.state }))
|
|
9039
9356
|
];
|
|
9040
|
-
|
|
9357
|
+
if (notes2.length > 0) return failed([...notes2, ...warnings]);
|
|
9358
|
+
return passed(warnings.length > 0 ? warnings : [note("gate.secrets.clean")]);
|
|
9041
9359
|
};
|
|
9042
9360
|
|
|
9043
9361
|
// src/gate/checks/tests.ts
|
|
9044
|
-
import { join as
|
|
9362
|
+
import { join as join19 } from "node:path";
|
|
9045
9363
|
|
|
9046
9364
|
// src/guards/tests/tree.ts
|
|
9047
9365
|
var outOfScope = (input, change) => input.testsProtected && input.scopePaths.length > 0 && change.status !== "A" && isTestFile(change.path) && !matchesAny(change.path, input.scopePaths) ? [{ kind: "out_of_scope", path: change.path }] : [];
|
|
@@ -9073,7 +9391,7 @@ var treeViolations = (ctx) => {
|
|
|
9073
9391
|
changes: relevant,
|
|
9074
9392
|
before: (path) => fileAt(ctx.git, ctx.base, path),
|
|
9075
9393
|
after: (path) => {
|
|
9076
|
-
const text6 = ctx.runtime.readText(toAbsolutePath(
|
|
9394
|
+
const text6 = ctx.runtime.readText(toAbsolutePath(join19(ctx.paths.root, path)));
|
|
9077
9395
|
return text6.ok ? text6.value : void 0;
|
|
9078
9396
|
},
|
|
9079
9397
|
acceptedConfigs: ctx.trust.configs,
|
|
@@ -9094,7 +9412,7 @@ var testsCheck = (check2, ctx) => {
|
|
|
9094
9412
|
};
|
|
9095
9413
|
|
|
9096
9414
|
// src/git/tree.ts
|
|
9097
|
-
import { join as
|
|
9415
|
+
import { join as join20 } from "node:path";
|
|
9098
9416
|
|
|
9099
9417
|
// src/secrets/guards/paths.ts
|
|
9100
9418
|
var SAFE_ENV = /^\.env\.(?:example|sample|template|dist|defaults|schema)$/i;
|
|
@@ -9133,7 +9451,7 @@ var dropSensitive = (runtime, repo, env, options) => {
|
|
|
9133
9451
|
return removed.ok ? accept(void 0) : removed;
|
|
9134
9452
|
};
|
|
9135
9453
|
var withTempIndex = (runtime, repo, options, work) => {
|
|
9136
|
-
const path = toAbsolutePath(
|
|
9454
|
+
const path = toAbsolutePath(join20(repo.gitDir, `finitemonkey-${options.stamp}.index`));
|
|
9137
9455
|
const env = { GIT_INDEX_FILE: path, GIT_OPTIONAL_LOCKS: "0" };
|
|
9138
9456
|
try {
|
|
9139
9457
|
return work(env);
|
|
@@ -9366,7 +9684,7 @@ var entryOf2 = (kind, xy, path) => ({
|
|
|
9366
9684
|
staged: kind !== "untracked" && !xy.startsWith("."),
|
|
9367
9685
|
unstaged: kind === "untracked" || xy.charAt(1) !== "."
|
|
9368
9686
|
});
|
|
9369
|
-
var afterFields = (record2,
|
|
9687
|
+
var afterFields = (record2, count4) => record2.split(" ").slice(count4).join(" ");
|
|
9370
9688
|
var parsePorcelainV2 = (stdout) => {
|
|
9371
9689
|
const records = stdout.split("\0");
|
|
9372
9690
|
const out = [];
|
|
@@ -9384,92 +9702,6 @@ var parsePorcelainV2 = (stdout) => {
|
|
|
9384
9702
|
return out.filter((entry2) => entry2.path !== "" && !entry2.path.startsWith(STATE_PREFIX));
|
|
9385
9703
|
};
|
|
9386
9704
|
|
|
9387
|
-
// src/git/project-config.ts
|
|
9388
|
-
import { join as join20 } from "node:path";
|
|
9389
|
-
var CONFIG_FILE3 = ".finitemonkey/config.yml";
|
|
9390
|
-
var unquote = (value) => value.trim().replace(/^(["'])(.*)\1$/s, "$2");
|
|
9391
|
-
var stripComment2 = (line2) => {
|
|
9392
|
-
let quote;
|
|
9393
|
-
for (let index = 0; index < line2.length; index += 1) {
|
|
9394
|
-
const char = line2.charAt(index);
|
|
9395
|
-
if (quote === void 0 && (char === '"' || char === "'")) quote = char;
|
|
9396
|
-
else if (char === quote) quote = void 0;
|
|
9397
|
-
else if (quote === void 0 && char === "#" && (index === 0 || /\s/.test(line2.charAt(index - 1)))) {
|
|
9398
|
-
return line2.slice(0, index);
|
|
9399
|
-
}
|
|
9400
|
-
}
|
|
9401
|
-
return line2;
|
|
9402
|
-
};
|
|
9403
|
-
var flowList = (value) => {
|
|
9404
|
-
const trimmed = value.trim();
|
|
9405
|
-
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return void 0;
|
|
9406
|
-
return trimmed.slice(1, -1).split(",").map(unquote).filter((item) => item !== "");
|
|
9407
|
-
};
|
|
9408
|
-
var keyValue = (body2) => {
|
|
9409
|
-
const match = /^("[^"]*"|'[^']*'|[^:]+?)\s*:(?:\s+(.*))?$/.exec(body2);
|
|
9410
|
-
if (match === null) return void 0;
|
|
9411
|
-
return { key: unquote(match[1] ?? ""), value: match[2] ?? "" };
|
|
9412
|
-
};
|
|
9413
|
-
var addItem = (frame, item) => {
|
|
9414
|
-
if (frame.key === void 0) return;
|
|
9415
|
-
const parent = frame.tree[frame.key];
|
|
9416
|
-
if (Array.isArray(parent)) parent.push(item);
|
|
9417
|
-
else frame.tree[frame.key] = [item];
|
|
9418
|
-
};
|
|
9419
|
-
var placeLine = (stack, indent, body2) => {
|
|
9420
|
-
while (stack.length > 1 && (stack[stack.length - 1]?.indent ?? 0) >= indent && !body2.startsWith("- ")) stack.pop();
|
|
9421
|
-
const top = stack[stack.length - 1];
|
|
9422
|
-
if (top === void 0) return;
|
|
9423
|
-
if (body2.startsWith("- ")) {
|
|
9424
|
-
addItem(top, unquote(body2.slice(2)));
|
|
9425
|
-
return;
|
|
9426
|
-
}
|
|
9427
|
-
const pair = keyValue(body2);
|
|
9428
|
-
if (pair === void 0) return;
|
|
9429
|
-
const parentTree = top.key === void 0 ? top.tree : ensureTree(top.tree, top.key);
|
|
9430
|
-
if (pair.value === "") {
|
|
9431
|
-
stack.push({ indent, tree: parentTree, key: pair.key });
|
|
9432
|
-
return;
|
|
9433
|
-
}
|
|
9434
|
-
parentTree[pair.key] = flowList(pair.value) ?? unquote(pair.value);
|
|
9435
|
-
};
|
|
9436
|
-
var ensureTree = (tree, key) => {
|
|
9437
|
-
const existing = tree[key];
|
|
9438
|
-
if (existing !== void 0 && typeof existing !== "string" && !Array.isArray(existing)) return existing;
|
|
9439
|
-
const fresh3 = {};
|
|
9440
|
-
tree[key] = fresh3;
|
|
9441
|
-
return fresh3;
|
|
9442
|
-
};
|
|
9443
|
-
var parseConfigYaml = (text6) => {
|
|
9444
|
-
const root = {};
|
|
9445
|
-
const stack = [{ indent: -1, tree: root, key: void 0 }];
|
|
9446
|
-
for (const raw of text6.split(/\r?\n/)) {
|
|
9447
|
-
const line2 = stripComment2(raw).trimEnd();
|
|
9448
|
-
if (line2.trim() === "" || line2.trim() === "---") continue;
|
|
9449
|
-
const indent = line2.length - line2.trimStart().length;
|
|
9450
|
-
placeLine(stack, indent, line2.trim());
|
|
9451
|
-
}
|
|
9452
|
-
return root;
|
|
9453
|
-
};
|
|
9454
|
-
var isTree = (node) => typeof node === "object" && !Array.isArray(node);
|
|
9455
|
-
var configAt = (tree, ...path) => path.reduce((node, key) => isTree(node) ? node[key] : void 0, tree);
|
|
9456
|
-
var configList = (tree, ...path) => {
|
|
9457
|
-
const node = configAt(tree, ...path);
|
|
9458
|
-
if (typeof node === "string") return node === "" ? [] : [node];
|
|
9459
|
-
return Array.isArray(node) ? node.filter((item) => typeof item === "string") : [];
|
|
9460
|
-
};
|
|
9461
|
-
var configMap = (tree, ...path) => {
|
|
9462
|
-
const node = configAt(tree, ...path);
|
|
9463
|
-
if (!isTree(node)) return {};
|
|
9464
|
-
return Object.fromEntries(
|
|
9465
|
-
Object.entries(node).filter((entry2) => typeof entry2[1] === "string")
|
|
9466
|
-
);
|
|
9467
|
-
};
|
|
9468
|
-
var loadProjectConfig = (runtime, root) => {
|
|
9469
|
-
const text6 = runtime.readText(toAbsolutePath(join20(root, CONFIG_FILE3)));
|
|
9470
|
-
return text6.ok ? parseConfigYaml(text6.value) : {};
|
|
9471
|
-
};
|
|
9472
|
-
|
|
9473
9705
|
// src/git/merge/dropped.ts
|
|
9474
9706
|
import { join as join21 } from "node:path";
|
|
9475
9707
|
|
|
@@ -10184,7 +10416,8 @@ var DEFAULT_CHECKS = {
|
|
|
10184
10416
|
secrets: secretsCheck,
|
|
10185
10417
|
prose: proseCheck,
|
|
10186
10418
|
worktree: worktreeCheck,
|
|
10187
|
-
merge: mergeCheck
|
|
10419
|
+
merge: mergeCheck,
|
|
10420
|
+
review: reviewCheck
|
|
10188
10421
|
};
|
|
10189
10422
|
|
|
10190
10423
|
// src/gate/run.ts
|
|
@@ -10364,7 +10597,18 @@ var isOwnWorktree = (input) => {
|
|
|
10364
10597
|
const main2 = findRepoRoot(runtime, input.cwd);
|
|
10365
10598
|
return front !== void 0 && main2 !== void 0 && front !== main2;
|
|
10366
10599
|
};
|
|
10367
|
-
var
|
|
10600
|
+
var noteReview = (input) => {
|
|
10601
|
+
const agent = textField(input.payload.raw, "agent_type") ?? "";
|
|
10602
|
+
if (!REVIEWER_AGENT.test(agent)) return;
|
|
10603
|
+
const root = findRepoRoot(input.runtime, sessionCwd(input));
|
|
10604
|
+
if (root === void 0) return;
|
|
10605
|
+
const tree = treeHash(gitAt(input.runtime, root));
|
|
10606
|
+
if (tree !== void 0) recordReview(input.runtime, fmPaths(root), tree, agent);
|
|
10607
|
+
};
|
|
10608
|
+
var subagentStopGate = (input) => {
|
|
10609
|
+
noteReview(input);
|
|
10610
|
+
return isOwnWorktree(input) ? stopGate(input) : { kind: "allow" };
|
|
10611
|
+
};
|
|
10368
10612
|
|
|
10369
10613
|
// src/gate/hooks/subagent.ts
|
|
10370
10614
|
import { join as join24 } from "node:path";
|
|
@@ -10380,7 +10624,8 @@ var KNOWN_KINDS = /* @__PURE__ */ new Set([
|
|
|
10380
10624
|
"secrets",
|
|
10381
10625
|
"prose",
|
|
10382
10626
|
"worktree",
|
|
10383
|
-
"merge"
|
|
10627
|
+
"merge",
|
|
10628
|
+
"review"
|
|
10384
10629
|
]);
|
|
10385
10630
|
var requirementText = (t, check2) => {
|
|
10386
10631
|
const key = `gate.require.${check2.kind}`;
|
|
@@ -12337,10 +12582,10 @@ var textOf2 = (input, found) => {
|
|
|
12337
12582
|
}
|
|
12338
12583
|
return accept(parts.join("\n\n"));
|
|
12339
12584
|
};
|
|
12340
|
-
var decisionFor = (input, found,
|
|
12585
|
+
var decisionFor = (input, found, blocking2) => {
|
|
12341
12586
|
const text6 = textOf2(input, found);
|
|
12342
12587
|
if (!text6.ok) {
|
|
12343
|
-
return
|
|
12588
|
+
return blocking2 ? { kind: "block", reason: input.t("prose.block.unreadable", { ...text6.error }) } : void 0;
|
|
12344
12589
|
}
|
|
12345
12590
|
const report2 = scoreProse(text6.value, { context: "record" });
|
|
12346
12591
|
if (report2.allowed || report2.spans.length === 0) return void 0;
|
|
@@ -12349,7 +12594,7 @@ var decisionFor = (input, found, blocking) => {
|
|
|
12349
12594
|
spans: describeSpans(input.t, report2.spans),
|
|
12350
12595
|
fixes: describeFixes(input.t, report2.spans)
|
|
12351
12596
|
};
|
|
12352
|
-
if (
|
|
12597
|
+
if (blocking2) return { kind: "block", reason: input.t("prose.block.command", params2) };
|
|
12353
12598
|
const additionalContext = `finitemonkey: ${input.t("prose.warn.command", params2)}`;
|
|
12354
12599
|
return { kind: "allow", output: { hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext } } };
|
|
12355
12600
|
};
|
|
@@ -12357,9 +12602,9 @@ var proseCommandGuard = (input) => {
|
|
|
12357
12602
|
if (!SHELLS3.has(toolName2(input.payload))) return ALLOW4;
|
|
12358
12603
|
const found = proseInCommand(optionalString(toolInput2(input.payload), "command") ?? "");
|
|
12359
12604
|
if (found.length === 0) return ALLOW4;
|
|
12360
|
-
const
|
|
12605
|
+
const blocking2 = projectPreset(input.runtime, projectRoot(input)) === "gabriel";
|
|
12361
12606
|
for (const item of found) {
|
|
12362
|
-
const decision = decisionFor(input, item,
|
|
12607
|
+
const decision = decisionFor(input, item, blocking2);
|
|
12363
12608
|
if (decision !== void 0) return decision;
|
|
12364
12609
|
}
|
|
12365
12610
|
return ALLOW4;
|
|
@@ -12523,11 +12768,24 @@ var relativeFile = (cwd, file) => {
|
|
|
12523
12768
|
const rel = relative7(cwd, file).split("\\").join("/");
|
|
12524
12769
|
return rel.startsWith("..") || rel === "" ? file : rel;
|
|
12525
12770
|
};
|
|
12771
|
+
var warningDecision = (t, warnings) => {
|
|
12772
|
+
if (warnings.length === 0) return ALLOW5;
|
|
12773
|
+
const context = t("secrets.warn.write", { found: describeFindings(t, warnings) });
|
|
12774
|
+
return { kind: "allow", output: { hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext: context } } };
|
|
12775
|
+
};
|
|
12526
12776
|
var writeDecision = (input, tool) => {
|
|
12527
12777
|
const { t } = input;
|
|
12778
|
+
const warnings = [];
|
|
12528
12779
|
for (const written of writtenTexts(tool, toolInput2(input.payload))) {
|
|
12529
12780
|
const file = relativeFile(input.cwd, written.file);
|
|
12530
|
-
const
|
|
12781
|
+
const all = scanText(written.text, { path: file });
|
|
12782
|
+
warnings.push(
|
|
12783
|
+
...all.filter(isWarning).map((finding3) => ({
|
|
12784
|
+
ruleId: finding3.ruleId,
|
|
12785
|
+
where: t("secrets.where.file", { file, line: finding3.line, column: finding3.column })
|
|
12786
|
+
}))
|
|
12787
|
+
);
|
|
12788
|
+
const found = blocking(all);
|
|
12531
12789
|
if (found.length === 0) continue;
|
|
12532
12790
|
const items = found.map((finding3) => ({
|
|
12533
12791
|
ruleId: finding3.ruleId,
|
|
@@ -12535,7 +12793,7 @@ var writeDecision = (input, tool) => {
|
|
|
12535
12793
|
}));
|
|
12536
12794
|
return { kind: "block", reason: t("secrets.block.write", { file, found: describeFindings(t, items) }) };
|
|
12537
12795
|
}
|
|
12538
|
-
return
|
|
12796
|
+
return warningDecision(t, warnings);
|
|
12539
12797
|
};
|
|
12540
12798
|
var bashMessage = (t, finding3) => {
|
|
12541
12799
|
switch (finding3.kind) {
|
|
@@ -12549,8 +12807,9 @@ var bashMessage = (t, finding3) => {
|
|
|
12549
12807
|
};
|
|
12550
12808
|
var commitDecision = (input, includeUnstaged) => {
|
|
12551
12809
|
const staged = scanStaged(input.runtime, input.cwd, includeUnstaged);
|
|
12552
|
-
|
|
12553
|
-
|
|
12810
|
+
const found = staged.ok ? blocking(staged.value) : [];
|
|
12811
|
+
if (found.length === 0) return ALLOW5;
|
|
12812
|
+
const items = found.map((finding3) => ({
|
|
12554
12813
|
ruleId: finding3.ruleId,
|
|
12555
12814
|
where: input.t("secrets.where.file", { file: finding3.file, line: finding3.line, column: finding3.column })
|
|
12556
12815
|
}));
|
|
@@ -12562,7 +12821,7 @@ var bashDecision = (input) => {
|
|
|
12562
12821
|
const segments = segmentsOf(command);
|
|
12563
12822
|
const [first] = bashFindings(command, segments);
|
|
12564
12823
|
if (first !== void 0) return { kind: "block", reason: bashMessage(input.t, first) };
|
|
12565
|
-
const found = scanText(command);
|
|
12824
|
+
const found = blocking(scanText(command));
|
|
12566
12825
|
if (found.length > 0) {
|
|
12567
12826
|
const items = found.map((finding3) => ({ ruleId: finding3.ruleId, where: lineWhere(input.t, finding3) }));
|
|
12568
12827
|
return { kind: "block", reason: input.t("secrets.block.bash", { found: describeFindings(input.t, items) }) };
|
|
@@ -12587,6 +12846,44 @@ var secretsPreToolUse = (input) => {
|
|
|
12587
12846
|
return ALLOW5;
|
|
12588
12847
|
};
|
|
12589
12848
|
|
|
12849
|
+
// src/secrets/remotes.ts
|
|
12850
|
+
var USER_AND_SECRET = /^[a-z][a-z0-9+.-]*:\/\/[^\s/@:]+:[^\s/@]+@/i;
|
|
12851
|
+
var USERINFO = /^[a-z][a-z0-9+.-]*:\/\/(?<user>[^\s/@]+)@/i;
|
|
12852
|
+
var hasCredential = (url) => {
|
|
12853
|
+
if (USER_AND_SECRET.test(url)) return true;
|
|
12854
|
+
const user = USERINFO.exec(url)?.groups?.["user"];
|
|
12855
|
+
return user !== void 0 && scanText(user, { rules: CREDENTIAL_RULES }).length > 0;
|
|
12856
|
+
};
|
|
12857
|
+
var parseRemotes = (output) => {
|
|
12858
|
+
const remotes = /* @__PURE__ */ new Map();
|
|
12859
|
+
for (const line2 of output.split(/\r?\n/)) {
|
|
12860
|
+
const [name, url] = line2.trim().split(/\s+/);
|
|
12861
|
+
if (name === void 0 || url === void 0 || name === "") continue;
|
|
12862
|
+
remotes.set(name, [...remotes.get(name) ?? [], url]);
|
|
12863
|
+
}
|
|
12864
|
+
return remotes;
|
|
12865
|
+
};
|
|
12866
|
+
var remotesWithCredentials = (runtime, root) => {
|
|
12867
|
+
const listed4 = runtime.git(["remote", "-v"], root);
|
|
12868
|
+
if (!listed4.ok) return [];
|
|
12869
|
+
return [...parseRemotes(listed4.stdout)].filter(([, urls]) => urls.some(hasCredential)).map(([name]) => name).sort();
|
|
12870
|
+
};
|
|
12871
|
+
|
|
12872
|
+
// src/secrets/guards/session.ts
|
|
12873
|
+
var secretsSessionStart = (input) => {
|
|
12874
|
+
const remotes = remotesWithCredentials(input.runtime, input.cwd);
|
|
12875
|
+
if (remotes.length === 0) return { kind: "allow" };
|
|
12876
|
+
const params2 = { remotes: remotes.join(", "), remote: remotes[0] ?? "origin" };
|
|
12877
|
+
const text6 = input.t("secrets.session.remote_credential", params2);
|
|
12878
|
+
return {
|
|
12879
|
+
kind: "allow",
|
|
12880
|
+
output: {
|
|
12881
|
+
systemMessage: `finitemonkey: ${text6}`,
|
|
12882
|
+
hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: text6 }
|
|
12883
|
+
}
|
|
12884
|
+
};
|
|
12885
|
+
};
|
|
12886
|
+
|
|
12590
12887
|
// src/hooks/handlers.ts
|
|
12591
12888
|
var allow = () => ({ kind: "allow" });
|
|
12592
12889
|
var DECISION_RANK = { allow: 0, defer: 0, ask: 1, deny: 2 };
|
|
@@ -12640,7 +12937,8 @@ var mergeHandlers = (...sets) => {
|
|
|
12640
12937
|
var SECRETS_HANDLERS = {
|
|
12641
12938
|
PreToolUse: [secretsPreToolUse],
|
|
12642
12939
|
UserPromptSubmit: [secretsUserPromptSubmit],
|
|
12643
|
-
PostToolUse: [secretsPostToolUse]
|
|
12940
|
+
PostToolUse: [secretsPostToolUse],
|
|
12941
|
+
SessionStart: [secretsSessionStart]
|
|
12644
12942
|
};
|
|
12645
12943
|
var DEFAULT_HANDLERS = mergeHandlers(
|
|
12646
12944
|
SECRETS_HANDLERS,
|
|
@@ -13407,6 +13705,22 @@ var checkProjectFiles = (env) => [
|
|
|
13407
13705
|
)
|
|
13408
13706
|
];
|
|
13409
13707
|
|
|
13708
|
+
// src/doctor/remotes.ts
|
|
13709
|
+
var checkRemoteCredentials = ({ root, runtime }) => {
|
|
13710
|
+
const remotes = remotesWithCredentials(runtime, root);
|
|
13711
|
+
if (remotes.length === 0) return [];
|
|
13712
|
+
const params2 = { remotes: remotes.join(", "), remote: remotes[0] ?? "origin" };
|
|
13713
|
+
return [
|
|
13714
|
+
finding(
|
|
13715
|
+
"secrets.remote_credential",
|
|
13716
|
+
"warn",
|
|
13717
|
+
"doctor.secrets.remote_credential",
|
|
13718
|
+
params2,
|
|
13719
|
+
"doctor.secrets.fix_remote_credential"
|
|
13720
|
+
)
|
|
13721
|
+
];
|
|
13722
|
+
};
|
|
13723
|
+
|
|
13410
13724
|
// src/doctor/run.ts
|
|
13411
13725
|
var DOCTOR_SCHEMA = "finitemonkey/doctor@1";
|
|
13412
13726
|
var CHECKS = [
|
|
@@ -13422,6 +13736,7 @@ var CHECKS = [
|
|
|
13422
13736
|
checkInstructions,
|
|
13423
13737
|
checkProjectFiles,
|
|
13424
13738
|
checkLineEndings,
|
|
13739
|
+
checkRemoteCredentials,
|
|
13425
13740
|
checkAgentsFile,
|
|
13426
13741
|
checkHooks
|
|
13427
13742
|
];
|
|
@@ -13449,7 +13764,7 @@ var toItem = (ctx, finding3) => ({
|
|
|
13449
13764
|
detail: ctx.t(finding3.messageKey, finding3.params),
|
|
13450
13765
|
...finding3.fixKey === void 0 ? {} : { fix: ctx.t(finding3.fixKey, finding3.params) }
|
|
13451
13766
|
});
|
|
13452
|
-
var
|
|
13767
|
+
var count3 = (items, status) => items.filter((item) => item.status === status).length;
|
|
13453
13768
|
var hookItems = (ctx, findings) => findings.flatMap(
|
|
13454
13769
|
(finding3) => finding3.hook === void 0 ? [] : [
|
|
13455
13770
|
{
|
|
@@ -13465,7 +13780,7 @@ var buildReport2 = (ctx, findings) => {
|
|
|
13465
13780
|
schema: DOCTOR_SCHEMA,
|
|
13466
13781
|
checks,
|
|
13467
13782
|
hooks: hookItems(ctx, findings),
|
|
13468
|
-
summary: { ok:
|
|
13783
|
+
summary: { ok: count3(checks, "ok"), warn: count3(checks, "warn"), fail: count3(checks, "fail") }
|
|
13469
13784
|
};
|
|
13470
13785
|
};
|
|
13471
13786
|
|
|
@@ -13782,7 +14097,7 @@ var resultOf = (ctx, checked) => {
|
|
|
13782
14097
|
const problem4 = gitProblem("merge_unanalyzable", "git.error.merge_unanalyzable", detail, "git status");
|
|
13783
14098
|
return failureFrom(ctx.t, { ...problem4, exitCode: ExitCode.Usage, data }, human2);
|
|
13784
14099
|
}
|
|
13785
|
-
const lost = checked.reduce((
|
|
14100
|
+
const lost = checked.reduce((count4, item) => count4 + item.report.findings.length, 0);
|
|
13786
14101
|
if (lost === 0) return success(data, human2);
|
|
13787
14102
|
return failureFrom(
|
|
13788
14103
|
ctx.t,
|
|
@@ -14313,17 +14628,17 @@ var rulesFor = (locale) => {
|
|
|
14313
14628
|
var branchFor = (node, value, locale) => {
|
|
14314
14629
|
const { branches } = node;
|
|
14315
14630
|
if (node.kind === "select") return branches[String(value)] ?? branches["other"];
|
|
14316
|
-
const
|
|
14317
|
-
if (!Number.isFinite(
|
|
14318
|
-
return branches[`=${String(
|
|
14631
|
+
const count4 = typeof value === "number" ? value : Number(value);
|
|
14632
|
+
if (!Number.isFinite(count4)) return branches["other"];
|
|
14633
|
+
return branches[`=${String(count4)}`] ?? branches[rulesFor(locale).select(count4)] ?? branches["other"];
|
|
14319
14634
|
};
|
|
14320
|
-
var render = (nodes, params2, locale,
|
|
14635
|
+
var render = (nodes, params2, locale, count4) => nodes.map((node) => {
|
|
14321
14636
|
if (node.kind === "text") return node.text;
|
|
14322
|
-
if (node.kind === "count") return
|
|
14637
|
+
if (node.kind === "count") return count4 === void 0 ? "#" : String(count4);
|
|
14323
14638
|
const value = params2[node.name];
|
|
14324
14639
|
if (node.kind === "arg") return value === void 0 ? `{${node.name}}` : String(value);
|
|
14325
14640
|
const branch = branchFor(node, value, locale) ?? [];
|
|
14326
|
-
return render(branch, params2, locale, node.kind === "plural" ? value :
|
|
14641
|
+
return render(branch, params2, locale, node.kind === "plural" ? value : count4);
|
|
14327
14642
|
}).join("");
|
|
14328
14643
|
var formatMessage = (source, params2, locale) => render(parseMessage(source), params2, locale, void 0);
|
|
14329
14644
|
var walk = (nodes, visit2) => {
|
|
@@ -14353,7 +14668,7 @@ var choicesOf = (source) => {
|
|
|
14353
14668
|
var requiredPluralBranches = (locale) => {
|
|
14354
14669
|
const rules = rulesFor(locale);
|
|
14355
14670
|
const seen = /* @__PURE__ */ new Set(["other"]);
|
|
14356
|
-
for (let
|
|
14671
|
+
for (let count4 = 0; count4 <= 1e3; count4 += 1) seen.add(rules.select(count4));
|
|
14357
14672
|
return [...seen].sort();
|
|
14358
14673
|
};
|
|
14359
14674
|
|
|
@@ -14666,6 +14981,7 @@ var lookBack = (runtime, root, env) => {
|
|
|
14666
14981
|
|
|
14667
14982
|
// src/install/diff.ts
|
|
14668
14983
|
var MAX_DIFF_LINES = 120;
|
|
14984
|
+
var MAX_LCS_CELLS = 4e6;
|
|
14669
14985
|
var table = (a, b) => {
|
|
14670
14986
|
const rows = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
14671
14987
|
for (let i = a.length - 1; i >= 0; i -= 1) {
|
|
@@ -14700,8 +15016,27 @@ var walk2 = (a, b) => {
|
|
|
14700
15016
|
return ops;
|
|
14701
15017
|
};
|
|
14702
15018
|
var splitLines = (text6) => text6 === "" ? [] : text6.replace(/\r?\n$/, "").split(/\r?\n/);
|
|
15019
|
+
var sharedPrefix = (a, b) => {
|
|
15020
|
+
let count4 = 0;
|
|
15021
|
+
while (count4 < a.length && count4 < b.length && a[count4] === b[count4]) count4 += 1;
|
|
15022
|
+
return count4;
|
|
15023
|
+
};
|
|
15024
|
+
var sharedSuffix = (a, b, prefix) => {
|
|
15025
|
+
let count4 = 0;
|
|
15026
|
+
while (count4 < a.length - prefix && count4 < b.length - prefix && a[a.length - 1 - count4] === b[b.length - 1 - count4])
|
|
15027
|
+
count4 += 1;
|
|
15028
|
+
return count4;
|
|
15029
|
+
};
|
|
15030
|
+
var changedOps = (a, b) => {
|
|
15031
|
+
const prefix = sharedPrefix(a, b);
|
|
15032
|
+
const suffix = sharedSuffix(a, b, prefix);
|
|
15033
|
+
const left = a.slice(prefix, a.length - suffix);
|
|
15034
|
+
const right = b.slice(prefix, b.length - suffix);
|
|
15035
|
+
if (left.length * right.length <= MAX_LCS_CELLS) return walk2(left, right).filter(([op]) => op !== "=");
|
|
15036
|
+
return [...left.map((line2) => ["-", line2]), ...right.map((line2) => ["+", line2])];
|
|
15037
|
+
};
|
|
14703
15038
|
var lineDiff = (path, before, after) => {
|
|
14704
|
-
const changes =
|
|
15039
|
+
const changes = changedOps(splitLines(before), splitLines(after));
|
|
14705
15040
|
if (changes.length === 0) return [];
|
|
14706
15041
|
const lines3 = changes.map(([op, line2]) => `${op} ${line2}`);
|
|
14707
15042
|
const shown3 = lines3.slice(0, MAX_DIFF_LINES);
|
|
@@ -15274,7 +15609,7 @@ var humanOf2 = (ctx, data) => [
|
|
|
15274
15609
|
];
|
|
15275
15610
|
var resultOf3 = (ctx, data) => {
|
|
15276
15611
|
const human2 = humanOf2(ctx, data);
|
|
15277
|
-
const failing = data.
|
|
15612
|
+
const failing = data.findings.filter((item) => item.status !== "known" && item.severity !== "warning").length;
|
|
15278
15613
|
if (failing === 0) return success(data, human2);
|
|
15279
15614
|
return failureFrom(
|
|
15280
15615
|
ctx.t,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "finitemonkey",
|
|
3
|
-
"version": "0.1.0-next.
|
|
3
|
+
"version": "0.1.0-next.15",
|
|
4
4
|
"description": "Claude Code hooks that stop a coding agent from saying done before the tests pass: a machine-checked definition of done, plus guardrails for secrets, git safety and production. Pre-release.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"budgets": "tsx scripts/budgets.ts",
|
|
69
69
|
"docs": "tsx scripts/build-docs.ts",
|
|
70
70
|
"docs:check": "tsc -p scripts && tsx scripts/build-docs.ts --check",
|
|
71
|
+
"e2e:claude": "node --import tsx --test --test-concurrency=1 test/e2e/claude-stop.e2e.ts",
|
|
71
72
|
"plugin:sync": "node scripts/plugin-sync.mjs",
|
|
72
73
|
"plugin:validate": "node scripts/plugin-validate.mjs",
|
|
73
74
|
"version": "node scripts/plugin-sync.mjs"
|