finitemonkey 0.1.0-next.13 → 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.
Files changed (2) hide show
  1. package/dist/finitemonkey.mjs +633 -265
  2. package/package.json +2 -1
@@ -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 count3 of counts.values()) {
1624
- const p = count3 / text6.length;
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
 
@@ -2227,8 +2256,28 @@ var reportMarkdown = (env, ctx, envelope) => {
2227
2256
  };
2228
2257
  var reportPath = (env) => toAbsolutePath(join5(env.root, ...REPORTS_DIR.split("/"), `doctor-${stamp(env.runtime.now())}.md`));
2229
2258
 
2230
- // src/doctor/settings.ts
2259
+ // src/install/plugin-yield.ts
2231
2260
  import { join as join6 } from "node:path";
2261
+ var PLUGIN_VIA = "plugin";
2262
+ var YIELD_STDOUT = `${JSON.stringify({ continue: true })}
2263
+ `;
2264
+ var projectOwnsHooks = (ctx) => {
2265
+ const root = ctx.env["CLAUDE_PROJECT_DIR"] ?? ctx.cwd;
2266
+ const text6 = ctx.runtime.readText(toAbsolutePath(join6(root, ...SETTINGS_FILE.split("/"))));
2267
+ if (!text6.ok) return false;
2268
+ const parsed = parseJsonObject(text6.value);
2269
+ return parsed.ok && ownHandlersIn(parsed.value) > 0;
2270
+ };
2271
+
2272
+ // src/install/plugin-hooks.ts
2273
+ var PLUGIN_NAME = "finitemonkey";
2274
+ var MARKETPLACE_NAME = "finitemonkey";
2275
+ var NEXT_MARKETPLACE_NAME = "finitemonkey-next";
2276
+ var STABLE_PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
2277
+ var NEXT_PLUGIN_ID = `${PLUGIN_NAME}@${NEXT_MARKETPLACE_NAME}`;
2278
+
2279
+ // src/doctor/settings.ts
2280
+ import { join as join7 } from "node:path";
2232
2281
  var MANAGED = {
2233
2282
  darwin: "/Library/Application Support/ClaudeCode/managed-settings.json",
2234
2283
  linux: "/etc/claude-code/managed-settings.json",
@@ -2237,7 +2286,7 @@ var MANAGED = {
2237
2286
  var userSettingsPath = ({ env, runtime }) => {
2238
2287
  const configDir = env["CLAUDE_CONFIG_DIR"];
2239
2288
  return toAbsolutePath(
2240
- configDir === void 0 ? join6(runtime.homeDir(), ".claude", "settings.json") : join6(configDir, "settings.json")
2289
+ configDir === void 0 ? join7(runtime.homeDir(), ".claude", "settings.json") : join7(configDir, "settings.json")
2241
2290
  );
2242
2291
  };
2243
2292
  var settingsPaths = (env) => {
@@ -2245,8 +2294,8 @@ var settingsPaths = (env) => {
2245
2294
  return [
2246
2295
  ...managed === void 0 ? [] : [{ scope: "managed", path: toAbsolutePath(managed) }],
2247
2296
  { scope: "user", path: userSettingsPath(env) },
2248
- { scope: "project", path: toAbsolutePath(join6(env.root, ".claude", "settings.json")) },
2249
- { scope: "local", path: toAbsolutePath(join6(env.root, ".claude", "settings.local.json")) }
2297
+ { scope: "project", path: toAbsolutePath(join7(env.root, ".claude", "settings.json")) },
2298
+ { scope: "local", path: toAbsolutePath(join7(env.root, ".claude", "settings.local.json")) }
2250
2299
  ];
2251
2300
  };
2252
2301
  var readSettings = (env) => settingsPaths(env).flatMap(({ scope: scope2, path }) => {
@@ -2358,6 +2407,21 @@ var cleanupPeriod = (env, all) => {
2358
2407
  )
2359
2408
  ];
2360
2409
  };
2410
+ var PRECEDENCE = ["user", "project", "local", "managed"];
2411
+ var pluginOn = (all, id) => {
2412
+ const ordered = PRECEDENCE.flatMap((scope2) => all.filter((item) => item.scope === scope2));
2413
+ const values = ordered.map(({ settings }) => settings["enabledPlugins"]).map((plugins) => isRecord(plugins) ? plugins[id] : void 0).filter((value) => typeof value === "boolean");
2414
+ return values.at(-1) === true;
2415
+ };
2416
+ var pluginChannels = (all) => pluginOn(all, STABLE_PLUGIN_ID) && pluginOn(all, NEXT_PLUGIN_ID) ? [
2417
+ finding(
2418
+ "settings.plugin_channels",
2419
+ "fail",
2420
+ "doctor.settings.plugin_channels",
2421
+ { stable: STABLE_PLUGIN_ID, next: NEXT_PLUGIN_ID },
2422
+ "doctor.settings.fix_plugin_channels"
2423
+ )
2424
+ ] : [];
2361
2425
  var checkClaudeSettings = (env) => {
2362
2426
  const all = readSettings(env);
2363
2427
  const plugins = enabledPlugins(all);
@@ -2367,6 +2431,7 @@ var checkClaudeSettings = (env) => {
2367
2431
  all,
2368
2432
  plugins.some((id) => id.startsWith("finitemonkey@"))
2369
2433
  ),
2434
+ ...pluginChannels(all),
2370
2435
  ...agentTeams(env, all),
2371
2436
  ...loopPlugins(plugins),
2372
2437
  ...cleanupPeriod(env, all)
@@ -2374,7 +2439,7 @@ var checkClaudeSettings = (env) => {
2374
2439
  };
2375
2440
 
2376
2441
  // src/doctor/environment.ts
2377
- import { join as join7 } from "node:path";
2442
+ import { join as join8 } from "node:path";
2378
2443
  var AUTH_VARIABLES = [
2379
2444
  "ANTHROPIC_API_KEY",
2380
2445
  "ANTHROPIC_AUTH_TOKEN",
@@ -2412,7 +2477,7 @@ var pathDirs = ({ env, platform }) => {
2412
2477
  var copiesOnPath = (env) => {
2413
2478
  const names = EXECUTABLES[env.platform] ?? ["finitemonkey"];
2414
2479
  const found = pathDirs(env).flatMap(
2415
- (dir) => names.map((name) => toAbsolutePath(join7(dir, name))).filter((path) => env.runtime.stat(path)?.kind === "file").map((path) => env.runtime.realpath(path))
2480
+ (dir) => names.map((name) => toAbsolutePath(join8(dir, name))).filter((path) => env.runtime.stat(path)?.kind === "file").map((path) => env.runtime.realpath(path))
2416
2481
  );
2417
2482
  const byDir = new Map(found.map((path) => [path.replace(/\.(cmd|exe|ps1)$/i, ""), path]));
2418
2483
  return [...byDir.values()];
@@ -2446,7 +2511,7 @@ var checkAuthVariables = ({ env, platform }) => {
2446
2511
  };
2447
2512
 
2448
2513
  // src/doctor/hooks.ts
2449
- import { join as join28 } from "node:path";
2514
+ import { join as join29 } from "node:path";
2450
2515
 
2451
2516
  // src/hooks/events.ts
2452
2517
  var HOOK_EVENTS = [
@@ -3126,9 +3191,9 @@ var skipOptions = (args, wrapper) => {
3126
3191
  }
3127
3192
  return index + wrapper.positionals;
3128
3193
  };
3129
- var shift = (stage, count3, name) => ({
3194
+ var shift = (stage, count4, name) => ({
3130
3195
  ...stage,
3131
- argv: stage.argv.slice(count3),
3196
+ argv: stage.argv.slice(count4),
3132
3197
  via: [...stage.via, name]
3133
3198
  });
3134
3199
  var envCommand = (stage) => {
@@ -3509,7 +3574,7 @@ var selfGuard = (input) => {
3509
3574
  };
3510
3575
 
3511
3576
  // src/guards/tests/hook.ts
3512
- import { join as join8, relative as relative3 } from "node:path";
3577
+ import { join as join9, relative as relative3 } from "node:path";
3513
3578
 
3514
3579
  // src/guards/tests/patterns.ts
3515
3580
  var JS_TEST = /(^|\/)(__tests__|__test__)\/|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)tests?\/.*\.[cm]?[jt]sx?$/;
@@ -3699,7 +3764,7 @@ var shellViolations = (input, root) => {
3699
3764
  return parseShell(command).flatMap(
3700
3765
  (simple) => writtenPaths(simple).flatMap((path) => {
3701
3766
  const local = localPath(root, path);
3702
- const exists = input.runtime.stat(toAbsolutePath(join8(root, local))) !== void 0;
3767
+ const exists = input.runtime.stat(toAbsolutePath(join9(root, local))) !== void 0;
3703
3768
  if (!exists) return [];
3704
3769
  if (isProtectedConfig(local)) return [{ kind: "config_changed", path: local }];
3705
3770
  return isTestFile(local) ? [{ kind: "test_removed", path: local }] : [];
@@ -3719,17 +3784,17 @@ var testsGuard = (input) => {
3719
3784
  };
3720
3785
 
3721
3786
  // src/gate/hooks/config.ts
3722
- import { join as join10 } from "node:path";
3787
+ import { join as join11 } from "node:path";
3723
3788
 
3724
3789
  // src/gate/hooks/common.ts
3725
- import { dirname as dirname5, join as join9 } from "node:path";
3790
+ import { dirname as dirname5, join as join10 } from "node:path";
3726
3791
  var DEFAULT_GATE_BUDGET_MS = 3e5;
3727
3792
  var sessionCwd = (input) => input.payload.cwd === void 0 ? input.cwd : toAbsolutePath(input.payload.cwd, input.cwd);
3728
3793
  var MAX_DEPTH2 = 64;
3729
3794
  var findRepoRoot = (runtime, start) => {
3730
3795
  let current = start;
3731
3796
  for (let depth = 0; depth < MAX_DEPTH2; depth += 1) {
3732
- if (runtime.stat(toAbsolutePath(join9(current, ".git"))) !== void 0) return current;
3797
+ if (runtime.stat(toAbsolutePath(join10(current, ".git"))) !== void 0) return current;
3733
3798
  const parent = toAbsolutePath(dirname5(current));
3734
3799
  if (parent === current) return void 0;
3735
3800
  current = parent;
@@ -3766,9 +3831,9 @@ var censusOf = (text6) => {
3766
3831
  return { hooks: countIn(parsed.value["hooks"]), disabled: parsed.value["disableAllHooks"] === true };
3767
3832
  };
3768
3833
  var settingsFiles = (paths, env) => [
3769
- toAbsolutePath(join10(paths.root, ".claude", "settings.json")),
3770
- toAbsolutePath(join10(paths.root, ".claude", "settings.local.json")),
3771
- ...env["HOME"] === void 0 ? [] : [toAbsolutePath(join10(env["HOME"], ".claude", "settings.json"))]
3834
+ toAbsolutePath(join11(paths.root, ".claude", "settings.json")),
3835
+ toAbsolutePath(join11(paths.root, ".claude", "settings.local.json")),
3836
+ ...env["HOME"] === void 0 ? [] : [toAbsolutePath(join11(env["HOME"], ".claude", "settings.json"))]
3772
3837
  ];
3773
3838
  var read2 = (runtime, path) => {
3774
3839
  const text6 = runtime.readText(path);
@@ -3912,7 +3977,93 @@ var implicitContract = (files) => {
3912
3977
  };
3913
3978
 
3914
3979
  // src/gate/prepare.ts
3915
- import { join as join11 } from "node:path";
3980
+ import { join as join14 } from "node:path";
3981
+
3982
+ // src/git/project-config.ts
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
+ };
3916
4067
 
3917
4068
  // src/core/yaml/scalar.ts
3918
4069
  var ESCAPES = {
@@ -4060,7 +4211,7 @@ var parseValue = (text6) => {
4060
4211
 
4061
4212
  // src/core/yaml/parse.ts
4062
4213
  var BOM = new RegExp(`^${String.fromCharCode(65279)}`);
4063
- var stripComment = (text6) => {
4214
+ var stripComment2 = (text6) => {
4064
4215
  for (let index = 0; index < text6.length; index += 1) {
4065
4216
  const char = text6[index];
4066
4217
  if (char === '"' || char === "'") {
@@ -4075,7 +4226,7 @@ var toLines = (source) => {
4075
4226
  const lines3 = [];
4076
4227
  const raw = source.replace(BOM, "").split(/\r?\n/);
4077
4228
  for (const [index, full] of raw.entries()) {
4078
- const text6 = stripComment(full).trimEnd();
4229
+ const text6 = stripComment2(full).trimEnd();
4079
4230
  const body2 = text6.trimStart();
4080
4231
  if (body2 === "" || body2 === "---" && lines3.length === 0) continue;
4081
4232
  const indentText = text6.slice(0, text6.length - body2.length);
@@ -4183,7 +4334,8 @@ var CHECK_KINDS = [
4183
4334
  "worktree",
4184
4335
  "merge",
4185
4336
  "prod",
4186
- "prose"
4337
+ "prose",
4338
+ "review"
4187
4339
  ];
4188
4340
  var GIT_REQUIREMENTS = ["committed", "pushed", "pr_open"];
4189
4341
  var SIZES = ["S", "M", "L"];
@@ -4389,6 +4541,9 @@ var readBaseline = (runtime, paths) => {
4389
4541
  };
4390
4542
  var writeBaseline = (runtime, paths, treeHash2, checks) => writeJsonFile(runtime, paths.cache(BASELINE_FILE), { treeHash: treeHash2, checks: { ...checks } }).ok;
4391
4543
 
4544
+ // src/gate/diff-shape.ts
4545
+ import { join as join13 } from "node:path";
4546
+
4392
4547
  // src/gate/git.ts
4393
4548
  var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
4394
4549
  var GIT_TIMEOUT_MS = 15e3;
@@ -4488,10 +4643,20 @@ var numstat = (git, base) => {
4488
4643
  };
4489
4644
  var dirtyPaths = (git) => nulFields(git.out(["status", "--porcelain", "-z", "--", ".", EXCLUDE_FM]) ?? "").map((entry2) => entry2.slice(3));
4490
4645
  var aheadOfUpstream = (git) => {
4491
- const count3 = git.out(["rev-list", "--count", "@{u}..HEAD"])?.trim();
4492
- return count3 === void 0 ? void 0 : Number(count3);
4646
+ const count4 = git.out(["rev-list", "--count", "@{u}..HEAD"])?.trim();
4647
+ return count4 === void 0 ? void 0 : Number(count4);
4493
4648
  };
4494
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
+
4495
4660
  // src/gate/pr-parse.ts
4496
4661
  var FAILED = /* @__PURE__ */ new Set(["FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE", "ERROR"]);
4497
4662
  var PENDING = /* @__PURE__ */ new Set(["IN_PROGRESS", "QUEUED", "PENDING", "WAITING", "REQUESTED", "EXPECTED"]);
@@ -4582,13 +4747,118 @@ var conflictFiles = (git, base) => {
4582
4747
  return [];
4583
4748
  };
4584
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
+
4585
4855
  // src/gate/prepare.ts
4586
4856
  var diskFiles = (runtime, root) => ({
4587
4857
  read: (relative9) => {
4588
- const text6 = runtime.readText(toAbsolutePath(join11(root, relative9)));
4858
+ const text6 = runtime.readText(toAbsolutePath(join14(root, relative9)));
4589
4859
  return text6.ok ? text6.value : void 0;
4590
4860
  },
4591
- exists: (relative9) => runtime.stat(toAbsolutePath(join11(root, relative9))) !== void 0
4861
+ exists: (relative9) => runtime.stat(toAbsolutePath(join14(root, relative9))) !== void 0
4592
4862
  });
4593
4863
  var baseFiles = (git, base, disk) => ({
4594
4864
  read: (relative9) => base === EMPTY_TREE ? disk.read(relative9) : fileAt(git, base, relative9),
@@ -4620,6 +4890,7 @@ var skipReason = (runtime, paths, loaded, changes, tree, sessionId) => {
4620
4890
  const session = readSession(runtime, paths);
4621
4891
  return session !== void 0 && session.sessionId === sessionId && session.startTree === tree ? "no_session_edits" : void 0;
4622
4892
  };
4893
+ var measureSize = (runtime, git, base, changes, contract) => sizeVerdict(contract.size, diffShape(runtime, git, base, changes), sizeConfig(loadProjectConfig(runtime, git.cwd)));
4623
4894
  var prepareGate = (options) => {
4624
4895
  const probe = gitAt(options.runtime, options.cwd);
4625
4896
  const root = gitRoot(probe);
@@ -4639,11 +4910,13 @@ var prepareGate = (options) => {
4639
4910
  const skip = options.noSkip === true ? void 0 : skipReason(options.runtime, paths, loaded.value, changes, tree, options.sessionId);
4640
4911
  if (skip !== void 0) return { kind: "skipped", reason: skip };
4641
4912
  const pr = memo(() => prStatus(options.runtime, git, paths));
4913
+ const size = measureSize(options.runtime, git, base, changes, loaded.value.contract);
4642
4914
  const ctx = {
4643
4915
  runtime: options.runtime,
4644
4916
  git,
4645
4917
  paths,
4646
- contract: loaded.value.contract,
4918
+ contract: applySize(loaded.value.contract, size),
4919
+ size,
4647
4920
  trusted: loaded.value.trusted,
4648
4921
  treeHash: tree,
4649
4922
  base,
@@ -5006,6 +5279,8 @@ var en_default = {
5006
5279
  "doctor.settings.fix_managed_hooks_only": "Ask the administrator of the managed settings to add the Finite Monkey hooks there, or to turn allowManagedHooksOnly off",
5007
5280
  "doctor.settings.plugin_only": "strictPluginOnlyCustomization is on ({scopes}) and the finitemonkey plugin is not enabled; the project hooks do not load",
5008
5281
  "doctor.settings.fix_plugin_only": "Install the plugin: finitemonkey init --marketplace --yes, then /plugin install finitemonkey@finitemonkey in Claude Code",
5282
+ "doctor.settings.plugin_channels": "Both {stable} and {next} are enabled. They share the plugin name, so Claude Code keeps only one set of hooks",
5283
+ "doctor.settings.fix_plugin_channels": "To stay on stable run claude plugin disable {next}; to test next run claude plugin disable {stable}",
5009
5284
  "doctor.settings.agent_teams": "{name} is on; teammates run in their own sessions, and Finite Monkey does not check their work before they report done",
5010
5285
  "doctor.settings.fix_agent_teams": "Run `finitemonkey gate` before accepting a teammate's result, or unset the variable",
5011
5286
  "doctor.settings.loop_plugins": "{names} enabled: it sends the agent back to work at Stop, and its loop has no guard of its own",
@@ -5420,7 +5695,18 @@ var en_default = {
5420
5695
  "history.pattern.no_verify": "{count, plural, one {# git command} other {# git commands}} with --no-verify",
5421
5696
  "history.pattern.force_push": "{count, plural, one {# force push} other {# force pushes}}",
5422
5697
  "history.pattern.test_skip": "{count, plural, one {# test edit} other {# test edits}} that added .skip, .only or a skip marker",
5423
- "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."
5424
5710
  };
5425
5711
 
5426
5712
  // src/i18n/es.json
@@ -5517,6 +5803,8 @@ var es_default = {
5517
5803
  "doctor.settings.fix_managed_hooks_only": "Pide al administrador de los settings gestionados que añada allí los hooks de Finite Monkey, o que desactive allowManagedHooksOnly",
5518
5804
  "doctor.settings.plugin_only": "strictPluginOnlyCustomization está activado ({scopes}) y el plugin finitemonkey no está activo; los hooks del proyecto no se cargan",
5519
5805
  "doctor.settings.fix_plugin_only": "Instala el plugin: finitemonkey init --marketplace --yes, después /plugin install finitemonkey@finitemonkey en Claude Code",
5806
+ "doctor.settings.plugin_channels": "{stable} y {next} están activos a la vez. Comparten el nombre del plugin, así que Claude Code se queda con un solo conjunto de hooks",
5807
+ "doctor.settings.fix_plugin_channels": "Para quedarte en la versión estable ejecuta claude plugin disable {next}; para probar next ejecuta claude plugin disable {stable}",
5520
5808
  "doctor.settings.agent_teams": "{name} está activado; los compañeros de equipo corren en sesiones propias, y Finite Monkey no revisa su trabajo antes de que digan que terminaron",
5521
5809
  "doctor.settings.fix_agent_teams": "Ejecuta `finitemonkey gate` antes de aceptar el resultado de un compañero, o quita la variable",
5522
5810
  "doctor.settings.loop_plugins": "{names} activo: devuelve al agente al trabajo en Stop, y su bucle no tiene guardia propia",
@@ -5931,7 +6219,18 @@ var es_default = {
5931
6219
  "history.pattern.no_verify": "{count, plural, one {# comando git} other {# comandos git}} con --no-verify",
5932
6220
  "history.pattern.force_push": "{count, plural, one {# push forzado} other {# push forzados}}",
5933
6221
  "history.pattern.test_skip": "{count, plural, one {# edición} other {# ediciones}} de pruebas que añadieron .skip, .only o una marca para saltar",
5934
- "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."
5935
6234
  };
5936
6235
 
5937
6236
  // src/i18n/pt-BR.json
@@ -6028,6 +6327,8 @@ var pt_BR_default = {
6028
6327
  "doctor.settings.fix_managed_hooks_only": "Peça ao administrador dos settings gerenciados para incluir os hooks do Finite Monkey lá, ou para desligar allowManagedHooksOnly",
6029
6328
  "doctor.settings.plugin_only": "strictPluginOnlyCustomization está ligado ({scopes}) e o plugin finitemonkey não está ativo; os hooks do projeto não carregam",
6030
6329
  "doctor.settings.fix_plugin_only": "Instale o plugin: finitemonkey init --marketplace --yes, depois /plugin install finitemonkey@finitemonkey no Claude Code",
6330
+ "doctor.settings.plugin_channels": "{stable} e {next} estão ativos ao mesmo tempo. Os dois têm o mesmo nome de plugin, então o Claude Code fica com só um conjunto de hooks",
6331
+ "doctor.settings.fix_plugin_channels": "Para ficar no estável rode claude plugin disable {next}; para testar o next rode claude plugin disable {stable}",
6031
6332
  "doctor.settings.agent_teams": "{name} está ligado; os colegas de equipe rodam em sessões próprias, e o Finite Monkey não confere o trabalho deles antes de dizerem que terminaram",
6032
6333
  "doctor.settings.fix_agent_teams": "Rode `finitemonkey gate` antes de aceitar o resultado de um colega, ou remova a variável",
6033
6334
  "doctor.settings.loop_plugins": "{names} ativo: ele manda o agente de volta ao trabalho no Stop, e o laço dele não tem guarda própria",
@@ -6442,7 +6743,18 @@ var pt_BR_default = {
6442
6743
  "history.pattern.no_verify": "{count, plural, one {# comando git} other {# comandos git}} com --no-verify",
6443
6744
  "history.pattern.force_push": "{count, plural, one {# push forçado} other {# pushes forçados}}",
6444
6745
  "history.pattern.test_skip": "{count, plural, one {# edição} other {# edições}} de teste que acrescentaram .skip, .only ou marcador de pular",
6445
- "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."
6446
6758
  };
6447
6759
 
6448
6760
  // src/i18n/review.json
@@ -6540,6 +6852,8 @@ var review_default = {
6540
6852
  "doctor.settings.fix_managed_hooks_only": "machine",
6541
6853
  "doctor.settings.plugin_only": "machine",
6542
6854
  "doctor.settings.fix_plugin_only": "machine",
6855
+ "doctor.settings.plugin_channels": "machine",
6856
+ "doctor.settings.fix_plugin_channels": "machine",
6543
6857
  "doctor.settings.agent_teams": "machine",
6544
6858
  "doctor.settings.fix_agent_teams": "machine",
6545
6859
  "doctor.settings.loop_plugins": "machine",
@@ -6954,7 +7268,18 @@ var review_default = {
6954
7268
  "history.pattern.no_verify": "machine",
6955
7269
  "history.pattern.force_push": "machine",
6956
7270
  "history.pattern.test_skip": "machine",
6957
- "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"
6958
7283
  },
6959
7284
  es: {
6960
7285
  "command.init.summary": "machine",
@@ -7049,6 +7374,8 @@ var review_default = {
7049
7374
  "doctor.settings.fix_managed_hooks_only": "machine",
7050
7375
  "doctor.settings.plugin_only": "machine",
7051
7376
  "doctor.settings.fix_plugin_only": "machine",
7377
+ "doctor.settings.plugin_channels": "machine",
7378
+ "doctor.settings.fix_plugin_channels": "machine",
7052
7379
  "doctor.settings.agent_teams": "machine",
7053
7380
  "doctor.settings.fix_agent_teams": "machine",
7054
7381
  "doctor.settings.loop_plugins": "machine",
@@ -7463,7 +7790,18 @@ var review_default = {
7463
7790
  "history.pattern.no_verify": "machine",
7464
7791
  "history.pattern.force_push": "machine",
7465
7792
  "history.pattern.test_skip": "machine",
7466
- "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"
7467
7805
  }
7468
7806
  };
7469
7807
 
@@ -7586,7 +7924,7 @@ var buildReport = (input) => {
7586
7924
  );
7587
7925
  return fromEvidence(check2.id, check2.kind, entry2, hasStale(ledger, check2.id, treeHash2));
7588
7926
  });
7589
- const count3 = (status) => items.filter((item) => item.status === status).length;
7927
+ const count4 = (status) => items.filter((item) => item.status === status).length;
7590
7928
  return {
7591
7929
  schema: GATE_SCHEMA,
7592
7930
  task: contract.id,
@@ -7597,7 +7935,7 @@ var buildReport = (input) => {
7597
7935
  source: input.source,
7598
7936
  contractEdited: input.contractEdited,
7599
7937
  verdict: verdictOf(items),
7600
- counts: { passed: count3("passed"), failed: count3("failed"), unverified: count3("unverified") },
7938
+ counts: { passed: count4("passed"), failed: count4("failed"), unverified: count4("unverified") },
7601
7939
  items,
7602
7940
  decisionsNeeded: contract.decisionsNeeded,
7603
7941
  fastPath: input.fastPath
@@ -7721,6 +8059,27 @@ var preCompact = (input) => {
7721
8059
  return { kind: "allow" };
7722
8060
  };
7723
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
+
7724
8083
  // src/gate/test-summary.ts
7725
8084
  var ANSI2 = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
7726
8085
  var COUNTERS = [
@@ -7860,44 +8219,14 @@ var runShellCheck = (check2, run4, ctx) => {
7860
8219
  };
7861
8220
  var commandCheck = (check2, ctx) => check2.run === void 0 ? unverified("not_implemented") : runShellCheck(check2, check2.run, ctx);
7862
8221
 
7863
- // src/gate/checks/diff.ts
7864
- import { join as join12 } from "node:path";
7865
-
7866
- // src/core/glob.ts
7867
- var escape = (char) => /[.+^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
7868
- var globToRegExp = (glob) => {
7869
- const pattern = glob.replace(/^\.\//, "");
7870
- let out = "";
7871
- for (let index = 0; index < pattern.length; index += 1) {
7872
- const char = pattern[index] ?? "";
7873
- if (char === "*" && pattern[index + 1] === "*") {
7874
- const slash = pattern[index + 2] === "/";
7875
- out += slash ? "(?:.*/)?" : ".*";
7876
- index += slash ? 2 : 1;
7877
- } else if (char === "*") out += "[^/]*";
7878
- else if (char === "?") out += "[^/]";
7879
- else out += escape(char);
7880
- }
7881
- return new RegExp(`^${out}${pattern.endsWith("/") ? ".*" : ""}$`);
7882
- };
7883
- var matchesAny = (path, globs) => {
7884
- const normalized = toPosixPath(path);
7885
- return globs.some((glob) => globToRegExp(glob).test(normalized));
7886
- };
7887
-
7888
8222
  // src/gate/checks/diff.ts
7889
8223
  var MAX_LISTED = 10;
7890
- var untrackedLines = (ctx) => ctx.changes.filter((change) => change.status === "A").reduce((sum, change) => {
7891
- const text6 = ctx.runtime.readText(toAbsolutePath(join12(ctx.paths.root, change.path)));
7892
- return sum + (text6.ok ? text6.value.split("\n").length : 0);
7893
- }, 0);
7894
8224
  var diffCheck = (_check, ctx) => {
7895
8225
  const { paths, diffBudget } = ctx.contract.scope;
7896
8226
  if (paths.length === 0 && diffBudget === void 0) return passed([note("gate.diff.no_scope")]);
7897
8227
  const outside = paths.length === 0 ? [] : ctx.changes.filter((change) => !matchesAny(change.path, paths));
7898
- const stat = numstat(ctx.git, ctx.base);
7899
8228
  const files = ctx.changes.length;
7900
- const lines3 = stat.lines + untrackedLines(ctx);
8229
+ const lines3 = diffShape(ctx.runtime, ctx.git, ctx.base, ctx.changes).lines;
7901
8230
  const notes2 = [
7902
8231
  ...outside.slice(0, MAX_LISTED).map((change) => note("gate.diff.outside", { file: change.path })),
7903
8232
  ...diffBudget?.files !== void 0 && files > diffBudget.files ? [note("gate.diff.too_many_files", { count: files, max: diffBudget.files })] : [],
@@ -7947,13 +8276,13 @@ var manualCheck = (check2, ctx) => {
7947
8276
  };
7948
8277
 
7949
8278
  // src/gate/checks/placeholders.ts
7950
- import { join as join13 } from "node:path";
8279
+ import { join as join15 } from "node:path";
7951
8280
  var MAX_LISTED2 = 10;
7952
8281
  var SKIP_FILES = /\.(md|mdx|txt|rst|lock|snap|svg|png|jpg|gif|json)$|(^|\/)(CHANGELOG|LICENSE)/i;
7953
8282
  var PLACEHOLDER = /\b(TODO|FIXME|XXX)\b|not implemented|NotImplementedError|unimplemented!|todo!\(|lorem ipsum/i;
7954
8283
  var findPlaceholders = (lines3) => lines3.filter((line2) => !isTestFile(line2.path) && !SKIP_FILES.test(line2.path) && PLACEHOLDER.test(line2.text));
7955
8284
  var untrackedAdded = (ctx) => ctx.changes.filter((change) => change.status === "A" && !SKIP_FILES.test(change.path)).flatMap((change) => {
7956
- const text6 = ctx.runtime.readText(toAbsolutePath(join13(ctx.paths.root, change.path)));
8285
+ const text6 = ctx.runtime.readText(toAbsolutePath(join15(ctx.paths.root, change.path)));
7957
8286
  return text6.ok ? text6.value.split("\n").map((line2, index) => ({ path: change.path, line: index + 1, text: line2 })) : [];
7958
8287
  });
7959
8288
  var placeholdersCheck = (_check, ctx) => {
@@ -8465,10 +8794,10 @@ var hit = (rule, units, index, weight) => {
8465
8794
  return { rule, unit: index, line: unit?.line ?? 0, weight, excerpt: unit?.text ?? "" };
8466
8795
  };
8467
8796
  var isProse = (unit) => unit?.kind === "sentence";
8468
- var samePara = (units, from, count3) => {
8797
+ var samePara = (units, from, count4) => {
8469
8798
  const first = units[from];
8470
8799
  if (!isProse(first)) return false;
8471
- return units.slice(from, from + count3).every((unit) => isProse(unit) && unit.paragraph === first.paragraph);
8800
+ return units.slice(from, from + count4).every((unit) => isProse(unit) && unit.paragraph === first.paragraph);
8472
8801
  };
8473
8802
  var runsOf = (units, size, matches) => {
8474
8803
  const starts = [];
@@ -8525,13 +8854,13 @@ var emoji = (units) => units.flatMap(
8525
8854
  var dashes = (units) => {
8526
8855
  const perParagraph = /* @__PURE__ */ new Map();
8527
8856
  for (const unit of units) {
8528
- const count3 = unit.text.split("—").length - 1;
8529
- perParagraph.set(unit.paragraph, (perParagraph.get(unit.paragraph) ?? 0) + count3);
8857
+ const count4 = unit.text.split("—").length - 1;
8858
+ perParagraph.set(unit.paragraph, (perParagraph.get(unit.paragraph) ?? 0) + count4);
8530
8859
  }
8531
- return [...perParagraph].flatMap(([paragraph, count3]) => {
8532
- if (count3 === 0) return [];
8860
+ return [...perParagraph].flatMap(([paragraph, count4]) => {
8861
+ if (count4 === 0) return [];
8533
8862
  const index = units.findIndex((unit) => unit.paragraph === paragraph && unit.text.includes("—"));
8534
- const weight = count3 >= 2 ? STRUCTURE_WEIGHTS.dashSeries : STRUCTURE_WEIGHTS.dash;
8863
+ const weight = count4 >= 2 ? STRUCTURE_WEIGHTS.dashSeries : STRUCTURE_WEIGHTS.dash;
8535
8864
  return [hit("em-dash-series", units, index, weight)];
8536
8865
  });
8537
8866
  };
@@ -8559,11 +8888,11 @@ var windowEnd = (units, from, size) => {
8559
8888
  return to;
8560
8889
  };
8561
8890
  var windowScore = (hits, window) => hits.filter((item) => item.unit >= window.from && item.unit <= window.to).reduce((sum, item) => sum + item.weight, 0);
8562
- var hotWindows = (hits, units, size, threshold) => {
8891
+ var hotWindows = (hits, units, size, threshold2) => {
8563
8892
  const windows = [];
8564
8893
  for (let from = 0; from < units.length; from += 1) {
8565
8894
  const window = { from, to: windowEnd(units, from, size) };
8566
- if (windowScore(hits, window) >= threshold) windows.push(window);
8895
+ if (windowScore(hits, window) >= threshold2) windows.push(window);
8567
8896
  }
8568
8897
  return windows;
8569
8898
  };
@@ -8590,10 +8919,10 @@ var byPosition = (left, right) => left.unit - right.unit || left.line - right.li
8590
8919
  var scoreProse = (text6, options = {}) => {
8591
8920
  const { units, allowed: allowed2 } = extractProse(text6);
8592
8921
  const size = options.window ?? DENSITY_WINDOW;
8593
- const threshold = options.threshold ?? DENSITY_THRESHOLD;
8922
+ const threshold2 = options.threshold ?? DENSITY_THRESHOLD;
8594
8923
  const catalog = options.context === "record" ? [...phraseRules(), ...recordRules()] : phraseRules();
8595
8924
  const hits = [...phraseHits(units, catalog), ...structureHits(units)].sort(byPosition);
8596
- const spans = mergeSpans(hits, hotWindows(hits, units, size, threshold));
8925
+ const spans = mergeSpans(hits, hotWindows(hits, units, size, threshold2));
8597
8926
  return { hits, spans, allowed: allowed2 };
8598
8927
  };
8599
8928
  var isFlagged = (report2) => report2.spans.length > 0;
@@ -8643,22 +8972,55 @@ var MAX_LISTED3 = 10;
8643
8972
  var rulesOf = (span) => [...new Set(span.hits.map((hit3) => hit3.rule))].join(", ");
8644
8973
  var proseOutcome = (status) => {
8645
8974
  if (!status.ok) return unverified("tool_missing", [note("gate.prose.unavailable")]);
8646
- const { files, flagged, blocking } = status.value;
8647
- const key = blocking ? "gate.prose.finding" : "gate.prose.warning";
8975
+ const { files, flagged, blocking: blocking2 } = status.value;
8976
+ const key = blocking2 ? "gate.prose.finding" : "gate.prose.warning";
8648
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) }));
8649
8978
  if (notes2.length === 0) return passed([note("gate.prose.clean", { count: files.length })]);
8650
- return blocking ? failed(notes2) : passed(notes2);
8979
+ return blocking2 ? failed(notes2) : passed(notes2);
8651
8980
  };
8652
8981
  var proseCheck = (_check, ctx) => proseOutcome(proseGateStatus(ctx.runtime, ctx.paths.root));
8653
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
+
8654
9016
  // src/secrets/baseline.ts
8655
- import { join as join14 } from "node:path";
9017
+ import { join as join16 } from "node:path";
8656
9018
  var BASELINE_FILE2 = ".finitemonkey/findings/baseline.json";
8657
9019
  var BASELINE_SCHEMA = "finitemonkey/secrets-baseline@1";
8658
9020
  var BASELINE_STATES = ["aceito", "falso-positivo"];
8659
9021
  var EXCEPTION_MAX_DAYS = 90;
8660
9022
  var EMPTY_BASELINE = { schema: BASELINE_SCHEMA, entries: [] };
8661
- var baselinePath = (root) => toAbsolutePath(join14(root, BASELINE_FILE2));
9023
+ var baselinePath = (root) => toAbsolutePath(join16(root, BASELINE_FILE2));
8662
9024
  var text5 = (record2, key) => {
8663
9025
  const value = record2[key];
8664
9026
  return typeof value === "string" && value !== "" ? value : void 0;
@@ -8718,14 +9080,14 @@ var withoutFixed = (baseline, fixed) => {
8718
9080
  var acceptedFingerprints = (baseline, now) => new Set(baseline.entries.filter((entry2) => entry2.expiresAt >= now).map((entry2) => entry2.fingerprint));
8719
9081
 
8720
9082
  // src/secrets/incidents.ts
8721
- import { join as join15 } from "node:path";
9083
+ import { join as join17 } from "node:path";
8722
9084
  var INCIDENTS_DIR = ".finitemonkey/incidents";
8723
9085
  var INCIDENT_STATES = ["aberto", "exposicao-local", "rotacionado", "fechado"];
8724
9086
  var INCIDENT_SOURCES = ["tool-output", "prompt", "history", "scan"];
8725
9087
  var ID = /^inc-[0-9a-f]{12}$/;
8726
9088
  var incidentId = (rule, fingerprint) => `inc-${sha256Hex(`${rule}\0${fingerprint}`).slice(0, 12)}`;
8727
- var incidentsDir = (root) => toAbsolutePath(join15(root, INCIDENTS_DIR));
8728
- var incidentFile = (root, id) => toAbsolutePath(join15(incidentsDir(root), `${id}.json`));
9089
+ var incidentsDir = (root) => toAbsolutePath(join17(root, INCIDENTS_DIR));
9090
+ var incidentFile = (root, id) => toAbsolutePath(join17(incidentsDir(root), `${id}.json`));
8729
9091
  var pick2 = (options, value) => options.find((option) => option === value);
8730
9092
  var str2 = (record2, key) => {
8731
9093
  const value = record2[key];
@@ -8780,7 +9142,7 @@ var listIncidents = (runtime, root) => {
8780
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));
8781
9143
  };
8782
9144
  var saveIncident = (runtime, root, incident) => {
8783
- const ignore = runtime.writeText(toAbsolutePath(join15(incidentsDir(root), ".gitignore")), "*\n");
9145
+ const ignore = runtime.writeText(toAbsolutePath(join17(incidentsDir(root), ".gitignore")), "*\n");
8784
9146
  if (!ignore.ok) return ignore;
8785
9147
  return runtime.writeText(incidentFile(root, incident.id), `${JSON.stringify(incident, null, 2)}
8786
9148
  `);
@@ -8827,7 +9189,7 @@ var moveIncident = (incident, to, now, by) => ({
8827
9189
  var isRotated = (incident) => incident.state === "rotacionado" || incident.state === "fechado";
8828
9190
 
8829
9191
  // src/secrets/scan.ts
8830
- import { join as join16, relative as relative4 } from "node:path";
9192
+ import { join as join18, relative as relative4 } from "node:path";
8831
9193
 
8832
9194
  // src/secrets/located.ts
8833
9195
  var findingId = (ruleId, file, fingerprint) => `fm-${sha256Hex(`${ruleId}\0${file}\0${fingerprint}`).slice(0, 16)}`;
@@ -8927,7 +9289,7 @@ var listed2 = (runtime, root, target) => {
8927
9289
  const base = toAbsolutePath(target, root);
8928
9290
  const walked = runtime.walkFiles(base);
8929
9291
  if (!walked.ok) return walked;
8930
- return accept(walked.value.map((name) => toPosixPath(relative4(root, join16(base, toPosixPath(name))))));
9292
+ return accept(walked.value.map((name) => toPosixPath(relative4(root, join18(base, toPosixPath(name))))));
8931
9293
  };
8932
9294
  var scanFile = (runtime, root, file) => {
8933
9295
  const text6 = runtime.readText(toAbsolutePath(file, root));
@@ -8987,15 +9349,17 @@ var secretsCheck = (_check, ctx) => {
8987
9349
  const status = secretsGateStatus(ctx.runtime, ctx.paths.root, sinceOf(ctx));
8988
9350
  if (!status.ok) return unverified("tool_missing", [note("gate.secrets.unavailable")]);
8989
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 }));
8990
9353
  const notes2 = [
8991
- ...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 })),
8992
9355
  ...openIncidents.slice(0, MAX_LISTED4).map((incident) => note("gate.secrets.incident", { id: incident.id, state: incident.state }))
8993
9356
  ];
8994
- return notes2.length > 0 ? failed(notes2) : passed([note("gate.secrets.clean")]);
9357
+ if (notes2.length > 0) return failed([...notes2, ...warnings]);
9358
+ return passed(warnings.length > 0 ? warnings : [note("gate.secrets.clean")]);
8995
9359
  };
8996
9360
 
8997
9361
  // src/gate/checks/tests.ts
8998
- import { join as join17 } from "node:path";
9362
+ import { join as join19 } from "node:path";
8999
9363
 
9000
9364
  // src/guards/tests/tree.ts
9001
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 }] : [];
@@ -9027,7 +9391,7 @@ var treeViolations = (ctx) => {
9027
9391
  changes: relevant,
9028
9392
  before: (path) => fileAt(ctx.git, ctx.base, path),
9029
9393
  after: (path) => {
9030
- const text6 = ctx.runtime.readText(toAbsolutePath(join17(ctx.paths.root, path)));
9394
+ const text6 = ctx.runtime.readText(toAbsolutePath(join19(ctx.paths.root, path)));
9031
9395
  return text6.ok ? text6.value : void 0;
9032
9396
  },
9033
9397
  acceptedConfigs: ctx.trust.configs,
@@ -9048,7 +9412,7 @@ var testsCheck = (check2, ctx) => {
9048
9412
  };
9049
9413
 
9050
9414
  // src/git/tree.ts
9051
- import { join as join18 } from "node:path";
9415
+ import { join as join20 } from "node:path";
9052
9416
 
9053
9417
  // src/secrets/guards/paths.ts
9054
9418
  var SAFE_ENV = /^\.env\.(?:example|sample|template|dist|defaults|schema)$/i;
@@ -9087,7 +9451,7 @@ var dropSensitive = (runtime, repo, env, options) => {
9087
9451
  return removed.ok ? accept(void 0) : removed;
9088
9452
  };
9089
9453
  var withTempIndex = (runtime, repo, options, work) => {
9090
- const path = toAbsolutePath(join18(repo.gitDir, `finitemonkey-${options.stamp}.index`));
9454
+ const path = toAbsolutePath(join20(repo.gitDir, `finitemonkey-${options.stamp}.index`));
9091
9455
  const env = { GIT_INDEX_FILE: path, GIT_OPTIONAL_LOCKS: "0" };
9092
9456
  try {
9093
9457
  return work(env);
@@ -9320,7 +9684,7 @@ var entryOf2 = (kind, xy, path) => ({
9320
9684
  staged: kind !== "untracked" && !xy.startsWith("."),
9321
9685
  unstaged: kind === "untracked" || xy.charAt(1) !== "."
9322
9686
  });
9323
- var afterFields = (record2, count3) => record2.split(" ").slice(count3).join(" ");
9687
+ var afterFields = (record2, count4) => record2.split(" ").slice(count4).join(" ");
9324
9688
  var parsePorcelainV2 = (stdout) => {
9325
9689
  const records = stdout.split("\0");
9326
9690
  const out = [];
@@ -9338,94 +9702,8 @@ var parsePorcelainV2 = (stdout) => {
9338
9702
  return out.filter((entry2) => entry2.path !== "" && !entry2.path.startsWith(STATE_PREFIX));
9339
9703
  };
9340
9704
 
9341
- // src/git/project-config.ts
9342
- import { join as join19 } from "node:path";
9343
- var CONFIG_FILE3 = ".finitemonkey/config.yml";
9344
- var unquote = (value) => value.trim().replace(/^(["'])(.*)\1$/s, "$2");
9345
- var stripComment2 = (line2) => {
9346
- let quote;
9347
- for (let index = 0; index < line2.length; index += 1) {
9348
- const char = line2.charAt(index);
9349
- if (quote === void 0 && (char === '"' || char === "'")) quote = char;
9350
- else if (char === quote) quote = void 0;
9351
- else if (quote === void 0 && char === "#" && (index === 0 || /\s/.test(line2.charAt(index - 1)))) {
9352
- return line2.slice(0, index);
9353
- }
9354
- }
9355
- return line2;
9356
- };
9357
- var flowList = (value) => {
9358
- const trimmed = value.trim();
9359
- if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return void 0;
9360
- return trimmed.slice(1, -1).split(",").map(unquote).filter((item) => item !== "");
9361
- };
9362
- var keyValue = (body2) => {
9363
- const match = /^("[^"]*"|'[^']*'|[^:]+?)\s*:(?:\s+(.*))?$/.exec(body2);
9364
- if (match === null) return void 0;
9365
- return { key: unquote(match[1] ?? ""), value: match[2] ?? "" };
9366
- };
9367
- var addItem = (frame, item) => {
9368
- if (frame.key === void 0) return;
9369
- const parent = frame.tree[frame.key];
9370
- if (Array.isArray(parent)) parent.push(item);
9371
- else frame.tree[frame.key] = [item];
9372
- };
9373
- var placeLine = (stack, indent, body2) => {
9374
- while (stack.length > 1 && (stack[stack.length - 1]?.indent ?? 0) >= indent && !body2.startsWith("- ")) stack.pop();
9375
- const top = stack[stack.length - 1];
9376
- if (top === void 0) return;
9377
- if (body2.startsWith("- ")) {
9378
- addItem(top, unquote(body2.slice(2)));
9379
- return;
9380
- }
9381
- const pair = keyValue(body2);
9382
- if (pair === void 0) return;
9383
- const parentTree = top.key === void 0 ? top.tree : ensureTree(top.tree, top.key);
9384
- if (pair.value === "") {
9385
- stack.push({ indent, tree: parentTree, key: pair.key });
9386
- return;
9387
- }
9388
- parentTree[pair.key] = flowList(pair.value) ?? unquote(pair.value);
9389
- };
9390
- var ensureTree = (tree, key) => {
9391
- const existing = tree[key];
9392
- if (existing !== void 0 && typeof existing !== "string" && !Array.isArray(existing)) return existing;
9393
- const fresh3 = {};
9394
- tree[key] = fresh3;
9395
- return fresh3;
9396
- };
9397
- var parseConfigYaml = (text6) => {
9398
- const root = {};
9399
- const stack = [{ indent: -1, tree: root, key: void 0 }];
9400
- for (const raw of text6.split(/\r?\n/)) {
9401
- const line2 = stripComment2(raw).trimEnd();
9402
- if (line2.trim() === "" || line2.trim() === "---") continue;
9403
- const indent = line2.length - line2.trimStart().length;
9404
- placeLine(stack, indent, line2.trim());
9405
- }
9406
- return root;
9407
- };
9408
- var isTree = (node) => typeof node === "object" && !Array.isArray(node);
9409
- var configAt = (tree, ...path) => path.reduce((node, key) => isTree(node) ? node[key] : void 0, tree);
9410
- var configList = (tree, ...path) => {
9411
- const node = configAt(tree, ...path);
9412
- if (typeof node === "string") return node === "" ? [] : [node];
9413
- return Array.isArray(node) ? node.filter((item) => typeof item === "string") : [];
9414
- };
9415
- var configMap = (tree, ...path) => {
9416
- const node = configAt(tree, ...path);
9417
- if (!isTree(node)) return {};
9418
- return Object.fromEntries(
9419
- Object.entries(node).filter((entry2) => typeof entry2[1] === "string")
9420
- );
9421
- };
9422
- var loadProjectConfig = (runtime, root) => {
9423
- const text6 = runtime.readText(toAbsolutePath(join19(root, CONFIG_FILE3)));
9424
- return text6.ok ? parseConfigYaml(text6.value) : {};
9425
- };
9426
-
9427
9705
  // src/git/merge/dropped.ts
9428
- import { join as join20 } from "node:path";
9706
+ import { join as join21 } from "node:path";
9429
9707
 
9430
9708
  // src/git/merge/lines.ts
9431
9709
  var normalizeLine = (line2) => line2.replace(/\r$/, "").trim().replace(/\s*,$/, "");
@@ -9505,7 +9783,7 @@ var parseDropped = (text6) => {
9505
9783
  return accept({ items, invalid });
9506
9784
  };
9507
9785
  var loadDropped = (runtime, root, file) => {
9508
- const path = toAbsolutePath(file ?? join20(root, DROPPED_FILE), root);
9786
+ const path = toAbsolutePath(file ?? join21(root, DROPPED_FILE), root);
9509
9787
  const text6 = runtime.readText(path);
9510
9788
  if (!text6.ok) return file === void 0 ? accept({ items: [], invalid: [] }) : reject(`${file}: ${text6.error}`);
9511
9789
  return parseDropped(text6.value);
@@ -9515,7 +9793,7 @@ var isDropped = (finding3, items) => items.some(
9515
9793
  );
9516
9794
 
9517
9795
  // src/git/merge/generated.ts
9518
- import { join as join21 } from "node:path";
9796
+ import { join as join22 } from "node:path";
9519
9797
  var LOCKFILES3 = /* @__PURE__ */ new Set([
9520
9798
  "package-lock.json",
9521
9799
  "npm-shrinkwrap.json",
@@ -9578,7 +9856,7 @@ var verifyGenerated = (runtime, repo, inputs, paths, generators) => {
9578
9856
  if (declared.length === 0) return accept([]);
9579
9857
  const commit2 = resultCommit(runtime, repo, inputs);
9580
9858
  if (!commit2.ok) return commit2;
9581
- const dir = join21(repo.gitDir, `finitemonkey-generate-${runtime.now().replace(/[:.]/g, "-")}`);
9859
+ const dir = join22(repo.gitDir, `finitemonkey-generate-${runtime.now().replace(/[:.]/g, "-")}`);
9582
9860
  if (!runtime.git(["worktree", "add", "--detach", dir, commit2.value], repo.root).ok)
9583
9861
  return reject("could not check out the merge result");
9584
9862
  try {
@@ -9588,7 +9866,7 @@ var verifyGenerated = (runtime, repo, inputs, paths, generators) => {
9588
9866
  }
9589
9867
  return accept(
9590
9868
  declared.flatMap(({ path }) => {
9591
- const fresh3 = runtime.readText(toAbsolutePath(join21(dir, path)));
9869
+ const fresh3 = runtime.readText(toAbsolutePath(join22(dir, path)));
9592
9870
  const merged = blobText(runtime, repo, commit2.value, path);
9593
9871
  if (fresh3.ok && fresh3.value === merged) return [];
9594
9872
  return [{ kind: "generated-differs", file: path, side: void 0, line: void 0, text: path, weak: false }];
@@ -9874,19 +10152,19 @@ var verifyMerge = (runtime, repo, options = {}) => {
9874
10152
  };
9875
10153
 
9876
10154
  // src/git/ownership.ts
9877
- import { join as join22 } from "node:path";
10155
+ import { join as join23 } from "node:path";
9878
10156
  var OWNERSHIP_DIR = ".finitemonkey/git";
9879
10157
  var ownerKey = (raw) => {
9880
10158
  const cleaned = (raw ?? "").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80);
9881
10159
  return cleaned === "" ? "unknown" : cleaned;
9882
10160
  };
9883
- var fileOf = (repo, kind, owner) => toAbsolutePath(join22(repo.root, OWNERSHIP_DIR, kind, `${ownerKey(owner)}.json`));
10161
+ var fileOf = (repo, kind, owner) => toAbsolutePath(join23(repo.root, OWNERSHIP_DIR, kind, `${ownerKey(owner)}.json`));
9884
10162
  var readDirty = (runtime, root) => {
9885
10163
  const status = runtime.git(["status", "--porcelain=v2", "-z", "--untracked-files=all"], root);
9886
10164
  return status.ok ? parsePorcelainV2(status.stdout) : void 0;
9887
10165
  };
9888
10166
  var hashDirty = (runtime, repo, entries) => {
9889
- const present2 = entries.filter((entry2) => runtime.stat(toAbsolutePath(join22(repo.root, entry2.path)))?.kind === "file");
10167
+ const present2 = entries.filter((entry2) => runtime.stat(toAbsolutePath(join23(repo.root, entry2.path)))?.kind === "file");
9890
10168
  const hashed = present2.length === 0 ? void 0 : runtime.gitWith(["hash-object", "--stdin-paths"], repo.root, {
9891
10169
  input: `${present2.map((entry2) => entry2.path).join("\n")}
9892
10170
  `
@@ -10138,7 +10416,8 @@ var DEFAULT_CHECKS = {
10138
10416
  secrets: secretsCheck,
10139
10417
  prose: proseCheck,
10140
10418
  worktree: worktreeCheck,
10141
- merge: mergeCheck
10419
+ merge: mergeCheck,
10420
+ review: reviewCheck
10142
10421
  };
10143
10422
 
10144
10423
  // src/gate/run.ts
@@ -10318,10 +10597,21 @@ var isOwnWorktree = (input) => {
10318
10597
  const main2 = findRepoRoot(runtime, input.cwd);
10319
10598
  return front !== void 0 && main2 !== void 0 && front !== main2;
10320
10599
  };
10321
- var subagentStopGate = (input) => isOwnWorktree(input) ? stopGate(input) : { kind: "allow" };
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
+ };
10322
10612
 
10323
10613
  // src/gate/hooks/subagent.ts
10324
- import { join as join23 } from "node:path";
10614
+ import { join as join24 } from "node:path";
10325
10615
  var NO_CONTRACT_AGENTS = /(^|:)(reviewer|verifier|explore|plan|claude-code-guide|statusline-setup)$/i;
10326
10616
  var KNOWN_KINDS = /* @__PURE__ */ new Set([
10327
10617
  "command",
@@ -10334,7 +10624,8 @@ var KNOWN_KINDS = /* @__PURE__ */ new Set([
10334
10624
  "secrets",
10335
10625
  "prose",
10336
10626
  "worktree",
10337
- "merge"
10627
+ "merge",
10628
+ "review"
10338
10629
  ]);
10339
10630
  var requirementText = (t, check2) => {
10340
10631
  const key = `gate.require.${check2.kind}`;
@@ -10355,12 +10646,12 @@ var subagentStart = (input) => {
10355
10646
  const active = readState(runtime, paths).active;
10356
10647
  if (active === void 0) return { kind: "allow" };
10357
10648
  const disk = (relative9) => {
10358
- const text6 = runtime.readText(toAbsolutePath(join23(paths.root, relative9)));
10649
+ const text6 = runtime.readText(toAbsolutePath(join24(paths.root, relative9)));
10359
10650
  return text6.ok ? text6.value : void 0;
10360
10651
  };
10361
10652
  const loaded = loadTask(runtime, paths, { disk, base: () => void 0 }, active);
10362
10653
  if (!loaded.ok) return { kind: "allow" };
10363
- const briefing = runtime.stat(toAbsolutePath(join23(paths.dir, "briefing.md"))) !== void 0;
10654
+ const briefing = runtime.stat(toAbsolutePath(join24(paths.dir, "briefing.md"))) !== void 0;
10364
10655
  return { kind: "allow", output: contextOutput("SubagentStart", contractFacts(t, loaded.value.contract, briefing)) };
10365
10656
  };
10366
10657
 
@@ -10377,7 +10668,7 @@ var PORTAO_HANDLERS = {
10377
10668
  };
10378
10669
 
10379
10670
  // src/prod/guard.ts
10380
- import { join as join25 } from "node:path";
10671
+ import { join as join26 } from "node:path";
10381
10672
 
10382
10673
  // src/git/command-text.ts
10383
10674
  var MAX_SHOWN = 160;
@@ -10719,7 +11010,7 @@ var remoteHostsOf = (command) => {
10719
11010
  };
10720
11011
 
10721
11012
  // src/prod/targets.ts
10722
- import { join as join24 } from "node:path";
11013
+ import { join as join25 } from "node:path";
10723
11014
  var DEPLOY_FILES = [
10724
11015
  "deploy.sh",
10725
11016
  "scripts/deploy.sh",
@@ -10743,12 +11034,12 @@ var hostsInText = (text6) => {
10743
11034
  return [...urls, ...keyed, ...commands].filter((host) => host !== "" && !host.includes("$"));
10744
11035
  };
10745
11036
  var workflowFiles = (runtime, root) => {
10746
- const listed4 = runtime.listDir(toAbsolutePath(join24(root, ".github", "workflows")));
11037
+ const listed4 = runtime.listDir(toAbsolutePath(join25(root, ".github", "workflows")));
10747
11038
  if (!listed4.ok) return [];
10748
11039
  return listed4.value.filter((name) => /deploy/i.test(name) && /\.ya?ml$/i.test(name)).map((name) => `.github/workflows/${name}`);
10749
11040
  };
10750
11041
  var detectedHosts = (runtime, root, files) => files.flatMap((file) => {
10751
- const text6 = runtime.readText(toAbsolutePath(join24(root, file)));
11042
+ const text6 = runtime.readText(toAbsolutePath(join25(root, file)));
10752
11043
  return text6.ok ? hostsInText(text6.value) : [];
10753
11044
  });
10754
11045
  var loadProdTargets = (runtime, root, config) => {
@@ -10760,7 +11051,7 @@ var loadProdTargets = (runtime, root, config) => {
10760
11051
  ...detectedHosts(runtime, root, deployFiles)
10761
11052
  ];
10762
11053
  const declaredDeploy = configAt(config, "prod", "deploy");
10763
- const existing = deployFiles.find((file) => runtime.stat(toAbsolutePath(join24(root, file))) !== void 0);
11054
+ const existing = deployFiles.find((file) => runtime.stat(toAbsolutePath(join25(root, file))) !== void 0);
10764
11055
  return {
10765
11056
  hosts: new Set(hosts.map(hostKey).filter((host) => host !== "")),
10766
11057
  kubeContexts: configList(config, "prod", "kube_contexts"),
@@ -10937,8 +11228,8 @@ var recordEvidence = (runtime, root, owner, reads) => {
10937
11228
  ] : []
10938
11229
  );
10939
11230
  if (lines3.length === 0) return;
10940
- const path = toAbsolutePath(join25(root, EVIDENCE_FILE));
10941
- runtime.mkdir(toAbsolutePath(join25(root, ".finitemonkey")));
11231
+ const path = toAbsolutePath(join26(root, EVIDENCE_FILE));
11232
+ runtime.mkdir(toAbsolutePath(join26(root, ".finitemonkey")));
10942
11233
  const before = runtime.readText(path);
10943
11234
  const text6 = before.ok ? before.value : "";
10944
11235
  runtime.writeAtomic(path, `${text6}${text6 === "" || text6.endsWith("\n") ? "" : "\n"}${lines3.join("\n")}
@@ -11750,10 +12041,10 @@ var listWorktrees = (runtime, cwd) => {
11750
12041
  };
11751
12042
 
11752
12043
  // src/git/journal.ts
11753
- import { join as join26 } from "node:path";
12044
+ import { join as join27 } from "node:path";
11754
12045
  var JOURNAL_FILE = ".finitemonkey/git-journal.jsonl";
11755
12046
  var JOURNAL_SCHEMA = "finitemonkey/git-journal@1";
11756
- var journalPath = (root) => toAbsolutePath(join26(root, JOURNAL_FILE));
12047
+ var journalPath = (root) => toAbsolutePath(join27(root, JOURNAL_FILE));
11757
12048
  var headsOf = (value) => isRecord(value) ? Object.fromEntries(
11758
12049
  Object.entries(value).filter((entry2) => typeof entry2[1] === "string")
11759
12050
  ) : {};
@@ -11789,7 +12080,7 @@ var readJournal = (runtime, root) => {
11789
12080
  };
11790
12081
  var appendJournal = (runtime, root, entry2) => {
11791
12082
  const path = journalPath(root);
11792
- const made = runtime.mkdir(toAbsolutePath(join26(root, ".finitemonkey")));
12083
+ const made = runtime.mkdir(toAbsolutePath(join27(root, ".finitemonkey")));
11793
12084
  if (!made.ok) return made;
11794
12085
  const previous = runtime.readText(path);
11795
12086
  const before = previous.ok ? previous.value : "";
@@ -12291,10 +12582,10 @@ var textOf2 = (input, found) => {
12291
12582
  }
12292
12583
  return accept(parts.join("\n\n"));
12293
12584
  };
12294
- var decisionFor = (input, found, blocking) => {
12585
+ var decisionFor = (input, found, blocking2) => {
12295
12586
  const text6 = textOf2(input, found);
12296
12587
  if (!text6.ok) {
12297
- return blocking ? { kind: "block", reason: input.t("prose.block.unreadable", { ...text6.error }) } : void 0;
12588
+ return blocking2 ? { kind: "block", reason: input.t("prose.block.unreadable", { ...text6.error }) } : void 0;
12298
12589
  }
12299
12590
  const report2 = scoreProse(text6.value, { context: "record" });
12300
12591
  if (report2.allowed || report2.spans.length === 0) return void 0;
@@ -12303,7 +12594,7 @@ var decisionFor = (input, found, blocking) => {
12303
12594
  spans: describeSpans(input.t, report2.spans),
12304
12595
  fixes: describeFixes(input.t, report2.spans)
12305
12596
  };
12306
- if (blocking) return { kind: "block", reason: input.t("prose.block.command", params2) };
12597
+ if (blocking2) return { kind: "block", reason: input.t("prose.block.command", params2) };
12307
12598
  const additionalContext = `finitemonkey: ${input.t("prose.warn.command", params2)}`;
12308
12599
  return { kind: "allow", output: { hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext } } };
12309
12600
  };
@@ -12311,9 +12602,9 @@ var proseCommandGuard = (input) => {
12311
12602
  if (!SHELLS3.has(toolName2(input.payload))) return ALLOW4;
12312
12603
  const found = proseInCommand(optionalString(toolInput2(input.payload), "command") ?? "");
12313
12604
  if (found.length === 0) return ALLOW4;
12314
- const blocking = projectPreset(input.runtime, projectRoot(input)) === "gabriel";
12605
+ const blocking2 = projectPreset(input.runtime, projectRoot(input)) === "gabriel";
12315
12606
  for (const item of found) {
12316
- const decision = decisionFor(input, item, blocking);
12607
+ const decision = decisionFor(input, item, blocking2);
12317
12608
  if (decision !== void 0) return decision;
12318
12609
  }
12319
12610
  return ALLOW4;
@@ -12477,11 +12768,24 @@ var relativeFile = (cwd, file) => {
12477
12768
  const rel = relative7(cwd, file).split("\\").join("/");
12478
12769
  return rel.startsWith("..") || rel === "" ? file : rel;
12479
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
+ };
12480
12776
  var writeDecision = (input, tool) => {
12481
12777
  const { t } = input;
12778
+ const warnings = [];
12482
12779
  for (const written of writtenTexts(tool, toolInput2(input.payload))) {
12483
12780
  const file = relativeFile(input.cwd, written.file);
12484
- const found = scanText(written.text, { path: file });
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);
12485
12789
  if (found.length === 0) continue;
12486
12790
  const items = found.map((finding3) => ({
12487
12791
  ruleId: finding3.ruleId,
@@ -12489,7 +12793,7 @@ var writeDecision = (input, tool) => {
12489
12793
  }));
12490
12794
  return { kind: "block", reason: t("secrets.block.write", { file, found: describeFindings(t, items) }) };
12491
12795
  }
12492
- return ALLOW5;
12796
+ return warningDecision(t, warnings);
12493
12797
  };
12494
12798
  var bashMessage = (t, finding3) => {
12495
12799
  switch (finding3.kind) {
@@ -12503,8 +12807,9 @@ var bashMessage = (t, finding3) => {
12503
12807
  };
12504
12808
  var commitDecision = (input, includeUnstaged) => {
12505
12809
  const staged = scanStaged(input.runtime, input.cwd, includeUnstaged);
12506
- if (!staged.ok || staged.value.length === 0) return ALLOW5;
12507
- const items = staged.value.map((finding3) => ({
12810
+ const found = staged.ok ? blocking(staged.value) : [];
12811
+ if (found.length === 0) return ALLOW5;
12812
+ const items = found.map((finding3) => ({
12508
12813
  ruleId: finding3.ruleId,
12509
12814
  where: input.t("secrets.where.file", { file: finding3.file, line: finding3.line, column: finding3.column })
12510
12815
  }));
@@ -12516,7 +12821,7 @@ var bashDecision = (input) => {
12516
12821
  const segments = segmentsOf(command);
12517
12822
  const [first] = bashFindings(command, segments);
12518
12823
  if (first !== void 0) return { kind: "block", reason: bashMessage(input.t, first) };
12519
- const found = scanText(command);
12824
+ const found = blocking(scanText(command));
12520
12825
  if (found.length > 0) {
12521
12826
  const items = found.map((finding3) => ({ ruleId: finding3.ruleId, where: lineWhere(input.t, finding3) }));
12522
12827
  return { kind: "block", reason: input.t("secrets.block.bash", { found: describeFindings(input.t, items) }) };
@@ -12541,6 +12846,44 @@ var secretsPreToolUse = (input) => {
12541
12846
  return ALLOW5;
12542
12847
  };
12543
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
+
12544
12887
  // src/hooks/handlers.ts
12545
12888
  var allow = () => ({ kind: "allow" });
12546
12889
  var DECISION_RANK = { allow: 0, defer: 0, ask: 1, deny: 2 };
@@ -12594,7 +12937,8 @@ var mergeHandlers = (...sets) => {
12594
12937
  var SECRETS_HANDLERS = {
12595
12938
  PreToolUse: [secretsPreToolUse],
12596
12939
  UserPromptSubmit: [secretsUserPromptSubmit],
12597
- PostToolUse: [secretsPostToolUse]
12940
+ PostToolUse: [secretsPostToolUse],
12941
+ SessionStart: [secretsSessionStart]
12598
12942
  };
12599
12943
  var DEFAULT_HANDLERS = mergeHandlers(
12600
12944
  SECRETS_HANDLERS,
@@ -13012,7 +13356,7 @@ var hookSchemaProblems = (hooks, claude) => {
13012
13356
  };
13013
13357
 
13014
13358
  // src/doctor/hook-sources.ts
13015
- import { join as join27 } from "node:path";
13359
+ import { join as join28 } from "node:path";
13016
13360
  var entriesOf = (source, hooks, own) => Object.entries(asRecord(hooks)).flatMap(
13017
13361
  ([event, groups]) => asList(groups).flatMap((group) => {
13018
13362
  const record2 = asRecord(group);
@@ -13025,7 +13369,7 @@ var projectHookFiles = (env) => readSettings(env).flatMap(({ scope: scope2, path
13025
13369
  const source = PROJECT_SCOPES[scope2];
13026
13370
  return source === void 0 || !("hooks" in settings) ? [] : [{ source, path, hooks: settings["hooks"] }];
13027
13371
  });
13028
- var pluginsDir = ({ env, runtime }) => join27(env["CLAUDE_CONFIG_DIR"] ?? join27(runtime.homeDir(), ".claude"), "plugins");
13372
+ var pluginsDir = ({ env, runtime }) => join28(env["CLAUDE_CONFIG_DIR"] ?? join28(runtime.homeDir(), ".claude"), "plugins");
13029
13373
  var installRecord = (value, root) => {
13030
13374
  const records = Array.isArray(value) ? value.filter(isRecord) : isRecord(value) ? [value] : [];
13031
13375
  return records.find((record2) => record2["projectPath"] === root) ?? records[0];
@@ -13036,13 +13380,13 @@ var readJson = (env, path) => {
13036
13380
  return parsed?.ok === true ? parsed.value : void 0;
13037
13381
  };
13038
13382
  var installedPlugin = (env) => {
13039
- const index = readJson(env, toAbsolutePath(join27(pluginsDir(env), "installed_plugins.json")));
13383
+ const index = readJson(env, toAbsolutePath(join28(pluginsDir(env), "installed_plugins.json")));
13040
13384
  const record2 = installRecord(asRecord(index?.["plugins"])[PLUGIN_ID], env.root);
13041
13385
  const installPath = record2 === void 0 ? void 0 : optionalString(record2, "installPath");
13042
13386
  if (record2 === void 0 || installPath === void 0) return void 0;
13043
13387
  const root = toAbsolutePath(installPath);
13044
- const manifest = readJson(env, toAbsolutePath(join27(root, ".claude-plugin", "plugin.json")));
13045
- const hooksPath = toAbsolutePath(join27(root, "hooks", "hooks.json"));
13388
+ const manifest = readJson(env, toAbsolutePath(join28(root, ".claude-plugin", "plugin.json")));
13389
+ const hooksPath = toAbsolutePath(join28(root, "hooks", "hooks.json"));
13046
13390
  const text6 = env.runtime.readText(hooksPath);
13047
13391
  const parsed = text6.ok ? parseJsonObject(text6.value) : void 0;
13048
13392
  return {
@@ -13052,10 +13396,10 @@ var installedPlugin = (env) => {
13052
13396
  hooksText: text6.ok ? text6.value : void 0
13053
13397
  };
13054
13398
  };
13055
- var PRECEDENCE = ["managed", "local", "project", "user"];
13399
+ var PRECEDENCE2 = ["managed", "local", "project", "user"];
13056
13400
  var pluginEnabled = (env) => {
13057
13401
  const all = readSettings(env);
13058
- for (const scope2 of PRECEDENCE) {
13402
+ for (const scope2 of PRECEDENCE2) {
13059
13403
  const value = asRecord(all.find((item) => item.scope === scope2)?.settings["enabledPlugins"])[PLUGIN_ID];
13060
13404
  if (typeof value === "boolean") return { value, scope: scope2 };
13061
13405
  }
@@ -13087,7 +13431,7 @@ var schemaFindings = (env, plugin, claude) => {
13087
13431
  const files = [...projectHookFiles(env), ...plugin?.hooksFile === void 0 ? [] : [plugin.hooksFile]];
13088
13432
  const invalidJson = [
13089
13433
  ...unreadableProjectFiles(env),
13090
- ...plugin?.hooksText !== void 0 && plugin.hooksFile === void 0 ? [toAbsolutePath(join28(plugin.root, "hooks", "hooks.json"))] : []
13434
+ ...plugin?.hooksText !== void 0 && plugin.hooksFile === void 0 ? [toAbsolutePath(join29(plugin.root, "hooks", "hooks.json"))] : []
13091
13435
  ];
13092
13436
  return [
13093
13437
  ...invalidJson.flatMap((file) => schemaFinding(file, ["not valid JSON"], label2)),
@@ -13108,7 +13452,7 @@ var pluginFindings = (env, projectOwn) => {
13108
13452
  ];
13109
13453
  };
13110
13454
  var coverageFindings = (env, entries) => {
13111
- const installed = env.runtime.stat(toAbsolutePath(join28(env.root, ...LOCK_FILE.split("/")))) !== void 0;
13455
+ const installed = env.runtime.stat(toAbsolutePath(join29(env.root, ...LOCK_FILE.split("/")))) !== void 0;
13112
13456
  if (entries.length === 0 && !installed) {
13113
13457
  return [finding("hooks.registered", "warn", "doctor.hooks.none", {}, "doctor.hooks.fix_none")];
13114
13458
  }
@@ -13160,17 +13504,17 @@ var checkHooks = (env) => {
13160
13504
  };
13161
13505
 
13162
13506
  // src/doctor/instructions.ts
13163
- import { join as join29 } from "node:path";
13507
+ import { join as join30 } from "node:path";
13164
13508
  var INSTRUCTION_LINE_CEILING = 200;
13165
13509
  var TOP_INSTRUCTIONS = ["AGENTS.md", "CLAUDE.md", ".claude/CLAUDE.md", "CLAUDE.local.md"];
13166
13510
  var RULES_DIR = ".claude/rules";
13167
13511
  var SHOWN = 3;
13168
13512
  var readProjectFile = (env, relative9) => {
13169
- const text6 = env.runtime.readText(toAbsolutePath(join29(env.root, ...relative9.split("/"))));
13513
+ const text6 = env.runtime.readText(toAbsolutePath(join30(env.root, ...relative9.split("/"))));
13170
13514
  return text6.ok ? text6.value : void 0;
13171
13515
  };
13172
13516
  var instructionFiles = (env) => {
13173
- const listed4 = env.runtime.listDir(toAbsolutePath(join29(env.root, ...RULES_DIR.split("/"))));
13517
+ const listed4 = env.runtime.listDir(toAbsolutePath(join30(env.root, ...RULES_DIR.split("/"))));
13174
13518
  const rules = listed4.ok ? listed4.value.filter((name) => name.endsWith(".md")).map((name) => `${RULES_DIR}/${name}`) : [];
13175
13519
  return [...TOP_INSTRUCTIONS, ...rules].flatMap((path) => {
13176
13520
  const text6 = readProjectFile(env, path);
@@ -13275,12 +13619,12 @@ var checkInstructions = (env) => {
13275
13619
  };
13276
13620
 
13277
13621
  // src/doctor/line-endings.ts
13278
- import { join as join30 } from "node:path";
13622
+ import { join as join31 } from "node:path";
13279
13623
  var LF_RULE = /^\*\s.*\beol=lf\b/m;
13280
13624
  var checkLineEndings = ({ root, runtime }) => {
13281
13625
  const autocrlf = runtime.git(["config", "--get", "core.autocrlf"], root);
13282
13626
  if (!autocrlf.ok || autocrlf.stdout.trim().toLowerCase() !== "true") return [];
13283
- const attributes = runtime.readText(toAbsolutePath(join30(root, ".gitattributes")));
13627
+ const attributes = runtime.readText(toAbsolutePath(join31(root, ".gitattributes")));
13284
13628
  if (attributes.ok && LF_RULE.test(attributes.value)) return [];
13285
13629
  return [
13286
13630
  finding("git.autocrlf", "warn", "doctor.git.autocrlf", { file: ".gitattributes" }, "doctor.git.fix_autocrlf")
@@ -13288,7 +13632,7 @@ var checkLineEndings = ({ root, runtime }) => {
13288
13632
  };
13289
13633
 
13290
13634
  // src/doctor/memory.ts
13291
- import { join as join31 } from "node:path";
13635
+ import { join as join32 } from "node:path";
13292
13636
  var ceilingOf = (config) => {
13293
13637
  const value = configAt(parseConfigYaml(config ?? ""), "memory", "maxLines");
13294
13638
  const parsed = typeof value === "string" ? Number.parseInt(value, 10) : Number.NaN;
@@ -13296,9 +13640,9 @@ var ceilingOf = (config) => {
13296
13640
  };
13297
13641
  var lineCount2 = (text6) => text6.replace(/\r?\n$/, "").split(/\r?\n/).length;
13298
13642
  var checkAgentsFile = ({ root, runtime }) => {
13299
- const text6 = runtime.readText(toAbsolutePath(join31(root, AGENTS_FILE)));
13643
+ const text6 = runtime.readText(toAbsolutePath(join32(root, AGENTS_FILE)));
13300
13644
  if (!text6.ok) return [];
13301
- const config = runtime.readText(toAbsolutePath(join31(root, CONFIG_FILE)));
13645
+ const config = runtime.readText(toAbsolutePath(join32(root, CONFIG_FILE)));
13302
13646
  const max = ceilingOf(config.ok ? config.value : void 0);
13303
13647
  const lines3 = lineCount2(text6.value);
13304
13648
  const params2 = { file: AGENTS_FILE, lines: lines3, max };
@@ -13306,7 +13650,7 @@ var checkAgentsFile = ({ root, runtime }) => {
13306
13650
  };
13307
13651
 
13308
13652
  // src/doctor/project-files.ts
13309
- import { join as join32 } from "node:path";
13653
+ import { join as join33 } from "node:path";
13310
13654
  var variants = (base, extensions) => extensions.map((extension) => `${base}${extension}`);
13311
13655
  var SCRIPT = [".js", ".cjs", ".mjs", ".ts", ".cts", ".mts"];
13312
13656
  var DATA = ["", ".json", ".yml", ".yaml"];
@@ -13337,7 +13681,7 @@ var CONFIG_GROUPS = [
13337
13681
  files: [...variants("commitlint.config", SCRIPT), ...variants(".commitlintrc", [...DATA, ".js"])]
13338
13682
  }
13339
13683
  ];
13340
- var present = (env, files) => files.filter((file) => env.runtime.stat(toAbsolutePath(join32(env.root, file)))?.kind === "file");
13684
+ var present = (env, files) => files.filter((file) => env.runtime.stat(toAbsolutePath(join33(env.root, file)))?.kind === "file");
13341
13685
  var crowded = (env, groups, build) => groups.flatMap(({ name, files }) => {
13342
13686
  const found = present(env, files);
13343
13687
  return found.length > 1 ? [build(name, found.join(", "))] : [];
@@ -13361,6 +13705,22 @@ var checkProjectFiles = (env) => [
13361
13705
  )
13362
13706
  ];
13363
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
+
13364
13724
  // src/doctor/run.ts
13365
13725
  var DOCTOR_SCHEMA = "finitemonkey/doctor@1";
13366
13726
  var CHECKS = [
@@ -13376,6 +13736,7 @@ var CHECKS = [
13376
13736
  checkInstructions,
13377
13737
  checkProjectFiles,
13378
13738
  checkLineEndings,
13739
+ checkRemoteCredentials,
13379
13740
  checkAgentsFile,
13380
13741
  checkHooks
13381
13742
  ];
@@ -13403,7 +13764,7 @@ var toItem = (ctx, finding3) => ({
13403
13764
  detail: ctx.t(finding3.messageKey, finding3.params),
13404
13765
  ...finding3.fixKey === void 0 ? {} : { fix: ctx.t(finding3.fixKey, finding3.params) }
13405
13766
  });
13406
- var count2 = (items, status) => items.filter((item) => item.status === status).length;
13767
+ var count3 = (items, status) => items.filter((item) => item.status === status).length;
13407
13768
  var hookItems = (ctx, findings) => findings.flatMap(
13408
13769
  (finding3) => finding3.hook === void 0 ? [] : [
13409
13770
  {
@@ -13419,7 +13780,7 @@ var buildReport2 = (ctx, findings) => {
13419
13780
  schema: DOCTOR_SCHEMA,
13420
13781
  checks,
13421
13782
  hooks: hookItems(ctx, findings),
13422
- summary: { ok: count2(checks, "ok"), warn: count2(checks, "warn"), fail: count2(checks, "fail") }
13783
+ summary: { ok: count3(checks, "ok"), warn: count3(checks, "warn"), fail: count3(checks, "fail") }
13423
13784
  };
13424
13785
  };
13425
13786
 
@@ -13736,7 +14097,7 @@ var resultOf = (ctx, checked) => {
13736
14097
  const problem4 = gitProblem("merge_unanalyzable", "git.error.merge_unanalyzable", detail, "git status");
13737
14098
  return failureFrom(ctx.t, { ...problem4, exitCode: ExitCode.Usage, data }, human2);
13738
14099
  }
13739
- const lost = checked.reduce((count3, item) => count3 + item.report.findings.length, 0);
14100
+ const lost = checked.reduce((count4, item) => count4 + item.report.findings.length, 0);
13740
14101
  if (lost === 0) return success(data, human2);
13741
14102
  return failureFrom(
13742
14103
  ctx.t,
@@ -13903,7 +14264,7 @@ var rescueCommand = {
13903
14264
  };
13904
14265
 
13905
14266
  // src/git/undo.ts
13906
- import { basename as basename3, dirname as dirname8, join as join33 } from "node:path";
14267
+ import { basename as basename3, dirname as dirname8, join as join34 } from "node:path";
13907
14268
  var INDEX_SUBJECT = "finitemonkey index:";
13908
14269
  var gitLine = (runtime, repo, args) => {
13909
14270
  const result = runtime.git(args, repo.root);
@@ -13946,7 +14307,7 @@ var findSnapshot = (views, wanted) => {
13946
14307
  };
13947
14308
  var stampOf2 = (iso) => iso.replace(/[:.]/g, "-");
13948
14309
  var worktreePath = (runtime, repo) => {
13949
- const base = join33(dirname8(repo.root), `${basename3(repo.root)}-undo-${stampOf2(runtime.now())}`);
14310
+ const base = join34(dirname8(repo.root), `${basename3(repo.root)}-undo-${stampOf2(runtime.now())}`);
13950
14311
  return toAbsolutePath(runtime.stat(toAbsolutePath(base)) === void 0 ? base : `${base}-2`);
13951
14312
  };
13952
14313
  var intoWorktree = (runtime, repo, view2) => {
@@ -14134,19 +14495,6 @@ var helpCommand = {
14134
14495
  )
14135
14496
  };
14136
14497
 
14137
- // src/install/plugin-yield.ts
14138
- import { join as join34 } from "node:path";
14139
- var PLUGIN_VIA = "plugin";
14140
- var YIELD_STDOUT = `${JSON.stringify({ continue: true })}
14141
- `;
14142
- var projectOwnsHooks = (ctx) => {
14143
- const root = ctx.env["CLAUDE_PROJECT_DIR"] ?? ctx.cwd;
14144
- const text6 = ctx.runtime.readText(toAbsolutePath(join34(root, ...SETTINGS_FILE.split("/"))));
14145
- if (!text6.ok) return false;
14146
- const parsed = parseJsonObject(text6.value);
14147
- return parsed.ok && ownHandlersIn(parsed.value) > 0;
14148
- };
14149
-
14150
14498
  // src/commands/hook.ts
14151
14499
  var hookCommand = {
14152
14500
  path: ["hook"],
@@ -14280,17 +14628,17 @@ var rulesFor = (locale) => {
14280
14628
  var branchFor = (node, value, locale) => {
14281
14629
  const { branches } = node;
14282
14630
  if (node.kind === "select") return branches[String(value)] ?? branches["other"];
14283
- const count3 = typeof value === "number" ? value : Number(value);
14284
- if (!Number.isFinite(count3)) return branches["other"];
14285
- return branches[`=${String(count3)}`] ?? branches[rulesFor(locale).select(count3)] ?? branches["other"];
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"];
14286
14634
  };
14287
- var render = (nodes, params2, locale, count3) => nodes.map((node) => {
14635
+ var render = (nodes, params2, locale, count4) => nodes.map((node) => {
14288
14636
  if (node.kind === "text") return node.text;
14289
- if (node.kind === "count") return count3 === void 0 ? "#" : String(count3);
14637
+ if (node.kind === "count") return count4 === void 0 ? "#" : String(count4);
14290
14638
  const value = params2[node.name];
14291
14639
  if (node.kind === "arg") return value === void 0 ? `{${node.name}}` : String(value);
14292
14640
  const branch = branchFor(node, value, locale) ?? [];
14293
- return render(branch, params2, locale, node.kind === "plural" ? value : count3);
14641
+ return render(branch, params2, locale, node.kind === "plural" ? value : count4);
14294
14642
  }).join("");
14295
14643
  var formatMessage = (source, params2, locale) => render(parseMessage(source), params2, locale, void 0);
14296
14644
  var walk = (nodes, visit2) => {
@@ -14320,7 +14668,7 @@ var choicesOf = (source) => {
14320
14668
  var requiredPluralBranches = (locale) => {
14321
14669
  const rules = rulesFor(locale);
14322
14670
  const seen = /* @__PURE__ */ new Set(["other"]);
14323
- for (let count3 = 0; count3 <= 1e3; count3 += 1) seen.add(rules.select(count3));
14671
+ for (let count4 = 0; count4 <= 1e3; count4 += 1) seen.add(rules.select(count4));
14324
14672
  return [...seen].sort();
14325
14673
  };
14326
14674
 
@@ -14633,6 +14981,7 @@ var lookBack = (runtime, root, env) => {
14633
14981
 
14634
14982
  // src/install/diff.ts
14635
14983
  var MAX_DIFF_LINES = 120;
14984
+ var MAX_LCS_CELLS = 4e6;
14636
14985
  var table = (a, b) => {
14637
14986
  const rows = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
14638
14987
  for (let i = a.length - 1; i >= 0; i -= 1) {
@@ -14667,8 +15016,27 @@ var walk2 = (a, b) => {
14667
15016
  return ops;
14668
15017
  };
14669
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
+ };
14670
15038
  var lineDiff = (path, before, after) => {
14671
- const changes = walk2(splitLines(before), splitLines(after)).filter(([op]) => op !== "=");
15039
+ const changes = changedOps(splitLines(before), splitLines(after));
14672
15040
  if (changes.length === 0) return [];
14673
15041
  const lines3 = changes.map(([op, line2]) => `${op} ${line2}`);
14674
15042
  const shown3 = lines3.slice(0, MAX_DIFF_LINES);
@@ -15241,7 +15609,7 @@ var humanOf2 = (ctx, data) => [
15241
15609
  ];
15242
15610
  var resultOf3 = (ctx, data) => {
15243
15611
  const human2 = humanOf2(ctx, data);
15244
- const failing = data.summary.new + data.summary.expired;
15612
+ const failing = data.findings.filter((item) => item.status !== "known" && item.severity !== "warning").length;
15245
15613
  if (failing === 0) return success(data, human2);
15246
15614
  return failureFrom(
15247
15615
  ctx.t,