finitemonkey 0.1.0-next.14 → 0.1.0-next.16

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.
@@ -40,8 +40,8 @@ var parseJsonObject = (text6) => {
40
40
  if (!parsed.ok) return parsed;
41
41
  return isRecord(parsed.value) ? accept(parsed.value) : reject("expected a JSON object");
42
42
  };
43
- var optionalString = (record2, key) => {
44
- const value = record2[key];
43
+ var optionalString = (record3, key) => {
44
+ const value = record3[key];
45
45
  return typeof value === "string" ? value : void 0;
46
46
  };
47
47
 
@@ -544,11 +544,11 @@ var parseEntries = (value) => {
544
544
  const entries = value.map(parseEntry);
545
545
  return entries.every((entry2) => entry2 !== void 0) ? entries : void 0;
546
546
  };
547
- var fromRecord = (record2) => {
548
- const { schema, version, preset, installedAt, updatedAt } = record2;
547
+ var fromRecord = (record3) => {
548
+ const { schema, version, preset, installedAt, updatedAt } = record3;
549
549
  const known2 = PRESETS.find((item) => item === preset);
550
- const entries = parseEntries(record2["entries"]);
551
- const createdDirs = strings(record2["createdDirs"]);
550
+ const entries = parseEntries(record3["entries"]);
551
+ const createdDirs = strings(record3["createdDirs"]);
552
552
  if (schema !== LOCK_SCHEMA) return reject(`schema is not ${LOCK_SCHEMA}`);
553
553
  if (typeof version !== "string" || typeof installedAt !== "string" || typeof updatedAt !== "string") {
554
554
  return reject("version, installedAt and updatedAt must be text");
@@ -565,6 +565,134 @@ var parseLock = (text6) => {
565
565
  var renderLock = (lock) => `${JSON.stringify(lock, null, 2)}
566
566
  `;
567
567
 
568
+ // src/policy/policy.ts
569
+ import { join as join3 } from "node:path";
570
+
571
+ // src/git/project-config.ts
572
+ import { join as join2 } from "node:path";
573
+ var CONFIG_FILE2 = ".finitemonkey/config.yml";
574
+ var unquote = (value) => value.trim().replace(/^(["'])(.*)\1$/s, "$2");
575
+ var stripComment = (line2) => {
576
+ let quote;
577
+ for (let index = 0; index < line2.length; index += 1) {
578
+ const char = line2.charAt(index);
579
+ if (quote === void 0 && (char === '"' || char === "'")) quote = char;
580
+ else if (char === quote) quote = void 0;
581
+ else if (quote === void 0 && char === "#" && (index === 0 || /\s/.test(line2.charAt(index - 1)))) {
582
+ return line2.slice(0, index);
583
+ }
584
+ }
585
+ return line2;
586
+ };
587
+ var flowList = (value) => {
588
+ const trimmed = value.trim();
589
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return void 0;
590
+ return trimmed.slice(1, -1).split(",").map(unquote).filter((item) => item !== "");
591
+ };
592
+ var keyValue = (body2) => {
593
+ const match = /^("[^"]*"|'[^']*'|[^:]+?)\s*:(?:\s+(.*))?$/.exec(body2);
594
+ if (match === null) return void 0;
595
+ return { key: unquote(match[1] ?? ""), value: match[2] ?? "" };
596
+ };
597
+ var addItem = (frame, item) => {
598
+ if (frame.key === void 0) return;
599
+ const parent = frame.tree[frame.key];
600
+ if (Array.isArray(parent)) parent.push(item);
601
+ else frame.tree[frame.key] = [item];
602
+ };
603
+ var placeLine = (stack, indent, body2) => {
604
+ while (stack.length > 1 && (stack[stack.length - 1]?.indent ?? 0) >= indent && !body2.startsWith("- ")) stack.pop();
605
+ const top = stack[stack.length - 1];
606
+ if (top === void 0) return;
607
+ if (body2.startsWith("- ")) {
608
+ addItem(top, unquote(body2.slice(2)));
609
+ return;
610
+ }
611
+ const pair = keyValue(body2);
612
+ if (pair === void 0) return;
613
+ const parentTree = top.key === void 0 ? top.tree : ensureTree(top.tree, top.key);
614
+ if (pair.value === "") {
615
+ stack.push({ indent, tree: parentTree, key: pair.key });
616
+ return;
617
+ }
618
+ parentTree[pair.key] = flowList(pair.value) ?? unquote(pair.value);
619
+ };
620
+ var ensureTree = (tree, key) => {
621
+ const existing = tree[key];
622
+ if (existing !== void 0 && typeof existing !== "string" && !Array.isArray(existing)) return existing;
623
+ const fresh3 = {};
624
+ tree[key] = fresh3;
625
+ return fresh3;
626
+ };
627
+ var parseConfigYaml = (text6) => {
628
+ const root = {};
629
+ const stack = [{ indent: -1, tree: root, key: void 0 }];
630
+ for (const raw of text6.split(/\r?\n/)) {
631
+ const line2 = stripComment(raw).trimEnd();
632
+ if (line2.trim() === "" || line2.trim() === "---") continue;
633
+ const indent = line2.length - line2.trimStart().length;
634
+ placeLine(stack, indent, line2.trim());
635
+ }
636
+ return root;
637
+ };
638
+ var isTree = (node) => typeof node === "object" && !Array.isArray(node);
639
+ var configAt = (tree, ...path) => path.reduce((node, key) => isTree(node) ? node[key] : void 0, tree);
640
+ var configList = (tree, ...path) => {
641
+ const node = configAt(tree, ...path);
642
+ if (typeof node === "string") return node === "" ? [] : [node];
643
+ return Array.isArray(node) ? node.filter((item) => typeof item === "string") : [];
644
+ };
645
+ var configMap = (tree, ...path) => {
646
+ const node = configAt(tree, ...path);
647
+ if (!isTree(node)) return {};
648
+ return Object.fromEntries(
649
+ Object.entries(node).filter((entry2) => typeof entry2[1] === "string")
650
+ );
651
+ };
652
+ var loadProjectConfig = (runtime, root) => {
653
+ const text6 = runtime.readText(toAbsolutePath(join2(root, CONFIG_FILE2)));
654
+ return text6.ok ? parseConfigYaml(text6.value) : {};
655
+ };
656
+
657
+ // src/policy/policy.ts
658
+ var POLICY_FILE = ".finitemonkey/policy.yml";
659
+ var GUARD_MODES = ["off", "shadow", "active"];
660
+ var LEVELS = ["light", "standard", "strict"];
661
+ var SEVERITIES = ["low", "medium", "high"];
662
+ var DEFAULT_PROMOTE_AFTER = 20;
663
+ var OPTIONAL_GUARDS = ["prose"];
664
+ var DEFAULT_POLICY = {
665
+ exists: false,
666
+ level: "standard",
667
+ severity: "medium",
668
+ promoteAfter: DEFAULT_PROMOTE_AFTER,
669
+ guards: {}
670
+ };
671
+ var pick = (options, value, fallback) => options.find((option) => option === value) ?? fallback;
672
+ var parsePolicy = (text6) => {
673
+ const tree = parseConfigYaml(text6);
674
+ const raw = configAt(tree, "promoteAfter");
675
+ const promoteAfter = typeof raw === "string" ? Number.parseInt(raw, 10) : Number.NaN;
676
+ const guards = Object.fromEntries(
677
+ Object.entries(configMap(tree, "guards")).flatMap(([id, mode]) => {
678
+ const known2 = GUARD_MODES.find((option) => option === mode);
679
+ return known2 === void 0 ? [] : [[id, known2]];
680
+ })
681
+ );
682
+ return {
683
+ exists: true,
684
+ level: pick(LEVELS, configAt(tree, "level"), DEFAULT_POLICY.level),
685
+ severity: pick(SEVERITIES, configAt(tree, "severity"), DEFAULT_POLICY.severity),
686
+ promoteAfter: Number.isInteger(promoteAfter) && promoteAfter > 0 ? promoteAfter : DEFAULT_PROMOTE_AFTER,
687
+ guards
688
+ };
689
+ };
690
+ var policyPath = (root) => toAbsolutePath(join3(root, POLICY_FILE));
691
+ var loadPolicy = (runtime, root) => {
692
+ const text6 = runtime.readText(policyPath(root));
693
+ return text6.ok ? parsePolicy(text6.value) : DEFAULT_POLICY;
694
+ };
695
+
568
696
  // src/install/managed-block.ts
569
697
  var lineBreak = (text6) => text6.includes("\r\n") ? "\r\n" : "\n";
570
698
  var blockRange = (lines3, markers) => {
@@ -594,6 +722,11 @@ var withoutManagedBlock = (text6, markers) => {
594
722
  // src/install/templates/en.ts
595
723
  var en = {
596
724
  configHeader: "Finite Monkey project settings. Commit this file. `finitemonkey update` keeps your edits.",
725
+ policyHeader: [
726
+ "Guard policy for this project. Modes: off, shadow (record what it would block), active.",
727
+ "The guards for secrets, production, lost git work and false done stay active whatever this file says.",
728
+ "A guard missing from this list starts in shadow and is promoted after promoteAfter firings without a false positive."
729
+ ],
597
730
  agentsBlock: ({ gate, doctor }) => [
598
731
  "## Finite Monkey",
599
732
  "",
@@ -616,6 +749,11 @@ var en = {
616
749
  // src/install/templates/es.ts
617
750
  var es = {
618
751
  configHeader: "Configuración de Finite Monkey en este proyecto. Versiona este archivo. `finitemonkey update` conserva tus cambios.",
752
+ policyHeader: [
753
+ "Política de los guards de este proyecto. Modos: off, shadow (registra lo que bloquearía), active.",
754
+ "Los guards de secretos, producción, trabajo de git perdido y falso terminado siguen activos diga lo que diga este archivo.",
755
+ "Un guard que no está en esta lista empieza en shadow y se promueve tras promoteAfter disparos sin falso positivo."
756
+ ],
619
757
  agentsBlock: ({ gate, doctor }) => [
620
758
  "## Finite Monkey",
621
759
  "",
@@ -638,6 +776,11 @@ var es = {
638
776
  // src/install/templates/pt-BR.ts
639
777
  var ptBR = {
640
778
  configHeader: "Configuração do Finite Monkey neste projeto. Versione este arquivo. O `finitemonkey update` mantém suas edições.",
779
+ policyHeader: [
780
+ "Política dos guards deste projeto. Modos: off, shadow (registra o que bloquearia), active.",
781
+ "Os guards de segredos, produção, trabalho de git perdido e falso pronto ficam ativos, diga o que disser este arquivo.",
782
+ "Um guard que não está nesta lista começa em shadow e é promovido depois de promoteAfter disparos sem falso positivo."
783
+ ],
641
784
  agentsBlock: ({ gate, doctor }) => [
642
785
  "## Finite Monkey",
643
786
  "",
@@ -856,7 +999,7 @@ var DENY_FLOOR = {
856
999
  var DENY_RULES = [...DENY_FLOOR.secrets, ...DENY_FLOOR.git, ...DENY_FLOOR.proof];
857
1000
 
858
1001
  // src/install/detect.ts
859
- import { join as join2 } from "node:path";
1002
+ import { join as join4 } from "node:path";
860
1003
 
861
1004
  // src/install/json-text.ts
862
1005
  var DEFAULT_STYLE = { indent: " ", finalNewline: true };
@@ -868,11 +1011,11 @@ var renderJson = (value, style) => `${JSON.stringify(value, null, style.indent)}
868
1011
  var parseSettings = (text6) => text6 === void 0 || text6.trim() === "" ? { ok: true, value: {} } : parseJsonObject(text6);
869
1012
  var asList = (value) => Array.isArray(value) ? value : [];
870
1013
  var asRecord = (value) => isRecord(value) ? value : {};
871
- var setKey = (record2, key, value) => {
872
- if (!(key in record2)) return value === void 0 ? record2 : { ...record2, [key]: value };
1014
+ var setKey = (record3, key, value) => {
1015
+ if (!(key in record3)) return value === void 0 ? record3 : { ...record3, [key]: value };
873
1016
  return Object.fromEntries(
874
- Object.entries(record2).flatMap(([name, current]) => {
875
- if (name !== key) return [[name, current]];
1017
+ Object.entries(record3).flatMap(([name, current2]) => {
1018
+ if (name !== key) return [[name, current2]];
876
1019
  return value === void 0 ? [] : [[name, value]];
877
1020
  })
878
1021
  );
@@ -949,7 +1092,7 @@ var scopeAuthorities = (text6, source) => {
949
1092
  var userSettingsText = (project, env) => {
950
1093
  const home = env["CLAUDE_CONFIG_DIR"] ?? env["HOME"] ?? env["USERPROFILE"];
951
1094
  if (home === void 0) return void 0;
952
- const file = env["CLAUDE_CONFIG_DIR"] === void 0 ? join2(home, ".claude", "settings.json") : join2(home, "settings.json");
1095
+ const file = env["CLAUDE_CONFIG_DIR"] === void 0 ? join4(home, ".claude", "settings.json") : join4(home, "settings.json");
953
1096
  const text6 = project.runtime.readText(toAbsolutePath(file));
954
1097
  return text6.ok ? text6.value : void 0;
955
1098
  };
@@ -1041,6 +1184,18 @@ var applyMigrations = (items, previous, version, migrations) => {
1041
1184
  // src/install/migrations/index.ts
1042
1185
  var MIGRATIONS = [];
1043
1186
 
1187
+ // src/install/policy-template.ts
1188
+ var policyTemplate = (lang) => [
1189
+ `# ${GENERATED_MARKER}`,
1190
+ ...TEMPLATES[lang].policyHeader.map((line2) => `# ${line2}`),
1191
+ "level: standard",
1192
+ "severity: medium",
1193
+ `promoteAfter: ${String(DEFAULT_PROMOTE_AFTER)}`,
1194
+ "guards:",
1195
+ ...OPTIONAL_GUARDS.map((id) => ` ${id}: active`),
1196
+ ""
1197
+ ].join("\n");
1198
+
1044
1199
  // src/install/settings-merge.ts
1045
1200
  var SETTINGS_SCHEMA_URL = "https://json.schemastore.org/claude-code-settings.json";
1046
1201
  var ATTRIBUTION_OFF = { commit: "", pr: "", sessionUrl: false };
@@ -1095,9 +1250,9 @@ var withPermissions = (settings, lists) => {
1095
1250
  var missing = (wanted, existing) => wanted.filter((rule) => !existing.includes(rule));
1096
1251
  var withoutAdded = (existing, added) => existing.filter((rule) => typeof rule !== "string" || !added.includes(rule));
1097
1252
  var ownHookMatcher = (recorded) => (handler) => isOwnHandler(handler) || recorded.some((own) => JSON.stringify(own) === JSON.stringify(handler));
1098
- var mergeSettings = (current, wanted, recorded) => {
1099
- const schema = Object.keys(current).length === 0;
1100
- const base = schema ? { $schema: SETTINGS_SCHEMA_URL } : current;
1253
+ var mergeSettings = (current2, wanted, recorded) => {
1254
+ const schema = Object.keys(current2).length === 0;
1255
+ const base = schema ? { $schema: SETTINGS_SCHEMA_URL } : current2;
1101
1256
  const hooks = mergeHooks(asRecord(base["hooks"]), ownHookMatcher(recorded), wanted.hooks);
1102
1257
  const permissions = asRecord(base["permissions"]);
1103
1258
  const existingDeny = asList(permissions["deny"]);
@@ -1122,10 +1277,10 @@ var mergeSettings = (current, wanted, recorded) => {
1122
1277
  return { settings, added: language === void 0 ? added : { ...added, language } };
1123
1278
  };
1124
1279
  var sameAttribution = (value) => JSON.stringify(value) === JSON.stringify(ATTRIBUTION_OFF);
1125
- var unmergeSettings = (current, added, recorded) => {
1126
- const hooks = mergeHooks(asRecord(current["hooks"]), ownHookMatcher(recorded), {});
1127
- const permissions = asRecord(current["permissions"]);
1128
- let settings = withPermissions(withHooks(current, hooks), {
1280
+ var unmergeSettings = (current2, added, recorded) => {
1281
+ const hooks = mergeHooks(asRecord(current2["hooks"]), ownHookMatcher(recorded), {});
1282
+ const permissions = asRecord(current2["permissions"]);
1283
+ let settings = withPermissions(withHooks(current2, hooks), {
1129
1284
  deny: withoutAdded(asList(permissions["deny"]), added.deny),
1130
1285
  ask: withoutAdded(asList(permissions["ask"]), added.ask),
1131
1286
  blockReads: added.blockReads && permissions[BLOCK_READS] === true ? void 0 : permissions[BLOCK_READS]
@@ -1202,8 +1357,8 @@ var wantedHooks = (detection) => Object.fromEntries(
1202
1357
  );
1203
1358
  var settingsItem = (project, detection, input, previous) => {
1204
1359
  const before = project.read(SETTINGS_FILE);
1205
- const current = parseSettings(before);
1206
- if (!current.ok) return reject({ kind: "settings", detail: current.error });
1360
+ const current2 = parseSettings(before);
1361
+ if (!current2.ok) return reject({ kind: "settings", detail: current2.error });
1207
1362
  const hooks = wantedHooks(detection);
1208
1363
  const wanted = {
1209
1364
  hooks,
@@ -1213,7 +1368,7 @@ var settingsItem = (project, detection, input, previous) => {
1213
1368
  marketplace: input.marketplace,
1214
1369
  language: input.claudeLanguage
1215
1370
  };
1216
- const merged = mergeSettings(current.value, wanted, previous?.handlers ?? []);
1371
+ const merged = mergeSettings(current2.value, wanted, previous?.handlers ?? []);
1217
1372
  const after = renderJson(merged.settings, before === void 0 ? DEFAULT_STYLE : styleOf(before));
1218
1373
  return accept({
1219
1374
  path: SETTINGS_FILE,
@@ -1254,7 +1409,8 @@ var generatedItem = (project, file, previous) => {
1254
1409
  var generatedFiles = (input, runner, vendor) => {
1255
1410
  const own = [
1256
1411
  { path: CONFIG_FILE, template: configTemplate(input.preset, input.lang), isOurs: hasMarker },
1257
- { path: VERIFY_SKILL, template: verifySkillTemplate(runner, input.lang), isOurs: isOwnSkill }
1412
+ { path: VERIFY_SKILL, template: verifySkillTemplate(runner, input.lang), isOurs: isOwnSkill },
1413
+ { path: POLICY_FILE, template: policyTemplate(input.lang), isOurs: hasMarker }
1258
1414
  ];
1259
1415
  if (!vendor) return accept(own);
1260
1416
  if (input.bundle === void 0) return reject({ kind: "bundle", detail: "the running finitemonkey is not a bundle" });
@@ -1428,9 +1584,9 @@ var detectLang = (flag2, env, settings = {}) => {
1428
1584
  };
1429
1585
 
1430
1586
  // src/install/project.ts
1431
- import { dirname as dirname4, join as join3 } from "node:path";
1587
+ import { dirname as dirname4, join as join5 } from "node:path";
1432
1588
  var projectAt = (root, runtime) => {
1433
- const abs = (relative9) => toAbsolutePath(join3(root, ...relative9.split("/")));
1589
+ const abs = (relative9) => toAbsolutePath(join5(root, ...relative9.split("/")));
1434
1590
  return {
1435
1591
  root,
1436
1592
  runtime,
@@ -1444,12 +1600,12 @@ var projectAt = (root, runtime) => {
1444
1600
  };
1445
1601
  };
1446
1602
  var findGitRoot = (start, runtime) => {
1447
- let current = start;
1603
+ let current2 = start;
1448
1604
  for (; ; ) {
1449
- if (runtime.stat(toAbsolutePath(join3(current, ".git"))) !== void 0) return toAbsolutePath(current);
1450
- const parent = dirname4(current);
1451
- if (parent === current) return void 0;
1452
- current = parent;
1605
+ if (runtime.stat(toAbsolutePath(join5(current2, ".git"))) !== void 0) return toAbsolutePath(current2);
1606
+ const parent = dirname4(current2);
1607
+ if (parent === current2) return void 0;
1608
+ current2 = parent;
1453
1609
  }
1454
1610
  };
1455
1611
 
@@ -1547,7 +1703,7 @@ var applyFixes = (ctx, write3) => {
1547
1703
  };
1548
1704
 
1549
1705
  // src/doctor/report-file.ts
1550
- import { join as join5 } from "node:path";
1706
+ import { join as join7 } from "node:path";
1551
1707
 
1552
1708
  // src/evidence/log.ts
1553
1709
  var logEvent = (runtime, paths, event, data) => runtime.appendText(paths.log, `${JSON.stringify({ at: runtime.now(), event, ...data })}
@@ -1620,8 +1776,8 @@ var shannonEntropy = (text6) => {
1620
1776
  const counts = /* @__PURE__ */ new Map();
1621
1777
  for (const char of text6) counts.set(char, (counts.get(char) ?? 0) + 1);
1622
1778
  let entropy = 0;
1623
- for (const count3 of counts.values()) {
1624
- const p = count3 / text6.length;
1779
+ for (const count4 of counts.values()) {
1780
+ const p = count4 / text6.length;
1625
1781
  entropy -= p * Math.log2(p);
1626
1782
  }
1627
1783
  return entropy;
@@ -1670,6 +1826,35 @@ var defineRule = (spec2) => ({
1670
1826
  skipTestPaths: spec2.skipTestPaths ?? false,
1671
1827
  placeholders: spec2.placeholders ?? true
1672
1828
  });
1829
+ var isWarning = (finding3) => finding3.severity === "warning";
1830
+ var blocking = (findings) => findings.filter((finding3) => !isWarning(finding3));
1831
+
1832
+ // src/secrets/rules/exposure.ts
1833
+ var ARGUMENT_CONTEXT = String.raw`(?:^|\n)[ \t-]*(?:command|entrypoint|CMD|ENTRYPOINT|RUN|ExecStart(?:Pre|Post)?|ExecReload)\b[^\n]*?`;
1834
+ var SECRET_FLAG = String.raw`(?<=[\s"'\[,])--?[A-Za-z0-9_-]*(?:password|passwd|pass|secret|token|requirepass|api-?key)`;
1835
+ var EXPOSURE_RULES = [
1836
+ defineRule({
1837
+ id: "secret-in-process-args",
1838
+ 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)",
1839
+ kind: "practice",
1840
+ severity: "warning",
1841
+ keywords: ["command", "entrypoint", "cmd", "run", "execstart", "execreload"],
1842
+ source: String.raw`${ARGUMENT_CONTEXT}${SECRET_FLAG}(?:=["']?|["'][ \t]*,[ \t]*["']|[ \t]+["']?)(?<secret>[^\s"',\]$\x60/][^\s"',\]\x60]{3,127})`,
1843
+ flags: "i",
1844
+ minLength: 4
1845
+ }),
1846
+ defineRule({
1847
+ id: "tls-disabled",
1848
+ 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",
1849
+ kind: "practice",
1850
+ severity: "warning",
1851
+ keywords: ["sslmode", "rejectunauthorized", "verify", "insecureskipverify", "node_tls_reject_unauthorized"],
1852
+ 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)`,
1853
+ minLength: 1,
1854
+ placeholders: false,
1855
+ skipTestPaths: true
1856
+ })
1857
+ ];
1673
1858
 
1674
1859
  // src/secrets/rules/providers.ts
1675
1860
  var PROVIDER_RULES = [
@@ -1893,7 +2078,7 @@ var STRUCTURAL_RULES = [
1893
2078
  ];
1894
2079
 
1895
2080
  // src/secrets/rules/catalog.ts
1896
- var RULES = [...PROVIDER_RULES, ...STRUCTURAL_RULES];
2081
+ var RULES = [...PROVIDER_RULES, ...STRUCTURAL_RULES, ...EXPOSURE_RULES];
1897
2082
  var CREDENTIAL_RULES = RULES.filter((rule) => rule.kind === "credential");
1898
2083
  var ruleById = (id) => RULES.find((rule) => rule.id === id);
1899
2084
 
@@ -1985,11 +2170,11 @@ var maskDeep = (value) => {
1985
2170
  };
1986
2171
 
1987
2172
  // src/task/paths.ts
1988
- import { join as join4 } from "node:path";
2173
+ import { join as join6 } from "node:path";
1989
2174
  var FM_DIR = ".finitemonkey";
1990
2175
  var fmPaths = (root) => {
1991
- const dir = toAbsolutePath(join4(root, FM_DIR));
1992
- const at = (...parts) => toAbsolutePath(join4(dir, ...parts));
2176
+ const dir = toAbsolutePath(join6(root, FM_DIR));
2177
+ const at = (...parts) => toAbsolutePath(join6(dir, ...parts));
1993
2178
  return {
1994
2179
  root,
1995
2180
  dir,
@@ -2225,16 +2410,16 @@ var reportMarkdown = (env, ctx, envelope) => {
2225
2410
  ];
2226
2411
  return maskText(withoutHome(lines3.join("\n"), env.runtime.homeDir()));
2227
2412
  };
2228
- var reportPath = (env) => toAbsolutePath(join5(env.root, ...REPORTS_DIR.split("/"), `doctor-${stamp(env.runtime.now())}.md`));
2413
+ var reportPath = (env) => toAbsolutePath(join7(env.root, ...REPORTS_DIR.split("/"), `doctor-${stamp(env.runtime.now())}.md`));
2229
2414
 
2230
2415
  // src/install/plugin-yield.ts
2231
- import { join as join6 } from "node:path";
2416
+ import { join as join8 } from "node:path";
2232
2417
  var PLUGIN_VIA = "plugin";
2233
2418
  var YIELD_STDOUT = `${JSON.stringify({ continue: true })}
2234
2419
  `;
2235
2420
  var projectOwnsHooks = (ctx) => {
2236
2421
  const root = ctx.env["CLAUDE_PROJECT_DIR"] ?? ctx.cwd;
2237
- const text6 = ctx.runtime.readText(toAbsolutePath(join6(root, ...SETTINGS_FILE.split("/"))));
2422
+ const text6 = ctx.runtime.readText(toAbsolutePath(join8(root, ...SETTINGS_FILE.split("/"))));
2238
2423
  if (!text6.ok) return false;
2239
2424
  const parsed = parseJsonObject(text6.value);
2240
2425
  return parsed.ok && ownHandlersIn(parsed.value) > 0;
@@ -2248,7 +2433,7 @@ var STABLE_PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
2248
2433
  var NEXT_PLUGIN_ID = `${PLUGIN_NAME}@${NEXT_MARKETPLACE_NAME}`;
2249
2434
 
2250
2435
  // src/doctor/settings.ts
2251
- import { join as join7 } from "node:path";
2436
+ import { join as join9 } from "node:path";
2252
2437
  var MANAGED = {
2253
2438
  darwin: "/Library/Application Support/ClaudeCode/managed-settings.json",
2254
2439
  linux: "/etc/claude-code/managed-settings.json",
@@ -2257,7 +2442,7 @@ var MANAGED = {
2257
2442
  var userSettingsPath = ({ env, runtime }) => {
2258
2443
  const configDir = env["CLAUDE_CONFIG_DIR"];
2259
2444
  return toAbsolutePath(
2260
- configDir === void 0 ? join7(runtime.homeDir(), ".claude", "settings.json") : join7(configDir, "settings.json")
2445
+ configDir === void 0 ? join9(runtime.homeDir(), ".claude", "settings.json") : join9(configDir, "settings.json")
2261
2446
  );
2262
2447
  };
2263
2448
  var settingsPaths = (env) => {
@@ -2265,8 +2450,8 @@ var settingsPaths = (env) => {
2265
2450
  return [
2266
2451
  ...managed === void 0 ? [] : [{ scope: "managed", path: toAbsolutePath(managed) }],
2267
2452
  { scope: "user", path: userSettingsPath(env) },
2268
- { scope: "project", path: toAbsolutePath(join7(env.root, ".claude", "settings.json")) },
2269
- { scope: "local", path: toAbsolutePath(join7(env.root, ".claude", "settings.local.json")) }
2453
+ { scope: "project", path: toAbsolutePath(join9(env.root, ".claude", "settings.json")) },
2454
+ { scope: "local", path: toAbsolutePath(join9(env.root, ".claude", "settings.local.json")) }
2270
2455
  ];
2271
2456
  };
2272
2457
  var readSettings = (env) => settingsPaths(env).flatMap(({ scope: scope2, path }) => {
@@ -2410,7 +2595,7 @@ var checkClaudeSettings = (env) => {
2410
2595
  };
2411
2596
 
2412
2597
  // src/doctor/environment.ts
2413
- import { join as join8 } from "node:path";
2598
+ import { join as join10 } from "node:path";
2414
2599
  var AUTH_VARIABLES = [
2415
2600
  "ANTHROPIC_API_KEY",
2416
2601
  "ANTHROPIC_AUTH_TOKEN",
@@ -2448,7 +2633,7 @@ var pathDirs = ({ env, platform }) => {
2448
2633
  var copiesOnPath = (env) => {
2449
2634
  const names = EXECUTABLES[env.platform] ?? ["finitemonkey"];
2450
2635
  const found = pathDirs(env).flatMap(
2451
- (dir) => names.map((name) => toAbsolutePath(join8(dir, name))).filter((path) => env.runtime.stat(path)?.kind === "file").map((path) => env.runtime.realpath(path))
2636
+ (dir) => names.map((name) => toAbsolutePath(join10(dir, name))).filter((path) => env.runtime.stat(path)?.kind === "file").map((path) => env.runtime.realpath(path))
2452
2637
  );
2453
2638
  const byDir = new Map(found.map((path) => [path.replace(/\.(cmd|exe|ps1)$/i, ""), path]));
2454
2639
  return [...byDir.values()];
@@ -2482,7 +2667,7 @@ var checkAuthVariables = ({ env, platform }) => {
2482
2667
  };
2483
2668
 
2484
2669
  // src/doctor/hooks.ts
2485
- import { join as join29 } from "node:path";
2670
+ import { join as join31 } from "node:path";
2486
2671
 
2487
2672
  // src/hooks/events.ts
2488
2673
  var HOOK_EVENTS = [
@@ -2529,9 +2714,9 @@ var deadlineFor = (event, raw) => {
2529
2714
  };
2530
2715
 
2531
2716
  // src/hooks/fields.ts
2532
- var textField = (record2, ...names) => {
2717
+ var textField = (record3, ...names) => {
2533
2718
  for (const name of names) {
2534
- const value = record2[name];
2719
+ const value = record3[name];
2535
2720
  if (typeof value === "string") return value;
2536
2721
  }
2537
2722
  return void 0;
@@ -2626,8 +2811,8 @@ var closeTask = (state, id) => assemble({
2626
2811
 
2627
2812
  // src/guards/release/grants.ts
2628
2813
  var GRANT_MINUTES = 30;
2629
- var text2 = (record2, key) => {
2630
- const value = record2[key];
2814
+ var text2 = (record3, key) => {
2815
+ const value = record3[key];
2631
2816
  return typeof value === "string" ? value : void 0;
2632
2817
  };
2633
2818
  var toGrant = (value) => {
@@ -3162,9 +3347,9 @@ var skipOptions = (args, wrapper) => {
3162
3347
  }
3163
3348
  return index + wrapper.positionals;
3164
3349
  };
3165
- var shift = (stage, count3, name) => ({
3350
+ var shift = (stage, count4, name) => ({
3166
3351
  ...stage,
3167
- argv: stage.argv.slice(count3),
3352
+ argv: stage.argv.slice(count4),
3168
3353
  via: [...stage.via, name]
3169
3354
  });
3170
3355
  var envCommand = (stage) => {
@@ -3269,10 +3454,10 @@ var PS_ENV = /^\$env:([A-Za-z_][A-Za-z0-9_]*)=?(.*)$/is;
3269
3454
  var CHANGES_DIR = /* @__PURE__ */ new Set(["cd", "pushd", "set-location", "push-location"]);
3270
3455
  var lex = (text6, language) => language === "bash" ? lexBash(text6) : lexPowerShell(text6, language === "cmd");
3271
3456
  var isWindowsAbsolute = (path) => /^[A-Za-z]:\//.test(path);
3272
- var joinDir = (current, target) => {
3457
+ var joinDir = (current2, target) => {
3273
3458
  const next2 = toPosixPath(target);
3274
3459
  if (next2.startsWith("/") || isWindowsAbsolute(next2) || next2.startsWith("~")) return next2;
3275
- return current === void 0 ? posix.normalize(next2) : posix.join(current, next2);
3460
+ return current2 === void 0 ? posix.normalize(next2) : posix.join(current2, next2);
3276
3461
  };
3277
3462
  var assignments = (words, language) => {
3278
3463
  if (language !== "bash") {
@@ -3385,9 +3570,9 @@ var COMMIT_VALUE_SHORTS = /* @__PURE__ */ new Set(["m", "F", "C", "c", "t"]);
3385
3570
  var VERIFY_SUBCOMMANDS = /* @__PURE__ */ new Set(["commit", "push", "merge", "am", "rebase", "cherry-pick", "revert", "pull"]);
3386
3571
  var isHooksPath = (value) => /^core\.hookspath(=|$)/i.test(value);
3387
3572
  var envFindings = (command) => {
3388
- const pairs = Object.entries(command.env);
3573
+ const pairs2 = Object.entries(command.env);
3389
3574
  const exported = command.argv[0] === "export" ? command.argv.slice(1).map((arg) => arg.split("=", 2)) : [];
3390
- return [...pairs, ...exported].filter(([name = ""]) => HOOK_ENV.test(name)).map(([name = "", value = ""]) => ({ kind: "hook_env", detail: `${name}=${value}` }));
3575
+ return [...pairs2, ...exported].filter(([name = ""]) => HOOK_ENV.test(name)).map(([name = "", value = ""]) => ({ kind: "hook_env", detail: `${name}=${value}` }));
3391
3576
  };
3392
3577
  var splitGit = (args) => {
3393
3578
  let index = 0;
@@ -3545,7 +3730,7 @@ var selfGuard = (input) => {
3545
3730
  };
3546
3731
 
3547
3732
  // src/guards/tests/hook.ts
3548
- import { join as join9, relative as relative3 } from "node:path";
3733
+ import { join as join11, relative as relative3 } from "node:path";
3549
3734
 
3550
3735
  // src/guards/tests/patterns.ts
3551
3736
  var JS_TEST = /(^|\/)(__tests__|__test__)\/|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)tests?\/.*\.[cm]?[jt]sx?$/;
@@ -3735,7 +3920,7 @@ var shellViolations = (input, root) => {
3735
3920
  return parseShell(command).flatMap(
3736
3921
  (simple) => writtenPaths(simple).flatMap((path) => {
3737
3922
  const local = localPath(root, path);
3738
- const exists = input.runtime.stat(toAbsolutePath(join9(root, local))) !== void 0;
3923
+ const exists = input.runtime.stat(toAbsolutePath(join11(root, local))) !== void 0;
3739
3924
  if (!exists) return [];
3740
3925
  if (isProtectedConfig(local)) return [{ kind: "config_changed", path: local }];
3741
3926
  return isTestFile(local) ? [{ kind: "test_removed", path: local }] : [];
@@ -3755,20 +3940,20 @@ var testsGuard = (input) => {
3755
3940
  };
3756
3941
 
3757
3942
  // src/gate/hooks/config.ts
3758
- import { join as join11 } from "node:path";
3943
+ import { join as join13 } from "node:path";
3759
3944
 
3760
3945
  // src/gate/hooks/common.ts
3761
- import { dirname as dirname5, join as join10 } from "node:path";
3946
+ import { dirname as dirname5, join as join12 } from "node:path";
3762
3947
  var DEFAULT_GATE_BUDGET_MS = 3e5;
3763
3948
  var sessionCwd = (input) => input.payload.cwd === void 0 ? input.cwd : toAbsolutePath(input.payload.cwd, input.cwd);
3764
3949
  var MAX_DEPTH2 = 64;
3765
3950
  var findRepoRoot = (runtime, start) => {
3766
- let current = start;
3951
+ let current2 = start;
3767
3952
  for (let depth = 0; depth < MAX_DEPTH2; depth += 1) {
3768
- if (runtime.stat(toAbsolutePath(join10(current, ".git"))) !== void 0) return current;
3769
- const parent = toAbsolutePath(dirname5(current));
3770
- if (parent === current) return void 0;
3771
- current = parent;
3953
+ if (runtime.stat(toAbsolutePath(join12(current2, ".git"))) !== void 0) return current2;
3954
+ const parent = toAbsolutePath(dirname5(current2));
3955
+ if (parent === current2) return void 0;
3956
+ current2 = parent;
3772
3957
  }
3773
3958
  return void 0;
3774
3959
  };
@@ -3788,7 +3973,7 @@ var contextOutput = (event, additionalContext, systemMessage) => ({
3788
3973
  var human = (text6) => `finitemonkey: ${text6}`;
3789
3974
 
3790
3975
  // src/gate/hooks/config.ts
3791
- var CONFIG_FILE2 = "config-hooks.json";
3976
+ var CONFIG_FILE3 = "config-hooks.json";
3792
3977
  var GUARDED_SOURCES = /* @__PURE__ */ new Set(["user_settings", "project_settings", "local_settings"]);
3793
3978
  var countIn = (value) => {
3794
3979
  if (typeof value === "string") return /\bfinitemonkey\b|\bfm hook\b/.test(value) ? 1 : 0;
@@ -3802,20 +3987,20 @@ var censusOf = (text6) => {
3802
3987
  return { hooks: countIn(parsed.value["hooks"]), disabled: parsed.value["disableAllHooks"] === true };
3803
3988
  };
3804
3989
  var settingsFiles = (paths, env) => [
3805
- toAbsolutePath(join11(paths.root, ".claude", "settings.json")),
3806
- toAbsolutePath(join11(paths.root, ".claude", "settings.local.json")),
3807
- ...env["HOME"] === void 0 ? [] : [toAbsolutePath(join11(env["HOME"], ".claude", "settings.json"))]
3990
+ toAbsolutePath(join13(paths.root, ".claude", "settings.json")),
3991
+ toAbsolutePath(join13(paths.root, ".claude", "settings.local.json")),
3992
+ ...env["HOME"] === void 0 ? [] : [toAbsolutePath(join13(env["HOME"], ".claude", "settings.json"))]
3808
3993
  ];
3809
3994
  var read2 = (runtime, path) => {
3810
3995
  const text6 = runtime.readText(path);
3811
3996
  return text6.ok ? text6.value : void 0;
3812
3997
  };
3813
3998
  var known = (runtime, paths) => {
3814
- const raw = readJsonFile(runtime, paths.cache(CONFIG_FILE2))?.["hooks"];
3999
+ const raw = readJsonFile(runtime, paths.cache(CONFIG_FILE3))?.["hooks"];
3815
4000
  return isRecord(raw) ? Object.fromEntries(Object.entries(raw).filter((entry2) => typeof entry2[1] === "number")) : {};
3816
4001
  };
3817
4002
  var save = (runtime, paths, counts) => {
3818
- writeJsonFile(runtime, paths.cache(CONFIG_FILE2), { hooks: { ...counts } });
4003
+ writeJsonFile(runtime, paths.cache(CONFIG_FILE3), { hooks: { ...counts } });
3819
4004
  };
3820
4005
  var recordConfigHooks = (runtime, paths, env) => {
3821
4006
  const counts = Object.fromEntries(
@@ -3940,6 +4125,7 @@ var implicitContract = (files) => {
3940
4125
  { id: "prose", kind: "prose" },
3941
4126
  { id: "worktree", kind: "worktree" },
3942
4127
  { id: "merge", kind: "merge" },
4128
+ { id: "callers", kind: "callers" },
3943
4129
  { id: "pr", kind: "pr", requireOpen: false }
3944
4130
  ],
3945
4131
  decisionsNeeded: [],
@@ -3948,7 +4134,7 @@ var implicitContract = (files) => {
3948
4134
  };
3949
4135
 
3950
4136
  // src/gate/prepare.ts
3951
- import { join as join12 } from "node:path";
4137
+ import { join as join15 } from "node:path";
3952
4138
 
3953
4139
  // src/core/yaml/scalar.ts
3954
4140
  var ESCAPES = {
@@ -4096,7 +4282,7 @@ var parseValue = (text6) => {
4096
4282
 
4097
4283
  // src/core/yaml/parse.ts
4098
4284
  var BOM = new RegExp(`^${String.fromCharCode(65279)}`);
4099
- var stripComment = (text6) => {
4285
+ var stripComment2 = (text6) => {
4100
4286
  for (let index = 0; index < text6.length; index += 1) {
4101
4287
  const char = text6[index];
4102
4288
  if (char === '"' || char === "'") {
@@ -4111,7 +4297,7 @@ var toLines = (source) => {
4111
4297
  const lines3 = [];
4112
4298
  const raw = source.replace(BOM, "").split(/\r?\n/);
4113
4299
  for (const [index, full] of raw.entries()) {
4114
- const text6 = stripComment(full).trimEnd();
4300
+ const text6 = stripComment2(full).trimEnd();
4115
4301
  const body2 = text6.trimStart();
4116
4302
  if (body2 === "" || body2 === "---" && lines3.length === 0) continue;
4117
4303
  const indentText = text6.slice(0, text6.length - body2.length);
@@ -4219,7 +4405,8 @@ var CHECK_KINDS = [
4219
4405
  "worktree",
4220
4406
  "merge",
4221
4407
  "prod",
4222
- "prose"
4408
+ "prose",
4409
+ "review"
4223
4410
  ];
4224
4411
  var GIT_REQUIREMENTS = ["committed", "pushed", "pr_open"];
4225
4412
  var SIZES = ["S", "M", "L"];
@@ -4227,8 +4414,8 @@ var TASK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
4227
4414
  var TASK_SCHEMA = "finitemonkey/task@1";
4228
4415
 
4229
4416
  // src/task/validate.ts
4230
- var str = (record2, key, at, issues, required) => {
4231
- const value = record2[key];
4417
+ var str = (record3, key, at, issues, required) => {
4418
+ const value = record3[key];
4232
4419
  if (value === void 0 || value === null) {
4233
4420
  if (required) issues.push(`${at}${key}: required`);
4234
4421
  return void 0;
@@ -4239,8 +4426,8 @@ var str = (record2, key, at, issues, required) => {
4239
4426
  }
4240
4427
  return value;
4241
4428
  };
4242
- var positive = (record2, key, at, issues) => {
4243
- const value = record2[key];
4429
+ var positive = (record3, key, at, issues) => {
4430
+ const value = record3[key];
4244
4431
  if (value === void 0 || value === null) return void 0;
4245
4432
  if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
4246
4433
  issues.push(`${at}${key}: must be a positive integer`);
@@ -4259,12 +4446,12 @@ var gitRequire = (value, at, issues) => {
4259
4446
  if (known2.some((item) => item === void 0)) issues.push(`${at}: use ${GIT_REQUIREMENTS.join(", ")}`);
4260
4447
  return known2.filter((item) => item !== void 0);
4261
4448
  };
4262
- var checkFields = (record2, kind, at, issues) => {
4263
- const run4 = str(record2, "run", at, issues, kind === "command");
4264
- const timeoutS = positive(record2, "timeout_s", at, issues);
4265
- const description = str(record2, "description", at, issues, kind === "manual");
4266
- const require2 = kind === "git" ? gitRequire(record2["require"] ?? ["committed"], `${at}require`, issues) : void 0;
4267
- const open = record2["require_open"];
4449
+ var checkFields = (record3, kind, at, issues) => {
4450
+ const run4 = str(record3, "run", at, issues, kind === "command");
4451
+ const timeoutS = positive(record3, "timeout_s", at, issues);
4452
+ const description = str(record3, "description", at, issues, kind === "manual");
4453
+ const require2 = kind === "git" ? gitRequire(record3["require"] ?? ["committed"], `${at}require`, issues) : void 0;
4454
+ const open = record3["require_open"];
4268
4455
  if (open !== void 0 && typeof open !== "boolean") issues.push(`${at}require_open: must be true or false`);
4269
4456
  return {
4270
4457
  ...run4 === void 0 ? {} : { run: run4 },
@@ -4335,6 +4522,7 @@ var validateContract = (value, implicit = false) => {
4335
4522
  const contractScope = scope(value["scope"], issues);
4336
4523
  const checks = acceptance(value["acceptance"], issues);
4337
4524
  const decisionsNeeded = strings2(value["decisions_needed"], "decisions_needed", issues);
4525
+ const callersIgnore = strings2(value["callers_ignore"], "callers_ignore", issues);
4338
4526
  if (issues.length > 0 || id === void 0 || goal === void 0 || size === void 0) return reject(issues);
4339
4527
  return accept({
4340
4528
  id,
@@ -4344,6 +4532,7 @@ var validateContract = (value, implicit = false) => {
4344
4532
  scope: contractScope,
4345
4533
  acceptance: checks,
4346
4534
  decisionsNeeded,
4535
+ ...callersIgnore.length === 0 ? {} : { callersIgnore },
4347
4536
  implicit
4348
4537
  });
4349
4538
  };
@@ -4360,7 +4549,7 @@ var parseContract = (id, text6) => {
4360
4549
  if (valid.value.id !== id) return reject({ kind: "invalid", id, issues: [`id: must match the file name (${id})`] });
4361
4550
  return accept(valid.value);
4362
4551
  };
4363
- var pick = (lock, sources, id) => {
4552
+ var pick2 = (lock, sources, id) => {
4364
4553
  const approved = lock.approved[id];
4365
4554
  if (approved !== void 0) return { text: approved, source: "lock" };
4366
4555
  const base = sources.base(taskRelative(id));
@@ -4371,7 +4560,7 @@ var pick = (lock, sources, id) => {
4371
4560
  var loadTask = (files, paths, sources, id) => {
4372
4561
  if (!TASK_ID.test(id)) return reject({ kind: "not_found", id });
4373
4562
  const lock = readTrust(files, paths);
4374
- const chosen = pick(lock, sources, id);
4563
+ const chosen = pick2(lock, sources, id);
4375
4564
  if (chosen === void 0) return reject({ kind: "not_found", id });
4376
4565
  const parsed = parseContract(id, chosen.text);
4377
4566
  if (!parsed.ok) return parsed;
@@ -4425,6 +4614,9 @@ var readBaseline = (runtime, paths) => {
4425
4614
  };
4426
4615
  var writeBaseline = (runtime, paths, treeHash2, checks) => writeJsonFile(runtime, paths.cache(BASELINE_FILE), { treeHash: treeHash2, checks: { ...checks } }).ok;
4427
4616
 
4617
+ // src/gate/diff-shape.ts
4618
+ import { join as join14 } from "node:path";
4619
+
4428
4620
  // src/gate/git.ts
4429
4621
  var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
4430
4622
  var GIT_TIMEOUT_MS = 15e3;
@@ -4524,10 +4716,20 @@ var numstat = (git, base) => {
4524
4716
  };
4525
4717
  var dirtyPaths = (git) => nulFields(git.out(["status", "--porcelain", "-z", "--", ".", EXCLUDE_FM]) ?? "").map((entry2) => entry2.slice(3));
4526
4718
  var aheadOfUpstream = (git) => {
4527
- const count3 = git.out(["rev-list", "--count", "@{u}..HEAD"])?.trim();
4528
- return count3 === void 0 ? void 0 : Number(count3);
4719
+ const count4 = git.out(["rev-list", "--count", "@{u}..HEAD"])?.trim();
4720
+ return count4 === void 0 ? void 0 : Number(count4);
4529
4721
  };
4530
4722
 
4723
+ // src/gate/diff-shape.ts
4724
+ var untrackedLines = (runtime, root, changes) => changes.filter((change) => change.status === "A").reduce((sum, change) => {
4725
+ const text6 = runtime.readText(toAbsolutePath(join14(root, change.path)));
4726
+ return sum + (text6.ok ? text6.value.split("\n").length : 0);
4727
+ }, 0);
4728
+ var diffShape = (runtime, git, base, changes) => ({
4729
+ files: changes.map((change) => change.path),
4730
+ lines: numstat(git, base).lines + untrackedLines(runtime, git.cwd, changes)
4731
+ });
4732
+
4531
4733
  // src/gate/pr-parse.ts
4532
4734
  var FAILED = /* @__PURE__ */ new Set(["FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE", "ERROR"]);
4533
4735
  var PENDING = /* @__PURE__ */ new Set(["IN_PROGRESS", "QUEUED", "PENDING", "WAITING", "REQUESTED", "EXPECTED"]);
@@ -4547,18 +4749,18 @@ var ciChecks = (value) => Array.isArray(value) ? value.flatMap((item) => {
4547
4749
  var parsePrView = (text6) => {
4548
4750
  const parsed = parseJsonObject(text6);
4549
4751
  if (!parsed.ok) return void 0;
4550
- const record2 = parsed.value;
4551
- const number = record2["number"];
4752
+ const record3 = parsed.value;
4753
+ const number = record3["number"];
4552
4754
  if (typeof number !== "number") return void 0;
4553
4755
  return {
4554
4756
  number,
4555
- url: optionalString(record2, "url") ?? "",
4556
- state: optionalString(record2, "state") ?? "",
4557
- mergeable: optionalString(record2, "mergeable") ?? "UNKNOWN",
4558
- mergeStateStatus: optionalString(record2, "mergeStateStatus") ?? "UNKNOWN",
4559
- baseRefName: optionalString(record2, "baseRefName") ?? "",
4560
- headRefOid: optionalString(record2, "headRefOid") ?? "",
4561
- checks: ciChecks(record2["statusCheckRollup"])
4757
+ url: optionalString(record3, "url") ?? "",
4758
+ state: optionalString(record3, "state") ?? "",
4759
+ mergeable: optionalString(record3, "mergeable") ?? "UNKNOWN",
4760
+ mergeStateStatus: optionalString(record3, "mergeStateStatus") ?? "UNKNOWN",
4761
+ baseRefName: optionalString(record3, "baseRefName") ?? "",
4762
+ headRefOid: optionalString(record3, "headRefOid") ?? "",
4763
+ checks: ciChecks(record3["statusCheckRollup"])
4562
4764
  };
4563
4765
  };
4564
4766
  var isConflicting = (pr) => pr.mergeable === "CONFLICTING" || pr.mergeStateStatus === "DIRTY";
@@ -4618,13 +4820,118 @@ var conflictFiles = (git, base) => {
4618
4820
  return [];
4619
4821
  };
4620
4822
 
4823
+ // src/core/glob.ts
4824
+ var escape = (char) => /[.+^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
4825
+ var globToRegExp = (glob) => {
4826
+ const pattern = glob.replace(/^\.\//, "");
4827
+ let out = "";
4828
+ for (let index = 0; index < pattern.length; index += 1) {
4829
+ const char = pattern[index] ?? "";
4830
+ if (char === "*" && pattern[index + 1] === "*") {
4831
+ const slash = pattern[index + 2] === "/";
4832
+ out += slash ? "(?:.*/)?" : ".*";
4833
+ index += slash ? 2 : 1;
4834
+ } else if (char === "*") out += "[^/]*";
4835
+ else if (char === "?") out += "[^/]";
4836
+ else out += escape(char);
4837
+ }
4838
+ return new RegExp(`^${out}${pattern.endsWith("/") ? ".*" : ""}$`);
4839
+ };
4840
+ var matchesAny = (path, globs) => {
4841
+ const normalized = toPosixPath(path);
4842
+ return globs.some((glob) => globToRegExp(glob).test(normalized));
4843
+ };
4844
+
4845
+ // src/gate/size.ts
4846
+ var DEFAULT_SENSITIVE = [
4847
+ "**/*payment*",
4848
+ "**/*payment*/**",
4849
+ "**/*billing*",
4850
+ "**/*billing*/**",
4851
+ "**/*checkout*/**",
4852
+ "**/auth/**",
4853
+ "**/*auth.*",
4854
+ "**/oauth*",
4855
+ "**/migrations/**",
4856
+ "**/migration/**",
4857
+ "**/*.sql",
4858
+ "**/openapi*.*",
4859
+ "**/swagger*.*",
4860
+ "**/*.proto",
4861
+ "**/*.graphql",
4862
+ "**/*stripe*",
4863
+ "**/*openai*",
4864
+ "**/*anthropic*"
4865
+ ];
4866
+ var DEFAULT_SIZE_CONFIG = {
4867
+ m: { files: 5, lines: 150 },
4868
+ l: { files: 20, lines: 600 },
4869
+ sensitive: DEFAULT_SENSITIVE
4870
+ };
4871
+ var count2 = (tree, fallback, ...path) => {
4872
+ const value = configAt(tree, ...path);
4873
+ const parsed = typeof value === "string" ? Number(value) : Number.NaN;
4874
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
4875
+ };
4876
+ var threshold = (tree, key, fallback) => ({
4877
+ files: count2(tree, fallback.files, "size", key, "files"),
4878
+ lines: count2(tree, fallback.lines, "size", key, "lines")
4879
+ });
4880
+ var sizeConfig = (tree) => {
4881
+ const declared = configAt(tree, "size", "sensitive");
4882
+ return {
4883
+ m: threshold(tree, "m", DEFAULT_SIZE_CONFIG.m),
4884
+ l: threshold(tree, "l", DEFAULT_SIZE_CONFIG.l),
4885
+ sensitive: declared === void 0 ? DEFAULT_SENSITIVE : configList(tree, "size", "sensitive")
4886
+ };
4887
+ };
4888
+ var reaches = (shape, limit) => shape.files.length >= limit.files || shape.lines >= limit.lines;
4889
+ var rank = (size) => SIZES.indexOf(size);
4890
+ var larger = (a, b) => rank(a) >= rank(b) ? a : b;
4891
+ var computeSize = (shape, config) => {
4892
+ const sensitive = shape.files.filter((file) => matchesAny(file, config.sensitive));
4893
+ const byVolume = reaches(shape, config.l) ? "L" : reaches(shape, config.m) ? "M" : "S";
4894
+ const computed = sensitive.length > 0 ? larger(byVolume, "M") : byVolume;
4895
+ return { computed, files: shape.files.length, lines: shape.lines, sensitive };
4896
+ };
4897
+ var sizeVerdict = (declared, shape, config) => {
4898
+ const result = computeSize(shape, config);
4899
+ return { ...result, declared, effective: larger(declared, result.computed) };
4900
+ };
4901
+ var upgraded = (verdict) => rank(verdict.effective) > rank(verdict.declared);
4902
+
4903
+ // src/gate/size-contract.ts
4904
+ var REVIEW_CHECK = "size-review";
4905
+ var INVENTORY_CHECK = "size-inventory";
4906
+ var DECISION_CHECK = "size-decision";
4907
+ var INVENTORY = "inventory of the files, behaviours and callers the change touches";
4908
+ var DECISION = "a recorded decision on the approach for a change this large";
4909
+ var reviewChecks = (verdict) => verdict.effective === "S" ? [] : [{ id: REVIEW_CHECK, kind: "review" }];
4910
+ var largeChecks = (verdict) => verdict.effective === "L" && verdict.declared !== "S" ? [
4911
+ { id: INVENTORY_CHECK, kind: "manual", description: INVENTORY },
4912
+ { id: DECISION_CHECK, kind: "manual", description: DECISION }
4913
+ ] : [];
4914
+ var sizeRequirements = (verdict) => [
4915
+ ...reviewChecks(verdict),
4916
+ ...largeChecks(verdict)
4917
+ ];
4918
+ var applySize = (contract, verdict) => {
4919
+ if (contract.implicit) return contract;
4920
+ const declaredIds = new Set(contract.acceptance.map((check2) => check2.id));
4921
+ const declaredKinds = new Set(contract.acceptance.map((check2) => check2.kind));
4922
+ const extra = sizeRequirements(verdict).filter(
4923
+ (check2) => !declaredIds.has(check2.id) && !(check2.kind === "review" && declaredKinds.has("review"))
4924
+ );
4925
+ return extra.length === 0 && !upgraded(verdict) ? contract : { ...contract, size: verdict.effective, acceptance: [...contract.acceptance, ...extra] };
4926
+ };
4927
+
4621
4928
  // src/gate/prepare.ts
4622
4929
  var diskFiles = (runtime, root) => ({
4623
4930
  read: (relative9) => {
4624
- const text6 = runtime.readText(toAbsolutePath(join12(root, relative9)));
4931
+ const text6 = runtime.readText(toAbsolutePath(join15(root, relative9)));
4625
4932
  return text6.ok ? text6.value : void 0;
4626
4933
  },
4627
- exists: (relative9) => runtime.stat(toAbsolutePath(join12(root, relative9))) !== void 0
4934
+ exists: (relative9) => runtime.stat(toAbsolutePath(join15(root, relative9))) !== void 0
4628
4935
  });
4629
4936
  var baseFiles = (git, base, disk) => ({
4630
4937
  read: (relative9) => base === EMPTY_TREE ? disk.read(relative9) : fileAt(git, base, relative9),
@@ -4656,6 +4963,7 @@ var skipReason = (runtime, paths, loaded, changes, tree, sessionId) => {
4656
4963
  const session = readSession(runtime, paths);
4657
4964
  return session !== void 0 && session.sessionId === sessionId && session.startTree === tree ? "no_session_edits" : void 0;
4658
4965
  };
4966
+ var measureSize = (runtime, git, base, changes, contract) => sizeVerdict(contract.size, diffShape(runtime, git, base, changes), sizeConfig(loadProjectConfig(runtime, git.cwd)));
4659
4967
  var prepareGate = (options) => {
4660
4968
  const probe = gitAt(options.runtime, options.cwd);
4661
4969
  const root = gitRoot(probe);
@@ -4675,11 +4983,13 @@ var prepareGate = (options) => {
4675
4983
  const skip = options.noSkip === true ? void 0 : skipReason(options.runtime, paths, loaded.value, changes, tree, options.sessionId);
4676
4984
  if (skip !== void 0) return { kind: "skipped", reason: skip };
4677
4985
  const pr = memo(() => prStatus(options.runtime, git, paths));
4986
+ const size = measureSize(options.runtime, git, base, changes, loaded.value.contract);
4678
4987
  const ctx = {
4679
4988
  runtime: options.runtime,
4680
4989
  git,
4681
4990
  paths,
4682
- contract: loaded.value.contract,
4991
+ contract: applySize(loaded.value.contract, size),
4992
+ size,
4683
4993
  trusted: loaded.value.trusted,
4684
4994
  treeHash: tree,
4685
4995
  base,
@@ -4883,19 +5193,19 @@ var params = (value) => isRecord(value) ? Object.fromEntries(
4883
5193
  var notes = (value) => Array.isArray(value) ? value.flatMap(
4884
5194
  (item) => isRecord(item) && typeof item["key"] === "string" ? [{ key: item["key"], params: params(item["params"]) }] : []
4885
5195
  ) : [];
4886
- var text3 = (record2, key) => {
4887
- const value = record2[key];
5196
+ var text3 = (record3, key) => {
5197
+ const value = record3[key];
4888
5198
  return typeof value === "string" ? value : void 0;
4889
5199
  };
4890
- var optional = (record2) => {
4891
- const exitCode = record2["exitCode"];
4892
- const durationMs = record2["durationMs"];
4893
- const command = text3(record2, "command");
4894
- const stdoutSha256 = text3(record2, "stdoutSha256");
4895
- const releasedBy = text3(record2, "releasedBy");
4896
- const reason = oneOf2(UNVERIFIED_REASONS, record2["reason"]);
4897
- const failures = record2["failures"];
4898
- const testCount = record2["testCount"];
5200
+ var optional = (record3) => {
5201
+ const exitCode = record3["exitCode"];
5202
+ const durationMs = record3["durationMs"];
5203
+ const command = text3(record3, "command");
5204
+ const stdoutSha256 = text3(record3, "stdoutSha256");
5205
+ const releasedBy = text3(record3, "releasedBy");
5206
+ const reason = oneOf2(UNVERIFIED_REASONS, record3["reason"]);
5207
+ const failures = record3["failures"];
5208
+ const testCount = record3["testCount"];
4899
5209
  return {
4900
5210
  ...command === void 0 ? {} : { command },
4901
5211
  ...typeof exitCode === "number" || exitCode === null ? { exitCode } : {},
@@ -4910,14 +5220,14 @@ var optional = (record2) => {
4910
5220
  var parseEvidence = (line2) => {
4911
5221
  const parsed = parseJsonObject(line2);
4912
5222
  if (!parsed.ok) return void 0;
4913
- const record2 = parsed.value;
4914
- const status = oneOf2(EVIDENCE_STATUSES, record2["status"]);
4915
- const task = text3(record2, "task");
4916
- const check2 = text3(record2, "check");
4917
- const kind = text3(record2, "kind");
4918
- const treeHash2 = text3(record2, "treeHash");
4919
- const at = text3(record2, "at");
4920
- if (record2["schema"] !== EVIDENCE_SCHEMA || status === void 0) return void 0;
5223
+ const record3 = parsed.value;
5224
+ const status = oneOf2(EVIDENCE_STATUSES, record3["status"]);
5225
+ const task = text3(record3, "task");
5226
+ const check2 = text3(record3, "check");
5227
+ const kind = text3(record3, "kind");
5228
+ const treeHash2 = text3(record3, "treeHash");
5229
+ const at = text3(record3, "at");
5230
+ if (record3["schema"] !== EVIDENCE_SCHEMA || status === void 0) return void 0;
4921
5231
  if (task === void 0 || check2 === void 0 || kind === void 0 || treeHash2 === void 0 || at === void 0) {
4922
5232
  return void 0;
4923
5233
  }
@@ -4929,8 +5239,8 @@ var parseEvidence = (line2) => {
4929
5239
  status,
4930
5240
  treeHash: treeHash2,
4931
5241
  at,
4932
- notes: notes(record2["notes"]),
4933
- ...optional(record2)
5242
+ notes: notes(record3["notes"]),
5243
+ ...optional(record3)
4934
5244
  };
4935
5245
  };
4936
5246
  var readLedger = (runtime, path) => {
@@ -5458,7 +5768,26 @@ var en_default = {
5458
5768
  "history.pattern.no_verify": "{count, plural, one {# git command} other {# git commands}} with --no-verify",
5459
5769
  "history.pattern.force_push": "{count, plural, one {# force push} other {# force pushes}}",
5460
5770
  "history.pattern.test_skip": "{count, plural, one {# test edit} other {# test edits}} that added .skip, .only or a skip marker",
5461
- "history.estimate": "These are estimates from pattern matching, not evidence; nothing was saved and nothing left this machine."
5771
+ "history.estimate": "These are estimates from pattern matching, not evidence; nothing was saved and nothing left this machine.",
5772
+ "gate.require.review": "a reviewer subagent read the change at its current state",
5773
+ "gate.size.declared": "size {effective}: {files, plural, one {# file} other {# files}}, {lines, plural, one {# line} other {# lines}}",
5774
+ "gate.size.grew": "declared {declared}, the diff makes it {effective}: {files, plural, one {# file} other {# files}}, {lines, plural, one {# line} other {# lines}}",
5775
+ "gate.size.sensitive": "sensitive path touched: {file}",
5776
+ "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",
5777
+ "gate.review.done": "reviewed by {agent} at {at}",
5778
+ "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.",
5779
+ "gate.secrets.warning": "warning: {rule} at {file}:{line}",
5780
+ "doctor.secrets.remote_credential": "git remote {remotes} has a credential in its URL, stored in plain text in .git/config",
5781
+ "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",
5782
+ "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.",
5783
+ "gate.require.callers": "no removed or renamed symbol is still called",
5784
+ "gate.callers.none": "{count, plural, one {# removed definition} other {# removed definitions}}, no caller left",
5785
+ "gate.callers.open": "{name} is gone but still called in {count, plural, one {# place} other {# places}}",
5786
+ "gate.callers.at": "{name} called at {file}:{line}",
5787
+ "gate.callers.warning": "warning only: {percent}% of callers findings were released with callers_ignore ({released} of {total}), above the 5% ceiling",
5788
+ "command.policy_false_positive.summary": "Record a false positive of a guard in shadow so it is not promoted",
5789
+ "policy.error.person_only": "Only the person can record a false positive, and this does not look like their terminal (no TTY, or running inside Claude Code)",
5790
+ "policy.false_positive.recorded": "Recorded for {guard}: {count, plural, one {# false positive} other {# false positives}}; it stays in shadow until you set it in .finitemonkey/policy.yml."
5462
5791
  };
5463
5792
 
5464
5793
  // src/i18n/es.json
@@ -5971,7 +6300,26 @@ var es_default = {
5971
6300
  "history.pattern.no_verify": "{count, plural, one {# comando git} other {# comandos git}} con --no-verify",
5972
6301
  "history.pattern.force_push": "{count, plural, one {# push forzado} other {# push forzados}}",
5973
6302
  "history.pattern.test_skip": "{count, plural, one {# edición} other {# ediciones}} de pruebas que añadieron .skip, .only o una marca para saltar",
5974
- "history.estimate": "Son estimaciones por patrones de texto, no evidencia; no se guardó nada y nada salió de esta máquina."
6303
+ "history.estimate": "Son estimaciones por patrones de texto, no evidencia; no se guardó nada y nada salió de esta máquina.",
6304
+ "gate.require.review": "un subagente revisor leyó el cambio en su estado actual",
6305
+ "gate.size.declared": "tamaño {effective}: {files, plural, one {# archivo} other {# archivos}}, {lines, plural, one {# línea} other {# líneas}}",
6306
+ "gate.size.grew": "declarada {declared}, el diff la vuelve {effective}: {files, plural, one {# archivo} other {# archivos}}, {lines, plural, one {# línea} other {# líneas}}",
6307
+ "gate.size.sensitive": "ruta sensible tocada: {file}",
6308
+ "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",
6309
+ "gate.review.done": "revisado por {agent} en {at}",
6310
+ "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.",
6311
+ "gate.secrets.warning": "aviso: {rule} en {file}:{line}",
6312
+ "doctor.secrets.remote_credential": "el remoto git {remotes} tiene una credencial en la URL, guardada en texto plano en .git/config",
6313
+ "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",
6314
+ "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.",
6315
+ "gate.require.callers": "ningún símbolo eliminado o renombrado sigue siendo llamado",
6316
+ "gate.callers.none": "{count, plural, one {# definición eliminada} other {# definiciones eliminadas}}, ningún llamador quedó",
6317
+ "gate.callers.open": "{name} ya no existe, pero se llama en {count, plural, one {# lugar} other {# lugares}}",
6318
+ "gate.callers.at": "{name} llamado en {file}:{line}",
6319
+ "gate.callers.warning": "solo aviso: {percent}% de los hallazgos de callers se liberaron con callers_ignore ({released} de {total}), por encima del techo de 5%",
6320
+ "command.policy_false_positive.summary": "Registra un falso positivo de un guard en shadow para que no se promueva",
6321
+ "policy.error.person_only": "Solo la persona puede registrar un falso positivo, y esto no parece su terminal (sin TTY, o dentro de Claude Code)",
6322
+ "policy.false_positive.recorded": "Registrado para {guard}: {count, plural, one {# falso positivo} other {# falsos positivos}}; se queda en shadow hasta que fijes el modo en .finitemonkey/policy.yml."
5975
6323
  };
5976
6324
 
5977
6325
  // src/i18n/pt-BR.json
@@ -6484,7 +6832,26 @@ var pt_BR_default = {
6484
6832
  "history.pattern.no_verify": "{count, plural, one {# comando git} other {# comandos git}} com --no-verify",
6485
6833
  "history.pattern.force_push": "{count, plural, one {# push forçado} other {# pushes forçados}}",
6486
6834
  "history.pattern.test_skip": "{count, plural, one {# edição} other {# edições}} de teste que acrescentaram .skip, .only ou marcador de pular",
6487
- "history.estimate": "São estimativas por padrão de texto, não evidência; nada foi salvo e nada saiu desta máquina."
6835
+ "history.estimate": "São estimativas por padrão de texto, não evidência; nada foi salvo e nada saiu desta máquina.",
6836
+ "gate.require.review": "um subagente revisor leu a mudança no estado atual",
6837
+ "gate.size.declared": "tamanho {effective}: {files, plural, one {# arquivo} other {# arquivos}}, {lines, plural, one {# linha} other {# linhas}}",
6838
+ "gate.size.grew": "declarada {declared}, o diff a torna {effective}: {files, plural, one {# arquivo} other {# arquivos}}, {lines, plural, one {# linha} other {# linhas}}",
6839
+ "gate.size.sensitive": "caminho sensível alterado: {file}",
6840
+ "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",
6841
+ "gate.review.done": "revisado por {agent} em {at}",
6842
+ "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.",
6843
+ "gate.secrets.warning": "aviso: {rule} em {file}:{line}",
6844
+ "doctor.secrets.remote_credential": "o remoto git {remotes} tem uma credencial na URL, guardada em texto puro no .git/config",
6845
+ "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",
6846
+ "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.",
6847
+ "gate.require.callers": "nenhum símbolo removido ou renomeado continua sendo chamado",
6848
+ "gate.callers.none": "{count, plural, one {# definição removida} other {# definições removidas}}, nenhum chamador sobrou",
6849
+ "gate.callers.open": "{name} saiu, mas é chamado em {count, plural, one {# lugar} other {# lugares}}",
6850
+ "gate.callers.at": "{name} chamado em {file}:{line}",
6851
+ "gate.callers.warning": "só aviso: {percent}% dos achados de callers foram liberados com callers_ignore ({released} de {total}), acima do teto de 5%",
6852
+ "command.policy_false_positive.summary": "Registra um falso positivo de um guard em shadow para ele não ser promovido",
6853
+ "policy.error.person_only": "Só a pessoa pode registrar um falso positivo, e isto não parece o terminal dela (sem TTY, ou rodando dentro do Claude Code)",
6854
+ "policy.false_positive.recorded": "Registrado para {guard}: {count, plural, one {# falso positivo} other {# falsos positivos}}; ele fica em shadow até você definir o modo em .finitemonkey/policy.yml."
6488
6855
  };
6489
6856
 
6490
6857
  // src/i18n/review.json
@@ -6998,7 +7365,26 @@ var review_default = {
6998
7365
  "history.pattern.no_verify": "machine",
6999
7366
  "history.pattern.force_push": "machine",
7000
7367
  "history.pattern.test_skip": "machine",
7001
- "history.estimate": "machine"
7368
+ "history.estimate": "machine",
7369
+ "gate.require.review": "machine",
7370
+ "gate.size.declared": "machine",
7371
+ "gate.size.grew": "machine",
7372
+ "gate.size.sensitive": "machine",
7373
+ "gate.review.missing": "machine",
7374
+ "gate.review.done": "machine",
7375
+ "secrets.warn.write": "machine",
7376
+ "gate.secrets.warning": "machine",
7377
+ "doctor.secrets.remote_credential": "machine",
7378
+ "doctor.secrets.fix_remote_credential": "machine",
7379
+ "secrets.session.remote_credential": "machine",
7380
+ "gate.require.callers": "machine",
7381
+ "gate.callers.none": "machine",
7382
+ "gate.callers.open": "machine",
7383
+ "gate.callers.at": "machine",
7384
+ "gate.callers.warning": "machine",
7385
+ "command.policy_false_positive.summary": "machine",
7386
+ "policy.error.person_only": "machine",
7387
+ "policy.false_positive.recorded": "machine"
7002
7388
  },
7003
7389
  es: {
7004
7390
  "command.init.summary": "machine",
@@ -7509,7 +7895,26 @@ var review_default = {
7509
7895
  "history.pattern.no_verify": "machine",
7510
7896
  "history.pattern.force_push": "machine",
7511
7897
  "history.pattern.test_skip": "machine",
7512
- "history.estimate": "machine"
7898
+ "history.estimate": "machine",
7899
+ "gate.require.review": "machine",
7900
+ "gate.size.declared": "machine",
7901
+ "gate.size.grew": "machine",
7902
+ "gate.size.sensitive": "machine",
7903
+ "gate.review.missing": "machine",
7904
+ "gate.review.done": "machine",
7905
+ "secrets.warn.write": "machine",
7906
+ "gate.secrets.warning": "machine",
7907
+ "doctor.secrets.remote_credential": "machine",
7908
+ "doctor.secrets.fix_remote_credential": "machine",
7909
+ "secrets.session.remote_credential": "machine",
7910
+ "gate.require.callers": "machine",
7911
+ "gate.callers.none": "machine",
7912
+ "gate.callers.open": "machine",
7913
+ "gate.callers.at": "machine",
7914
+ "gate.callers.warning": "machine",
7915
+ "command.policy_false_positive.summary": "machine",
7916
+ "policy.error.person_only": "machine",
7917
+ "policy.false_positive.recorded": "machine"
7513
7918
  }
7514
7919
  };
7515
7920
 
@@ -7632,7 +8037,7 @@ var buildReport = (input) => {
7632
8037
  );
7633
8038
  return fromEvidence(check2.id, check2.kind, entry2, hasStale(ledger, check2.id, treeHash2));
7634
8039
  });
7635
- const count3 = (status) => items.filter((item) => item.status === status).length;
8040
+ const count4 = (status) => items.filter((item) => item.status === status).length;
7636
8041
  return {
7637
8042
  schema: GATE_SCHEMA,
7638
8043
  task: contract.id,
@@ -7643,7 +8048,7 @@ var buildReport = (input) => {
7643
8048
  source: input.source,
7644
8049
  contractEdited: input.contractEdited,
7645
8050
  verdict: verdictOf(items),
7646
- counts: { passed: count3("passed"), failed: count3("failed"), unverified: count3("unverified") },
8051
+ counts: { passed: count4("passed"), failed: count4("failed"), unverified: count4("unverified") },
7647
8052
  items,
7648
8053
  decisionsNeeded: contract.decisionsNeeded,
7649
8054
  fastPath: input.fastPath
@@ -7767,6 +8172,277 @@ var preCompact = (input) => {
7767
8172
  return { kind: "allow" };
7768
8173
  };
7769
8174
 
8175
+ // src/gate/review.ts
8176
+ var REVIEWS_FILE = "reviews.json";
8177
+ var REVIEWER_AGENT = /(^|:)[\w-]*reviewer$/i;
8178
+ var KEEP = 50;
8179
+ var toReview = (value) => {
8180
+ if (!isRecord(value)) return void 0;
8181
+ const { treeHash: treeHash2, agent, at } = value;
8182
+ return typeof treeHash2 === "string" && typeof agent === "string" && typeof at === "string" ? { treeHash: treeHash2, agent, at } : void 0;
8183
+ };
8184
+ var readReviews = (runtime, paths) => {
8185
+ const raw = readJsonFile(runtime, paths.cache(REVIEWS_FILE))?.["reviews"];
8186
+ return Array.isArray(raw) ? raw.flatMap((item) => {
8187
+ const review2 = toReview(item);
8188
+ return review2 === void 0 ? [] : [review2];
8189
+ }) : [];
8190
+ };
8191
+ var recordReview = (runtime, paths, treeHash2, agent) => {
8192
+ const reviews = [...readReviews(runtime, paths), { treeHash: treeHash2, agent, at: runtime.now() }].slice(-KEEP);
8193
+ return writeJsonFile(runtime, paths.cache(REVIEWS_FILE), { reviews: reviews.map((review2) => ({ ...review2 })) }).ok;
8194
+ };
8195
+
8196
+ // src/gate/callers/rate.ts
8197
+ var FLAGGED_EVENT = "callers.flagged";
8198
+ var RELEASED_EVENT = "callers.released";
8199
+ var MIN_SAMPLE = 20;
8200
+ var MAX_FALSE_POSITIVE = 0.05;
8201
+ var pairs = (record3) => {
8202
+ const { task, names } = record3;
8203
+ if (typeof task !== "string" || !Array.isArray(names)) return [];
8204
+ return names.filter((name) => typeof name === "string").map((name) => `${task}\0${name}`);
8205
+ };
8206
+ var callersRate = (runtime, paths) => {
8207
+ const text6 = runtime.readText(paths.log);
8208
+ const flagged = /* @__PURE__ */ new Set();
8209
+ const released2 = /* @__PURE__ */ new Set();
8210
+ for (const line2 of text6.ok ? text6.value.split("\n") : []) {
8211
+ const parsed = line2.trim() === "" ? void 0 : parseJsonObject(line2);
8212
+ if (parsed === void 0 || !parsed.ok || !isRecord(parsed.value)) continue;
8213
+ const event = parsed.value["event"];
8214
+ const target = event === FLAGGED_EVENT ? flagged : event === RELEASED_EVENT ? released2 : void 0;
8215
+ if (target !== void 0) for (const pair of pairs(parsed.value)) target.add(pair);
8216
+ }
8217
+ const total = (/* @__PURE__ */ new Set([...flagged, ...released2])).size;
8218
+ const rate = total === 0 ? 0 : released2.size / total;
8219
+ return {
8220
+ flagged: total,
8221
+ released: released2.size,
8222
+ rate,
8223
+ downgraded: total >= MIN_SAMPLE && rate > MAX_FALSE_POSITIVE
8224
+ };
8225
+ };
8226
+
8227
+ // src/gate/callers/scan.ts
8228
+ import { join as join16 } from "node:path";
8229
+
8230
+ // src/gate/callers/imports.ts
8231
+ import { posix as posix2 } from "node:path";
8232
+ var JS_SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(?\s*|\brequire\s*\(\s*)["']([^"']+)["']/g;
8233
+ var JS_EXTENSION = /\.(?:[cm]?[jt]sx?)$/;
8234
+ var stripModule = (path) => path.replace(JS_EXTENSION, "").replace(/\/index$/, "");
8235
+ var jsSpecifiers = (text6) => [...text6.matchAll(JS_SPECIFIER)].map((match) => match[1] ?? "");
8236
+ var jsImports = (callerPath, callerText, origin) => {
8237
+ const target = stripModule(origin);
8238
+ return jsSpecifiers(callerText).some((specifier) => {
8239
+ if (specifier.startsWith(".")) {
8240
+ return stripModule(posix2.normalize(posix2.join(posix2.dirname(callerPath), specifier))) === target;
8241
+ }
8242
+ const bare = stripModule(specifier.replace(/^[@~#]\//, ""));
8243
+ return target === bare || target.endsWith(`/${bare}`);
8244
+ });
8245
+ };
8246
+ var pyImports = (callerText, origin) => {
8247
+ const stem = posix2.basename(origin).replace(/\.py$/, "");
8248
+ const module = stem === "__init__" ? posix2.basename(posix2.dirname(origin)) : stem;
8249
+ const word2 = new RegExp(`\\b${module}\\b`);
8250
+ return callerText.split(/\r?\n/).some((line2) => /^\s*(?:from|import)\s/.test(line2) && word2.test(line2));
8251
+ };
8252
+ var goImports = (callerPath, callerText, origin) => {
8253
+ const dir = posix2.dirname(origin);
8254
+ if (posix2.dirname(callerPath) === dir) return true;
8255
+ const last = posix2.basename(dir);
8256
+ return new RegExp(`"[^"]*\\b${last}"`).test(callerText);
8257
+ };
8258
+ var importsOrigin = (language, callerPath, callerText, origins) => origins.some((origin) => {
8259
+ if (callerPath === origin) return true;
8260
+ switch (language.imports) {
8261
+ case "none":
8262
+ return true;
8263
+ case "package":
8264
+ return goImports(callerPath, callerText, origin);
8265
+ case "module":
8266
+ return language.id === "py" ? pyImports(callerText, origin) : jsImports(callerPath, callerText, origin);
8267
+ }
8268
+ });
8269
+
8270
+ // src/gate/callers/languages.ts
8271
+ import { extname } from "node:path";
8272
+ var JS_NAME = "[A-Za-z_$][\\w$]*";
8273
+ var LANGUAGES2 = [
8274
+ {
8275
+ id: "js",
8276
+ extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"],
8277
+ definitions: [
8278
+ new RegExp(`^\\s*(?:export\\s+)?(?:default\\s+)?(?:async\\s+)?function\\s*\\*?\\s*(${JS_NAME})`),
8279
+ new RegExp(`^\\s*(?:export\\s+)?(?:default\\s+)?(?:abstract\\s+)?class\\s+(${JS_NAME})`),
8280
+ new RegExp(`^\\s*export\\s+(?:declare\\s+)?(?:const|let|var|enum|interface|type)\\s+(${JS_NAME})`),
8281
+ new RegExp(
8282
+ `^\\s+(?:public\\s+)?(?:static\\s+)?(?:async\\s+)?(${JS_NAME})\\s*\\([^)]*\\)\\s*(?::[^{=]+)?\\{\\s*$`
8283
+ )
8284
+ ],
8285
+ imports: "module",
8286
+ comment: /^\s*(?:\/\/|\/\*|\*)/
8287
+ },
8288
+ {
8289
+ id: "py",
8290
+ extensions: [".py"],
8291
+ definitions: [/^\s*(?:async\s+)?def\s+([A-Za-z]\w*)/, /^\s*class\s+([A-Za-z]\w*)/],
8292
+ imports: "module",
8293
+ comment: /^\s*#/
8294
+ },
8295
+ {
8296
+ id: "go",
8297
+ extensions: [".go"],
8298
+ definitions: [/^func\s+(?:\([^)]*\)\s*)?([A-Za-z]\w*)/, /^type\s+([A-Za-z]\w*)\s+(?:struct|interface)/],
8299
+ imports: "package",
8300
+ comment: /^\s*\/\//
8301
+ },
8302
+ {
8303
+ id: "rb",
8304
+ extensions: [".rb"],
8305
+ definitions: [/^\s*def\s+(?:self\.)?([A-Za-z]\w*[?!]?)/, /^\s*(?:class|module)\s+([A-Z]\w*)/],
8306
+ imports: "none",
8307
+ comment: /^\s*#/
8308
+ },
8309
+ {
8310
+ id: "rs",
8311
+ extensions: [".rs"],
8312
+ definitions: [
8313
+ /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([A-Za-z]\w*)/,
8314
+ /^\s*(?:pub\s+)?(?:struct|enum|trait)\s+([A-Z]\w*)/
8315
+ ],
8316
+ imports: "none",
8317
+ comment: /^\s*\/\//
8318
+ }
8319
+ ];
8320
+ var KEYWORDS = /* @__PURE__ */ new Set([
8321
+ "if",
8322
+ "for",
8323
+ "while",
8324
+ "switch",
8325
+ "catch",
8326
+ "function",
8327
+ "return",
8328
+ "constructor",
8329
+ "with",
8330
+ "do",
8331
+ "else"
8332
+ ]);
8333
+ var languageOf = (path) => {
8334
+ const extension = extname(path).toLowerCase();
8335
+ return LANGUAGES2.find((language) => language.extensions.includes(extension));
8336
+ };
8337
+ var definitionsIn = (language, text6) => {
8338
+ const names = /* @__PURE__ */ new Set();
8339
+ for (const line2 of text6.split(/\r?\n/)) {
8340
+ for (const pattern of language.definitions) {
8341
+ const name = pattern.exec(line2)?.[1];
8342
+ if (name !== void 0 && !KEYWORDS.has(name)) names.add(name);
8343
+ }
8344
+ }
8345
+ return names;
8346
+ };
8347
+ var definesName = (language, line2, name) => language.definitions.some((pattern) => pattern.exec(line2)?.[1] === name);
8348
+
8349
+ // src/gate/callers/scan.ts
8350
+ var MAX_SYMBOLS = 200;
8351
+ var current = (runtime, root, path) => {
8352
+ const text6 = runtime.readText(toAbsolutePath(join16(root, path)));
8353
+ return text6.ok ? text6.value : "";
8354
+ };
8355
+ var keyOf = (language, name) => `${language.id}\0${name}`;
8356
+ var redefinedNames = (runtime, git, changes) => {
8357
+ const keys = /* @__PURE__ */ new Set();
8358
+ for (const change of changes) {
8359
+ const language = languageOf(change.path);
8360
+ if (language === void 0 || change.status === "D") continue;
8361
+ for (const name of definitionsIn(language, current(runtime, git.cwd, change.path))) keys.add(keyOf(language, name));
8362
+ }
8363
+ return keys;
8364
+ };
8365
+ var removedSymbols = (runtime, git, base, changes) => {
8366
+ const redefined = redefinedNames(runtime, git, changes);
8367
+ const found = /* @__PURE__ */ new Map();
8368
+ for (const change of changes) {
8369
+ const language = languageOf(change.oldPath ?? change.path);
8370
+ if (language === void 0 || change.status === "A") continue;
8371
+ const before = definitionsIn(language, fileAt(git, base, change.oldPath ?? change.path) ?? "");
8372
+ const origins = [change.path, ...change.oldPath === void 0 ? [] : [change.oldPath]];
8373
+ for (const name of before) {
8374
+ const key = keyOf(language, name);
8375
+ if (redefined.has(key)) continue;
8376
+ const prior = found.get(key);
8377
+ found.set(key, { name, language, origins: [...prior?.origins ?? [], ...origins] });
8378
+ }
8379
+ }
8380
+ return [...found.values()].slice(0, MAX_SYMBOLS);
8381
+ };
8382
+ var parseGrep = (output) => output.split("\n").filter((row) => row !== "").flatMap((row) => {
8383
+ const [file = "", line2 = "", ...rest] = row.split("\0");
8384
+ const number = Number(line2);
8385
+ return file === "" || !Number.isInteger(number) ? [] : [{ file: toPosixPath(file), line: number, text: rest.join("\0") }];
8386
+ });
8387
+ var grep = (git, names) => {
8388
+ if (names.length === 0) return [];
8389
+ const patterns = names.flatMap((name) => ["-e", name]);
8390
+ return parseGrep(
8391
+ git.out(["grep", "-n", "-z", "-w", "-I", "--untracked", "--no-color", ...patterns, "--", ".", EXCLUDE_FM]) ?? ""
8392
+ );
8393
+ };
8394
+ var mentions = (text6, name) => new RegExp(`(?<![\\w$])${name.replace(/[$?!]/g, "\\$&")}(?![\\w$])`).test(text6);
8395
+ var callSites = (runtime, git, symbols) => {
8396
+ const hits = grep(git, [...new Set(symbols.map((symbol) => symbol.name))]);
8397
+ const texts = /* @__PURE__ */ new Map();
8398
+ const textOf4 = (file) => {
8399
+ const cached2 = texts.get(file) ?? current(runtime, git.cwd, file);
8400
+ texts.set(file, cached2);
8401
+ return cached2;
8402
+ };
8403
+ return symbols.flatMap(
8404
+ (symbol) => hits.filter((hit3) => languageOf(hit3.file)?.id === symbol.language.id && mentions(hit3.text, symbol.name)).filter((hit3) => !symbol.language.comment.test(hit3.text) && !definesName(symbol.language, hit3.text, symbol.name)).filter((hit3) => importsOrigin(symbol.language, hit3.file, textOf4(hit3.file), symbol.origins)).map((hit3) => ({ name: symbol.name, file: hit3.file, line: hit3.line }))
8405
+ );
8406
+ };
8407
+
8408
+ // src/gate/checks/callers.ts
8409
+ var MAX_SITES = 10;
8410
+ var byName = (sites) => {
8411
+ const grouped = /* @__PURE__ */ new Map();
8412
+ for (const site of sites) grouped.set(site.name, [...grouped.get(site.name) ?? [], site]);
8413
+ return grouped;
8414
+ };
8415
+ var siteNotes = (open) => [
8416
+ ...[...open].map(([name, sites]) => note("gate.callers.open", { name, count: sites.length })),
8417
+ ...[...open.values()].flat().slice(0, MAX_SITES).map((site) => note("gate.callers.at", { name: site.name, file: site.file, line: site.line }))
8418
+ ];
8419
+ var record = (ctx, event, names) => {
8420
+ if (names.length > 0) logEvent(ctx.runtime, ctx.paths, event, { task: ctx.contract.id, names: [...names] });
8421
+ };
8422
+ var callersCheck = (_check, ctx) => {
8423
+ const symbols = removedSymbols(ctx.runtime, ctx.git, ctx.base, ctx.changes);
8424
+ const grouped = byName(callSites(ctx.runtime, ctx.git, symbols));
8425
+ const ignore = new Set(ctx.contract.callersIgnore ?? []);
8426
+ const open = new Map([...grouped].filter(([name]) => !ignore.has(name)));
8427
+ record(
8428
+ ctx,
8429
+ RELEASED_EVENT,
8430
+ [...grouped.keys()].filter((name) => ignore.has(name))
8431
+ );
8432
+ record(ctx, FLAGGED_EVENT, [...open.keys()]);
8433
+ if (open.size === 0) return passed([note("gate.callers.none", { count: symbols.length })]);
8434
+ const rate = callersRate(ctx.runtime, ctx.paths);
8435
+ if (!rate.downgraded) return failed(siteNotes(open));
8436
+ return passed([
8437
+ note("gate.callers.warning", {
8438
+ percent: Math.round(rate.rate * 100),
8439
+ released: rate.released,
8440
+ total: rate.flagged
8441
+ }),
8442
+ ...siteNotes(open)
8443
+ ]);
8444
+ };
8445
+
7770
8446
  // src/gate/test-summary.ts
7771
8447
  var ANSI2 = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
7772
8448
  var COUNTERS = [
@@ -7906,44 +8582,14 @@ var runShellCheck = (check2, run4, ctx) => {
7906
8582
  };
7907
8583
  var commandCheck = (check2, ctx) => check2.run === void 0 ? unverified("not_implemented") : runShellCheck(check2, check2.run, ctx);
7908
8584
 
7909
- // src/gate/checks/diff.ts
7910
- import { join as join13 } from "node:path";
7911
-
7912
- // src/core/glob.ts
7913
- var escape = (char) => /[.+^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
7914
- var globToRegExp = (glob) => {
7915
- const pattern = glob.replace(/^\.\//, "");
7916
- let out = "";
7917
- for (let index = 0; index < pattern.length; index += 1) {
7918
- const char = pattern[index] ?? "";
7919
- if (char === "*" && pattern[index + 1] === "*") {
7920
- const slash = pattern[index + 2] === "/";
7921
- out += slash ? "(?:.*/)?" : ".*";
7922
- index += slash ? 2 : 1;
7923
- } else if (char === "*") out += "[^/]*";
7924
- else if (char === "?") out += "[^/]";
7925
- else out += escape(char);
7926
- }
7927
- return new RegExp(`^${out}${pattern.endsWith("/") ? ".*" : ""}$`);
7928
- };
7929
- var matchesAny = (path, globs) => {
7930
- const normalized = toPosixPath(path);
7931
- return globs.some((glob) => globToRegExp(glob).test(normalized));
7932
- };
7933
-
7934
8585
  // src/gate/checks/diff.ts
7935
8586
  var MAX_LISTED = 10;
7936
- var untrackedLines = (ctx) => ctx.changes.filter((change) => change.status === "A").reduce((sum, change) => {
7937
- const text6 = ctx.runtime.readText(toAbsolutePath(join13(ctx.paths.root, change.path)));
7938
- return sum + (text6.ok ? text6.value.split("\n").length : 0);
7939
- }, 0);
7940
8587
  var diffCheck = (_check, ctx) => {
7941
8588
  const { paths, diffBudget } = ctx.contract.scope;
7942
8589
  if (paths.length === 0 && diffBudget === void 0) return passed([note("gate.diff.no_scope")]);
7943
8590
  const outside = paths.length === 0 ? [] : ctx.changes.filter((change) => !matchesAny(change.path, paths));
7944
- const stat = numstat(ctx.git, ctx.base);
7945
8591
  const files = ctx.changes.length;
7946
- const lines3 = stat.lines + untrackedLines(ctx);
8592
+ const lines3 = diffShape(ctx.runtime, ctx.git, ctx.base, ctx.changes).lines;
7947
8593
  const notes2 = [
7948
8594
  ...outside.slice(0, MAX_LISTED).map((change) => note("gate.diff.outside", { file: change.path })),
7949
8595
  ...diffBudget?.files !== void 0 && files > diffBudget.files ? [note("gate.diff.too_many_files", { count: files, max: diffBudget.files })] : [],
@@ -7993,13 +8639,13 @@ var manualCheck = (check2, ctx) => {
7993
8639
  };
7994
8640
 
7995
8641
  // src/gate/checks/placeholders.ts
7996
- import { join as join14 } from "node:path";
8642
+ import { join as join17 } from "node:path";
7997
8643
  var MAX_LISTED2 = 10;
7998
8644
  var SKIP_FILES = /\.(md|mdx|txt|rst|lock|snap|svg|png|jpg|gif|json)$|(^|\/)(CHANGELOG|LICENSE)/i;
7999
8645
  var PLACEHOLDER = /\b(TODO|FIXME|XXX)\b|not implemented|NotImplementedError|unimplemented!|todo!\(|lorem ipsum/i;
8000
8646
  var findPlaceholders = (lines3) => lines3.filter((line2) => !isTestFile(line2.path) && !SKIP_FILES.test(line2.path) && PLACEHOLDER.test(line2.text));
8001
8647
  var untrackedAdded = (ctx) => ctx.changes.filter((change) => change.status === "A" && !SKIP_FILES.test(change.path)).flatMap((change) => {
8002
- const text6 = ctx.runtime.readText(toAbsolutePath(join14(ctx.paths.root, change.path)));
8648
+ const text6 = ctx.runtime.readText(toAbsolutePath(join17(ctx.paths.root, change.path)));
8003
8649
  return text6.ok ? text6.value.split("\n").map((line2, index) => ({ path: change.path, line: index + 1, text: line2 })) : [];
8004
8650
  });
8005
8651
  var placeholdersCheck = (_check, ctx) => {
@@ -8511,10 +9157,10 @@ var hit = (rule, units, index, weight) => {
8511
9157
  return { rule, unit: index, line: unit?.line ?? 0, weight, excerpt: unit?.text ?? "" };
8512
9158
  };
8513
9159
  var isProse = (unit) => unit?.kind === "sentence";
8514
- var samePara = (units, from, count3) => {
9160
+ var samePara = (units, from, count4) => {
8515
9161
  const first = units[from];
8516
9162
  if (!isProse(first)) return false;
8517
- return units.slice(from, from + count3).every((unit) => isProse(unit) && unit.paragraph === first.paragraph);
9163
+ return units.slice(from, from + count4).every((unit) => isProse(unit) && unit.paragraph === first.paragraph);
8518
9164
  };
8519
9165
  var runsOf = (units, size, matches) => {
8520
9166
  const starts = [];
@@ -8571,13 +9217,13 @@ var emoji = (units) => units.flatMap(
8571
9217
  var dashes = (units) => {
8572
9218
  const perParagraph = /* @__PURE__ */ new Map();
8573
9219
  for (const unit of units) {
8574
- const count3 = unit.text.split("—").length - 1;
8575
- perParagraph.set(unit.paragraph, (perParagraph.get(unit.paragraph) ?? 0) + count3);
9220
+ const count4 = unit.text.split("—").length - 1;
9221
+ perParagraph.set(unit.paragraph, (perParagraph.get(unit.paragraph) ?? 0) + count4);
8576
9222
  }
8577
- return [...perParagraph].flatMap(([paragraph, count3]) => {
8578
- if (count3 === 0) return [];
9223
+ return [...perParagraph].flatMap(([paragraph, count4]) => {
9224
+ if (count4 === 0) return [];
8579
9225
  const index = units.findIndex((unit) => unit.paragraph === paragraph && unit.text.includes("—"));
8580
- const weight = count3 >= 2 ? STRUCTURE_WEIGHTS.dashSeries : STRUCTURE_WEIGHTS.dash;
9226
+ const weight = count4 >= 2 ? STRUCTURE_WEIGHTS.dashSeries : STRUCTURE_WEIGHTS.dash;
8581
9227
  return [hit("em-dash-series", units, index, weight)];
8582
9228
  });
8583
9229
  };
@@ -8605,11 +9251,11 @@ var windowEnd = (units, from, size) => {
8605
9251
  return to;
8606
9252
  };
8607
9253
  var windowScore = (hits, window) => hits.filter((item) => item.unit >= window.from && item.unit <= window.to).reduce((sum, item) => sum + item.weight, 0);
8608
- var hotWindows = (hits, units, size, threshold) => {
9254
+ var hotWindows = (hits, units, size, threshold2) => {
8609
9255
  const windows = [];
8610
9256
  for (let from = 0; from < units.length; from += 1) {
8611
9257
  const window = { from, to: windowEnd(units, from, size) };
8612
- if (windowScore(hits, window) >= threshold) windows.push(window);
9258
+ if (windowScore(hits, window) >= threshold2) windows.push(window);
8613
9259
  }
8614
9260
  return windows;
8615
9261
  };
@@ -8636,10 +9282,10 @@ var byPosition = (left, right) => left.unit - right.unit || left.line - right.li
8636
9282
  var scoreProse = (text6, options = {}) => {
8637
9283
  const { units, allowed: allowed2 } = extractProse(text6);
8638
9284
  const size = options.window ?? DENSITY_WINDOW;
8639
- const threshold = options.threshold ?? DENSITY_THRESHOLD;
9285
+ const threshold2 = options.threshold ?? DENSITY_THRESHOLD;
8640
9286
  const catalog = options.context === "record" ? [...phraseRules(), ...recordRules()] : phraseRules();
8641
9287
  const hits = [...phraseHits(units, catalog), ...structureHits(units)].sort(byPosition);
8642
- const spans = mergeSpans(hits, hotWindows(hits, units, size, threshold));
9288
+ const spans = mergeSpans(hits, hotWindows(hits, units, size, threshold2));
8643
9289
  return { hits, spans, allowed: allowed2 };
8644
9290
  };
8645
9291
  var isFlagged = (report2) => report2.spans.length > 0;
@@ -8689,24 +9335,57 @@ var MAX_LISTED3 = 10;
8689
9335
  var rulesOf = (span) => [...new Set(span.hits.map((hit3) => hit3.rule))].join(", ");
8690
9336
  var proseOutcome = (status) => {
8691
9337
  if (!status.ok) return unverified("tool_missing", [note("gate.prose.unavailable")]);
8692
- const { files, flagged, blocking } = status.value;
8693
- const key = blocking ? "gate.prose.finding" : "gate.prose.warning";
9338
+ const { files, flagged, blocking: blocking2 } = status.value;
9339
+ const key = blocking2 ? "gate.prose.finding" : "gate.prose.warning";
8694
9340
  const notes2 = flagged.flatMap((item) => item.report.spans.map((span) => ({ file: item.file, span }))).slice(0, MAX_LISTED3).map(({ file, span }) => note(key, { file, line: span.startLine, rules: rulesOf(span) }));
8695
9341
  if (notes2.length === 0) return passed([note("gate.prose.clean", { count: files.length })]);
8696
- return blocking ? failed(notes2) : passed(notes2);
9342
+ return blocking2 ? failed(notes2) : passed(notes2);
8697
9343
  };
8698
9344
  var proseCheck = (_check, ctx) => proseOutcome(proseGateStatus(ctx.runtime, ctx.paths.root));
8699
9345
 
9346
+ // src/gate/checks/review.ts
9347
+ var MAX_SENSITIVE = 5;
9348
+ var sizeNotes = (ctx) => {
9349
+ const size = ctx.size;
9350
+ if (size === void 0) return [];
9351
+ return [
9352
+ note(size.declared === size.effective ? "gate.size.declared" : "gate.size.grew", {
9353
+ declared: size.declared,
9354
+ effective: size.effective,
9355
+ files: size.files,
9356
+ lines: size.lines
9357
+ }),
9358
+ ...size.sensitive.slice(0, MAX_SENSITIVE).map((file) => note("gate.size.sensitive", { file }))
9359
+ ];
9360
+ };
9361
+ var reviewCheck = (check2, ctx) => {
9362
+ const review2 = readReviews(ctx.runtime, ctx.paths).findLast((entry2) => entry2.treeHash === ctx.treeHash);
9363
+ if (review2 !== void 0) {
9364
+ return { status: "passed", notes: [note("gate.review.done", { agent: review2.agent, at: review2.at })] };
9365
+ }
9366
+ const approval = readApprovals(ctx.runtime, ctx.paths).findLast(
9367
+ (item) => item.task === ctx.contract.id && item.check === check2.id
9368
+ );
9369
+ if (approval !== void 0) {
9370
+ return {
9371
+ status: "passed",
9372
+ notes: [note("gate.manual.approved", { at: approval.at, reason: approval.reason })],
9373
+ releasedBy: `person@${approval.at}`
9374
+ };
9375
+ }
9376
+ return failed([...sizeNotes(ctx), note("gate.review.missing", { check: check2.id })]);
9377
+ };
9378
+
8700
9379
  // src/secrets/baseline.ts
8701
- import { join as join15 } from "node:path";
9380
+ import { join as join18 } from "node:path";
8702
9381
  var BASELINE_FILE2 = ".finitemonkey/findings/baseline.json";
8703
9382
  var BASELINE_SCHEMA = "finitemonkey/secrets-baseline@1";
8704
9383
  var BASELINE_STATES = ["aceito", "falso-positivo"];
8705
9384
  var EXCEPTION_MAX_DAYS = 90;
8706
9385
  var EMPTY_BASELINE = { schema: BASELINE_SCHEMA, entries: [] };
8707
- var baselinePath = (root) => toAbsolutePath(join15(root, BASELINE_FILE2));
8708
- var text5 = (record2, key) => {
8709
- const value = record2[key];
9386
+ var baselinePath = (root) => toAbsolutePath(join18(root, BASELINE_FILE2));
9387
+ var text5 = (record3, key) => {
9388
+ const value = record3[key];
8710
9389
  return typeof value === "string" && value !== "" ? value : void 0;
8711
9390
  };
8712
9391
  var stateOf = (value) => BASELINE_STATES.find((state) => state === value);
@@ -8764,22 +9443,22 @@ var withoutFixed = (baseline, fixed) => {
8764
9443
  var acceptedFingerprints = (baseline, now) => new Set(baseline.entries.filter((entry2) => entry2.expiresAt >= now).map((entry2) => entry2.fingerprint));
8765
9444
 
8766
9445
  // src/secrets/incidents.ts
8767
- import { join as join16 } from "node:path";
9446
+ import { join as join19 } from "node:path";
8768
9447
  var INCIDENTS_DIR = ".finitemonkey/incidents";
8769
9448
  var INCIDENT_STATES = ["aberto", "exposicao-local", "rotacionado", "fechado"];
8770
9449
  var INCIDENT_SOURCES = ["tool-output", "prompt", "history", "scan"];
8771
9450
  var ID = /^inc-[0-9a-f]{12}$/;
8772
9451
  var incidentId = (rule, fingerprint) => `inc-${sha256Hex(`${rule}\0${fingerprint}`).slice(0, 12)}`;
8773
- var incidentsDir = (root) => toAbsolutePath(join16(root, INCIDENTS_DIR));
8774
- var incidentFile = (root, id) => toAbsolutePath(join16(incidentsDir(root), `${id}.json`));
8775
- var pick2 = (options, value) => options.find((option) => option === value);
8776
- var str2 = (record2, key) => {
8777
- const value = record2[key];
9452
+ var incidentsDir = (root) => toAbsolutePath(join19(root, INCIDENTS_DIR));
9453
+ var incidentFile = (root, id) => toAbsolutePath(join19(incidentsDir(root), `${id}.json`));
9454
+ var pick3 = (options, value) => options.find((option) => option === value);
9455
+ var str2 = (record3, key) => {
9456
+ const value = record3[key];
8778
9457
  return typeof value === "string" ? value : void 0;
8779
9458
  };
8780
9459
  var parseEvent = (value) => {
8781
9460
  if (!isRecord(value)) return void 0;
8782
- const state = pick2(INCIDENT_STATES, value["state"]);
9461
+ const state = pick3(INCIDENT_STATES, value["state"]);
8783
9462
  const at = str2(value, "at");
8784
9463
  const by = str2(value, "by");
8785
9464
  return state === void 0 || at === void 0 || by === void 0 ? void 0 : { state, at, by };
@@ -8792,12 +9471,12 @@ var parseHistory = (value) => {
8792
9471
  var parseIncident = (raw) => {
8793
9472
  const parsed = parseJsonObject(raw);
8794
9473
  if (!parsed.ok) return parsed;
8795
- const record2 = parsed.value;
8796
- const [id, rule, where, fingerprint, createdAt, updatedAt] = ["id", "rule", "where", "fingerprint", "createdAt", "updatedAt"].map((key) => str2(record2, key));
8797
- const state = pick2(INCIDENT_STATES, record2["state"]);
8798
- const source = pick2(INCIDENT_SOURCES, record2["source"]);
8799
- const history = parseHistory(record2["history"]);
8800
- const firstCommit = str2(record2, "firstCommit");
9474
+ const record3 = parsed.value;
9475
+ const [id, rule, where, fingerprint, createdAt, updatedAt] = ["id", "rule", "where", "fingerprint", "createdAt", "updatedAt"].map((key) => str2(record3, key));
9476
+ const state = pick3(INCIDENT_STATES, record3["state"]);
9477
+ const source = pick3(INCIDENT_SOURCES, record3["source"]);
9478
+ const history = parseHistory(record3["history"]);
9479
+ const firstCommit = str2(record3, "firstCommit");
8801
9480
  if (id === void 0 || !ID.test(id) || rule === void 0 || where === void 0) return reject("incomplete");
8802
9481
  if (fingerprint === void 0 || createdAt === void 0 || updatedAt === void 0) return reject("incomplete");
8803
9482
  if (state === void 0 || source === void 0 || history === void 0) return reject("incomplete");
@@ -8826,7 +9505,7 @@ var listIncidents = (runtime, root) => {
8826
9505
  return names.value.filter((name) => name.endsWith(".json")).map((name) => readIncident(runtime, root, name.slice(0, -".json".length))).flatMap((parsed) => parsed.ok ? [parsed.value] : []).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
8827
9506
  };
8828
9507
  var saveIncident = (runtime, root, incident) => {
8829
- const ignore = runtime.writeText(toAbsolutePath(join16(incidentsDir(root), ".gitignore")), "*\n");
9508
+ const ignore = runtime.writeText(toAbsolutePath(join19(incidentsDir(root), ".gitignore")), "*\n");
8830
9509
  if (!ignore.ok) return ignore;
8831
9510
  return runtime.writeText(incidentFile(root, incident.id), `${JSON.stringify(incident, null, 2)}
8832
9511
  `);
@@ -8873,7 +9552,7 @@ var moveIncident = (incident, to, now, by) => ({
8873
9552
  var isRotated = (incident) => incident.state === "rotacionado" || incident.state === "fechado";
8874
9553
 
8875
9554
  // src/secrets/scan.ts
8876
- import { join as join17, relative as relative4 } from "node:path";
9555
+ import { join as join20, relative as relative4 } from "node:path";
8877
9556
 
8878
9557
  // src/secrets/located.ts
8879
9558
  var findingId = (ruleId, file, fingerprint) => `fm-${sha256Hex(`${ruleId}\0${file}\0${fingerprint}`).slice(0, 16)}`;
@@ -8973,7 +9652,7 @@ var listed2 = (runtime, root, target) => {
8973
9652
  const base = toAbsolutePath(target, root);
8974
9653
  const walked = runtime.walkFiles(base);
8975
9654
  if (!walked.ok) return walked;
8976
- return accept(walked.value.map((name) => toPosixPath(relative4(root, join17(base, toPosixPath(name))))));
9655
+ return accept(walked.value.map((name) => toPosixPath(relative4(root, join20(base, toPosixPath(name))))));
8977
9656
  };
8978
9657
  var scanFile = (runtime, root, file) => {
8979
9658
  const text6 = runtime.readText(toAbsolutePath(file, root));
@@ -9033,15 +9712,17 @@ var secretsCheck = (_check, ctx) => {
9033
9712
  const status = secretsGateStatus(ctx.runtime, ctx.paths.root, sinceOf(ctx));
9034
9713
  if (!status.ok) return unverified("tool_missing", [note("gate.secrets.unavailable")]);
9035
9714
  const { findings, openIncidents } = status.value;
9715
+ const warnings = findings.filter(isWarning).slice(0, MAX_LISTED4).map((finding3) => note("gate.secrets.warning", { file: finding3.file, line: finding3.line, rule: finding3.ruleId }));
9036
9716
  const notes2 = [
9037
- ...findings.slice(0, MAX_LISTED4).map((finding3) => note("gate.secrets.finding", { file: finding3.file, line: finding3.line, rule: finding3.ruleId })),
9717
+ ...blocking(findings).slice(0, MAX_LISTED4).map((finding3) => note("gate.secrets.finding", { file: finding3.file, line: finding3.line, rule: finding3.ruleId })),
9038
9718
  ...openIncidents.slice(0, MAX_LISTED4).map((incident) => note("gate.secrets.incident", { id: incident.id, state: incident.state }))
9039
9719
  ];
9040
- return notes2.length > 0 ? failed(notes2) : passed([note("gate.secrets.clean")]);
9720
+ if (notes2.length > 0) return failed([...notes2, ...warnings]);
9721
+ return passed(warnings.length > 0 ? warnings : [note("gate.secrets.clean")]);
9041
9722
  };
9042
9723
 
9043
9724
  // src/gate/checks/tests.ts
9044
- import { join as join18 } from "node:path";
9725
+ import { join as join21 } from "node:path";
9045
9726
 
9046
9727
  // src/guards/tests/tree.ts
9047
9728
  var outOfScope = (input, change) => input.testsProtected && input.scopePaths.length > 0 && change.status !== "A" && isTestFile(change.path) && !matchesAny(change.path, input.scopePaths) ? [{ kind: "out_of_scope", path: change.path }] : [];
@@ -9073,7 +9754,7 @@ var treeViolations = (ctx) => {
9073
9754
  changes: relevant,
9074
9755
  before: (path) => fileAt(ctx.git, ctx.base, path),
9075
9756
  after: (path) => {
9076
- const text6 = ctx.runtime.readText(toAbsolutePath(join18(ctx.paths.root, path)));
9757
+ const text6 = ctx.runtime.readText(toAbsolutePath(join21(ctx.paths.root, path)));
9077
9758
  return text6.ok ? text6.value : void 0;
9078
9759
  },
9079
9760
  acceptedConfigs: ctx.trust.configs,
@@ -9094,7 +9775,7 @@ var testsCheck = (check2, ctx) => {
9094
9775
  };
9095
9776
 
9096
9777
  // src/git/tree.ts
9097
- import { join as join19 } from "node:path";
9778
+ import { join as join22 } from "node:path";
9098
9779
 
9099
9780
  // src/secrets/guards/paths.ts
9100
9781
  var SAFE_ENV = /^\.env\.(?:example|sample|template|dist|defaults|schema)$/i;
@@ -9133,7 +9814,7 @@ var dropSensitive = (runtime, repo, env, options) => {
9133
9814
  return removed.ok ? accept(void 0) : removed;
9134
9815
  };
9135
9816
  var withTempIndex = (runtime, repo, options, work) => {
9136
- const path = toAbsolutePath(join19(repo.gitDir, `finitemonkey-${options.stamp}.index`));
9817
+ const path = toAbsolutePath(join22(repo.gitDir, `finitemonkey-${options.stamp}.index`));
9137
9818
  const env = { GIT_INDEX_FILE: path, GIT_OPTIONAL_LOCKS: "0" };
9138
9819
  try {
9139
9820
  return work(env);
@@ -9366,112 +10047,26 @@ var entryOf2 = (kind, xy, path) => ({
9366
10047
  staged: kind !== "untracked" && !xy.startsWith("."),
9367
10048
  unstaged: kind === "untracked" || xy.charAt(1) !== "."
9368
10049
  });
9369
- var afterFields = (record2, count3) => record2.split(" ").slice(count3).join(" ");
10050
+ var afterFields = (record3, count4) => record3.split(" ").slice(count4).join(" ");
9370
10051
  var parsePorcelainV2 = (stdout) => {
9371
10052
  const records = stdout.split("\0");
9372
10053
  const out = [];
9373
10054
  for (let index = 0; index < records.length; index += 1) {
9374
- const record2 = records[index] ?? "";
9375
- const xy = record2.split(" ")[1] ?? "..";
9376
- if (record2.startsWith("1 ")) out.push(entryOf2("changed", xy, afterFields(record2, PORCELAIN_FIELDS.ordinary)));
9377
- else if (record2.startsWith("2 ")) {
9378
- out.push(entryOf2("changed", xy, afterFields(record2, PORCELAIN_FIELDS.renamed)));
10055
+ const record3 = records[index] ?? "";
10056
+ const xy = record3.split(" ")[1] ?? "..";
10057
+ if (record3.startsWith("1 ")) out.push(entryOf2("changed", xy, afterFields(record3, PORCELAIN_FIELDS.ordinary)));
10058
+ else if (record3.startsWith("2 ")) {
10059
+ out.push(entryOf2("changed", xy, afterFields(record3, PORCELAIN_FIELDS.renamed)));
9379
10060
  index += 1;
9380
- } else if (record2.startsWith("u ")) {
9381
- out.push(entryOf2("unmerged", xy, afterFields(record2, PORCELAIN_FIELDS.unmerged)));
9382
- } else if (record2.startsWith("? ")) out.push(entryOf2("untracked", "??", record2.slice(2)));
10061
+ } else if (record3.startsWith("u ")) {
10062
+ out.push(entryOf2("unmerged", xy, afterFields(record3, PORCELAIN_FIELDS.unmerged)));
10063
+ } else if (record3.startsWith("? ")) out.push(entryOf2("untracked", "??", record3.slice(2)));
9383
10064
  }
9384
10065
  return out.filter((entry2) => entry2.path !== "" && !entry2.path.startsWith(STATE_PREFIX));
9385
10066
  };
9386
10067
 
9387
- // src/git/project-config.ts
9388
- import { join as join20 } from "node:path";
9389
- var CONFIG_FILE3 = ".finitemonkey/config.yml";
9390
- var unquote = (value) => value.trim().replace(/^(["'])(.*)\1$/s, "$2");
9391
- var stripComment2 = (line2) => {
9392
- let quote;
9393
- for (let index = 0; index < line2.length; index += 1) {
9394
- const char = line2.charAt(index);
9395
- if (quote === void 0 && (char === '"' || char === "'")) quote = char;
9396
- else if (char === quote) quote = void 0;
9397
- else if (quote === void 0 && char === "#" && (index === 0 || /\s/.test(line2.charAt(index - 1)))) {
9398
- return line2.slice(0, index);
9399
- }
9400
- }
9401
- return line2;
9402
- };
9403
- var flowList = (value) => {
9404
- const trimmed = value.trim();
9405
- if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return void 0;
9406
- return trimmed.slice(1, -1).split(",").map(unquote).filter((item) => item !== "");
9407
- };
9408
- var keyValue = (body2) => {
9409
- const match = /^("[^"]*"|'[^']*'|[^:]+?)\s*:(?:\s+(.*))?$/.exec(body2);
9410
- if (match === null) return void 0;
9411
- return { key: unquote(match[1] ?? ""), value: match[2] ?? "" };
9412
- };
9413
- var addItem = (frame, item) => {
9414
- if (frame.key === void 0) return;
9415
- const parent = frame.tree[frame.key];
9416
- if (Array.isArray(parent)) parent.push(item);
9417
- else frame.tree[frame.key] = [item];
9418
- };
9419
- var placeLine = (stack, indent, body2) => {
9420
- while (stack.length > 1 && (stack[stack.length - 1]?.indent ?? 0) >= indent && !body2.startsWith("- ")) stack.pop();
9421
- const top = stack[stack.length - 1];
9422
- if (top === void 0) return;
9423
- if (body2.startsWith("- ")) {
9424
- addItem(top, unquote(body2.slice(2)));
9425
- return;
9426
- }
9427
- const pair = keyValue(body2);
9428
- if (pair === void 0) return;
9429
- const parentTree = top.key === void 0 ? top.tree : ensureTree(top.tree, top.key);
9430
- if (pair.value === "") {
9431
- stack.push({ indent, tree: parentTree, key: pair.key });
9432
- return;
9433
- }
9434
- parentTree[pair.key] = flowList(pair.value) ?? unquote(pair.value);
9435
- };
9436
- var ensureTree = (tree, key) => {
9437
- const existing = tree[key];
9438
- if (existing !== void 0 && typeof existing !== "string" && !Array.isArray(existing)) return existing;
9439
- const fresh3 = {};
9440
- tree[key] = fresh3;
9441
- return fresh3;
9442
- };
9443
- var parseConfigYaml = (text6) => {
9444
- const root = {};
9445
- const stack = [{ indent: -1, tree: root, key: void 0 }];
9446
- for (const raw of text6.split(/\r?\n/)) {
9447
- const line2 = stripComment2(raw).trimEnd();
9448
- if (line2.trim() === "" || line2.trim() === "---") continue;
9449
- const indent = line2.length - line2.trimStart().length;
9450
- placeLine(stack, indent, line2.trim());
9451
- }
9452
- return root;
9453
- };
9454
- var isTree = (node) => typeof node === "object" && !Array.isArray(node);
9455
- var configAt = (tree, ...path) => path.reduce((node, key) => isTree(node) ? node[key] : void 0, tree);
9456
- var configList = (tree, ...path) => {
9457
- const node = configAt(tree, ...path);
9458
- if (typeof node === "string") return node === "" ? [] : [node];
9459
- return Array.isArray(node) ? node.filter((item) => typeof item === "string") : [];
9460
- };
9461
- var configMap = (tree, ...path) => {
9462
- const node = configAt(tree, ...path);
9463
- if (!isTree(node)) return {};
9464
- return Object.fromEntries(
9465
- Object.entries(node).filter((entry2) => typeof entry2[1] === "string")
9466
- );
9467
- };
9468
- var loadProjectConfig = (runtime, root) => {
9469
- const text6 = runtime.readText(toAbsolutePath(join20(root, CONFIG_FILE3)));
9470
- return text6.ok ? parseConfigYaml(text6.value) : {};
9471
- };
9472
-
9473
10068
  // src/git/merge/dropped.ts
9474
- import { join as join21 } from "node:path";
10069
+ import { join as join23 } from "node:path";
9475
10070
 
9476
10071
  // src/git/merge/lines.ts
9477
10072
  var normalizeLine = (line2) => line2.replace(/\r$/, "").trim().replace(/\s*,$/, "");
@@ -9551,7 +10146,7 @@ var parseDropped = (text6) => {
9551
10146
  return accept({ items, invalid });
9552
10147
  };
9553
10148
  var loadDropped = (runtime, root, file) => {
9554
- const path = toAbsolutePath(file ?? join21(root, DROPPED_FILE), root);
10149
+ const path = toAbsolutePath(file ?? join23(root, DROPPED_FILE), root);
9555
10150
  const text6 = runtime.readText(path);
9556
10151
  if (!text6.ok) return file === void 0 ? accept({ items: [], invalid: [] }) : reject(`${file}: ${text6.error}`);
9557
10152
  return parseDropped(text6.value);
@@ -9561,7 +10156,7 @@ var isDropped = (finding3, items) => items.some(
9561
10156
  );
9562
10157
 
9563
10158
  // src/git/merge/generated.ts
9564
- import { join as join22 } from "node:path";
10159
+ import { join as join24 } from "node:path";
9565
10160
  var LOCKFILES3 = /* @__PURE__ */ new Set([
9566
10161
  "package-lock.json",
9567
10162
  "npm-shrinkwrap.json",
@@ -9624,7 +10219,7 @@ var verifyGenerated = (runtime, repo, inputs, paths, generators) => {
9624
10219
  if (declared.length === 0) return accept([]);
9625
10220
  const commit2 = resultCommit(runtime, repo, inputs);
9626
10221
  if (!commit2.ok) return commit2;
9627
- const dir = join22(repo.gitDir, `finitemonkey-generate-${runtime.now().replace(/[:.]/g, "-")}`);
10222
+ const dir = join24(repo.gitDir, `finitemonkey-generate-${runtime.now().replace(/[:.]/g, "-")}`);
9628
10223
  if (!runtime.git(["worktree", "add", "--detach", dir, commit2.value], repo.root).ok)
9629
10224
  return reject("could not check out the merge result");
9630
10225
  try {
@@ -9634,7 +10229,7 @@ var verifyGenerated = (runtime, repo, inputs, paths, generators) => {
9634
10229
  }
9635
10230
  return accept(
9636
10231
  declared.flatMap(({ path }) => {
9637
- const fresh3 = runtime.readText(toAbsolutePath(join22(dir, path)));
10232
+ const fresh3 = runtime.readText(toAbsolutePath(join24(dir, path)));
9638
10233
  const merged = blobText(runtime, repo, commit2.value, path);
9639
10234
  if (fresh3.ok && fresh3.value === merged) return [];
9640
10235
  return [{ kind: "generated-differs", file: path, side: void 0, line: void 0, text: path, weak: false }];
@@ -9848,16 +10443,16 @@ var failed2 = (error, inputs) => ({
9848
10443
  generated: [],
9849
10444
  error
9850
10445
  });
9851
- var keyOf = (diff) => (diff.status === "added" || diff.status === "copied" ? diff.newPath : diff.oldPath ?? diff.newPath) ?? "";
10446
+ var keyOf2 = (diff) => (diff.status === "added" || diff.status === "copied" ? diff.newPath : diff.oldPath ?? diff.newPath) ?? "";
9852
10447
  var pairsOf = (ours, theirs) => {
9853
- const keys = [...new Set([...ours, ...theirs].map(keyOf))].filter((key) => key !== "").sort();
10448
+ const keys = [...new Set([...ours, ...theirs].map(keyOf2))].filter((key) => key !== "").sort();
9854
10449
  return keys.map((key) => ({
9855
10450
  key,
9856
- ours: ours.find((diff) => keyOf(diff) === key),
9857
- theirs: theirs.find((diff) => keyOf(diff) === key)
10451
+ ours: ours.find((diff) => keyOf2(diff) === key),
10452
+ theirs: theirs.find((diff) => keyOf2(diff) === key)
9858
10453
  }));
9859
10454
  };
9860
- var contextOf2 = (runtime, repo, inputs, pairs) => {
10455
+ var contextOf2 = (runtime, repo, inputs, pairs2) => {
9861
10456
  const resultText = memoBy((path) => blobText(runtime, repo, inputs.result, path));
9862
10457
  const context = {
9863
10458
  runtime,
@@ -9865,7 +10460,7 @@ var contextOf2 = (runtime, repo, inputs, pairs) => {
9865
10460
  inputs,
9866
10461
  resultText,
9867
10462
  resultFiles: memo2(
9868
- () => pairs.flatMap((pair) => {
10463
+ () => pairs2.flatMap((pair) => {
9869
10464
  const path = resultPathOf(context, pair);
9870
10465
  return path === void 0 ? [] : [path];
9871
10466
  })
@@ -9882,24 +10477,24 @@ var verifyMerge = (runtime, repo, options = {}) => {
9882
10477
  const theirs = sideDiff(runtime, repo, inputs.value.base, inputs.value.theirs);
9883
10478
  if (!ours.ok) return failed2(ours.error, inputs.value);
9884
10479
  if (!theirs.ok) return failed2(theirs.error, inputs.value);
9885
- const pairs = pairsOf(ours.value, theirs.value);
10480
+ const pairs2 = pairsOf(ours.value, theirs.value);
9886
10481
  const generators = configMap(loadProjectConfig(runtime, repo.root), "merge", "generators");
9887
10482
  const generated = generatedPaths(
9888
10483
  runtime,
9889
10484
  repo,
9890
- pairs.map((pair) => pair.key),
10485
+ pairs2.map((pair) => pair.key),
9891
10486
  generators
9892
10487
  );
9893
10488
  const context = contextOf2(
9894
10489
  runtime,
9895
10490
  repo,
9896
10491
  inputs.value,
9897
- pairs.filter((pair) => !generated.has(pair.key))
10492
+ pairs2.filter((pair) => !generated.has(pair.key))
9898
10493
  );
9899
10494
  const regenerated = verifyGenerated(runtime, repo, inputs.value, [...generated], generators);
9900
10495
  if (!regenerated.ok) return failed2(regenerated.error, inputs.value);
9901
10496
  const all = [
9902
- ...pairs.filter((pair) => !generated.has(pair.key)).flatMap((pair) => verifyFile(context, pair)),
10497
+ ...pairs2.filter((pair) => !generated.has(pair.key)).flatMap((pair) => verifyFile(context, pair)),
9903
10498
  ...regenerated.value
9904
10499
  ];
9905
10500
  const open = [...dropped.value.invalid, ...all.filter((item) => !isDropped(item, dropped.value.items))];
@@ -9911,7 +10506,7 @@ var verifyMerge = (runtime, repo, options = {}) => {
9911
10506
  theirs: inputs.value.theirs,
9912
10507
  result: inputs.value.result,
9913
10508
  bases: inputs.value.bases,
9914
- files: pairs.length,
10509
+ files: pairs2.length,
9915
10510
  findings: open,
9916
10511
  justified: all.length - (open.length - dropped.value.invalid.length),
9917
10512
  generated: [...generated].sort(),
@@ -9920,19 +10515,19 @@ var verifyMerge = (runtime, repo, options = {}) => {
9920
10515
  };
9921
10516
 
9922
10517
  // src/git/ownership.ts
9923
- import { join as join23 } from "node:path";
10518
+ import { join as join25 } from "node:path";
9924
10519
  var OWNERSHIP_DIR = ".finitemonkey/git";
9925
10520
  var ownerKey = (raw) => {
9926
10521
  const cleaned = (raw ?? "").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80);
9927
10522
  return cleaned === "" ? "unknown" : cleaned;
9928
10523
  };
9929
- var fileOf = (repo, kind, owner) => toAbsolutePath(join23(repo.root, OWNERSHIP_DIR, kind, `${ownerKey(owner)}.json`));
10524
+ var fileOf = (repo, kind, owner) => toAbsolutePath(join25(repo.root, OWNERSHIP_DIR, kind, `${ownerKey(owner)}.json`));
9930
10525
  var readDirty = (runtime, root) => {
9931
10526
  const status = runtime.git(["status", "--porcelain=v2", "-z", "--untracked-files=all"], root);
9932
10527
  return status.ok ? parsePorcelainV2(status.stdout) : void 0;
9933
10528
  };
9934
10529
  var hashDirty = (runtime, repo, entries) => {
9935
- const present2 = entries.filter((entry2) => runtime.stat(toAbsolutePath(join23(repo.root, entry2.path)))?.kind === "file");
10530
+ const present2 = entries.filter((entry2) => runtime.stat(toAbsolutePath(join25(repo.root, entry2.path)))?.kind === "file");
9936
10531
  const hashed = present2.length === 0 ? void 0 : runtime.gitWith(["hash-object", "--stdin-paths"], repo.root, {
9937
10532
  input: `${present2.map((entry2) => entry2.path).join("\n")}
9938
10533
  `
@@ -10184,7 +10779,9 @@ var DEFAULT_CHECKS = {
10184
10779
  secrets: secretsCheck,
10185
10780
  prose: proseCheck,
10186
10781
  worktree: worktreeCheck,
10187
- merge: mergeCheck
10782
+ merge: mergeCheck,
10783
+ review: reviewCheck,
10784
+ callers: callersCheck
10188
10785
  };
10189
10786
 
10190
10787
  // src/gate/run.ts
@@ -10364,10 +10961,21 @@ var isOwnWorktree = (input) => {
10364
10961
  const main2 = findRepoRoot(runtime, input.cwd);
10365
10962
  return front !== void 0 && main2 !== void 0 && front !== main2;
10366
10963
  };
10367
- var subagentStopGate = (input) => isOwnWorktree(input) ? stopGate(input) : { kind: "allow" };
10964
+ var noteReview = (input) => {
10965
+ const agent = textField(input.payload.raw, "agent_type") ?? "";
10966
+ if (!REVIEWER_AGENT.test(agent)) return;
10967
+ const root = findRepoRoot(input.runtime, sessionCwd(input));
10968
+ if (root === void 0) return;
10969
+ const tree = treeHash(gitAt(input.runtime, root));
10970
+ if (tree !== void 0) recordReview(input.runtime, fmPaths(root), tree, agent);
10971
+ };
10972
+ var subagentStopGate = (input) => {
10973
+ noteReview(input);
10974
+ return isOwnWorktree(input) ? stopGate(input) : { kind: "allow" };
10975
+ };
10368
10976
 
10369
10977
  // src/gate/hooks/subagent.ts
10370
- import { join as join24 } from "node:path";
10978
+ import { join as join26 } from "node:path";
10371
10979
  var NO_CONTRACT_AGENTS = /(^|:)(reviewer|verifier|explore|plan|claude-code-guide|statusline-setup)$/i;
10372
10980
  var KNOWN_KINDS = /* @__PURE__ */ new Set([
10373
10981
  "command",
@@ -10380,7 +10988,9 @@ var KNOWN_KINDS = /* @__PURE__ */ new Set([
10380
10988
  "secrets",
10381
10989
  "prose",
10382
10990
  "worktree",
10383
- "merge"
10991
+ "merge",
10992
+ "review",
10993
+ "callers"
10384
10994
  ]);
10385
10995
  var requirementText = (t, check2) => {
10386
10996
  const key = `gate.require.${check2.kind}`;
@@ -10401,12 +11011,12 @@ var subagentStart = (input) => {
10401
11011
  const active = readState(runtime, paths).active;
10402
11012
  if (active === void 0) return { kind: "allow" };
10403
11013
  const disk = (relative9) => {
10404
- const text6 = runtime.readText(toAbsolutePath(join24(paths.root, relative9)));
11014
+ const text6 = runtime.readText(toAbsolutePath(join26(paths.root, relative9)));
10405
11015
  return text6.ok ? text6.value : void 0;
10406
11016
  };
10407
11017
  const loaded = loadTask(runtime, paths, { disk, base: () => void 0 }, active);
10408
11018
  if (!loaded.ok) return { kind: "allow" };
10409
- const briefing = runtime.stat(toAbsolutePath(join24(paths.dir, "briefing.md"))) !== void 0;
11019
+ const briefing = runtime.stat(toAbsolutePath(join26(paths.dir, "briefing.md"))) !== void 0;
10410
11020
  return { kind: "allow", output: contextOutput("SubagentStart", contractFacts(t, loaded.value.contract, briefing)) };
10411
11021
  };
10412
11022
 
@@ -10423,7 +11033,7 @@ var PORTAO_HANDLERS = {
10423
11033
  };
10424
11034
 
10425
11035
  // src/prod/guard.ts
10426
- import { join as join26 } from "node:path";
11036
+ import { join as join28 } from "node:path";
10427
11037
 
10428
11038
  // src/git/command-text.ts
10429
11039
  var MAX_SHOWN = 160;
@@ -10765,7 +11375,7 @@ var remoteHostsOf = (command) => {
10765
11375
  };
10766
11376
 
10767
11377
  // src/prod/targets.ts
10768
- import { join as join25 } from "node:path";
11378
+ import { join as join27 } from "node:path";
10769
11379
  var DEPLOY_FILES = [
10770
11380
  "deploy.sh",
10771
11381
  "scripts/deploy.sh",
@@ -10789,12 +11399,12 @@ var hostsInText = (text6) => {
10789
11399
  return [...urls, ...keyed, ...commands].filter((host) => host !== "" && !host.includes("$"));
10790
11400
  };
10791
11401
  var workflowFiles = (runtime, root) => {
10792
- const listed4 = runtime.listDir(toAbsolutePath(join25(root, ".github", "workflows")));
11402
+ const listed4 = runtime.listDir(toAbsolutePath(join27(root, ".github", "workflows")));
10793
11403
  if (!listed4.ok) return [];
10794
11404
  return listed4.value.filter((name) => /deploy/i.test(name) && /\.ya?ml$/i.test(name)).map((name) => `.github/workflows/${name}`);
10795
11405
  };
10796
11406
  var detectedHosts = (runtime, root, files) => files.flatMap((file) => {
10797
- const text6 = runtime.readText(toAbsolutePath(join25(root, file)));
11407
+ const text6 = runtime.readText(toAbsolutePath(join27(root, file)));
10798
11408
  return text6.ok ? hostsInText(text6.value) : [];
10799
11409
  });
10800
11410
  var loadProdTargets = (runtime, root, config) => {
@@ -10806,7 +11416,7 @@ var loadProdTargets = (runtime, root, config) => {
10806
11416
  ...detectedHosts(runtime, root, deployFiles)
10807
11417
  ];
10808
11418
  const declaredDeploy = configAt(config, "prod", "deploy");
10809
- const existing = deployFiles.find((file) => runtime.stat(toAbsolutePath(join25(root, file))) !== void 0);
11419
+ const existing = deployFiles.find((file) => runtime.stat(toAbsolutePath(join27(root, file))) !== void 0);
10810
11420
  return {
10811
11421
  hosts: new Set(hosts.map(hostKey).filter((host) => host !== "")),
10812
11422
  kubeContexts: configList(config, "prod", "kube_contexts"),
@@ -10983,8 +11593,8 @@ var recordEvidence = (runtime, root, owner, reads) => {
10983
11593
  ] : []
10984
11594
  );
10985
11595
  if (lines3.length === 0) return;
10986
- const path = toAbsolutePath(join26(root, EVIDENCE_FILE));
10987
- runtime.mkdir(toAbsolutePath(join26(root, ".finitemonkey")));
11596
+ const path = toAbsolutePath(join28(root, EVIDENCE_FILE));
11597
+ runtime.mkdir(toAbsolutePath(join28(root, ".finitemonkey")));
10988
11598
  const before = runtime.readText(path);
10989
11599
  const text6 = before.ok ? before.value : "";
10990
11600
  runtime.writeAtomic(path, `${text6}${text6 === "" || text6.endsWith("\n") ? "" : "\n"}${lines3.join("\n")}
@@ -11086,7 +11696,7 @@ var withAlias = (invocation, expansion) => ({
11086
11696
  });
11087
11697
 
11088
11698
  // src/git/path-key.ts
11089
- import { posix as posix2 } from "node:path";
11699
+ import { posix as posix3 } from "node:path";
11090
11700
  var WSL_MOUNT3 = /^\/mnt\/([a-zA-Z])(\/|$)/;
11091
11701
  var DRIVE = /^([a-zA-Z]):(\/|$)/;
11092
11702
  var pathKey = (path) => {
@@ -11095,7 +11705,7 @@ var pathKey = (path) => {
11095
11705
  (_match, letter, slash) => `${letter}:${slash || "/"}`
11096
11706
  );
11097
11707
  const drive = DRIVE.exec(posixPath);
11098
- const normalized = posix2.normalize(drive === null ? posixPath : posixPath.slice(2) || "/");
11708
+ const normalized = posix3.normalize(drive === null ? posixPath : posixPath.slice(2) || "/");
11099
11709
  const trimmed = normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
11100
11710
  return drive === null ? trimmed : `${(drive[1] ?? "").toLowerCase()}:${trimmed}`.toLowerCase();
11101
11711
  };
@@ -11111,15 +11721,15 @@ var isRootLike = (path, home) => {
11111
11721
  if (key === "/" || /^[a-z]:\/?$/.test(key)) return true;
11112
11722
  return home !== void 0 && home !== "" && key === pathKey(home);
11113
11723
  };
11114
- var resolveFrom = (base, ...parts) => parts.reduce((current, part2) => {
11115
- if (part2 === void 0 || part2 === "") return current;
11724
+ var resolveFrom = (base, ...parts) => parts.reduce((current2, part2) => {
11725
+ if (part2 === void 0 || part2 === "") return current2;
11116
11726
  const next2 = toPosixPath(part2);
11117
11727
  if (next2.startsWith("/") || DRIVE.test(next2)) return next2;
11118
- return posix2.join(toPosixPath(current), next2);
11728
+ return posix3.join(toPosixPath(current2), next2);
11119
11729
  }, toPosixPath(base));
11120
11730
 
11121
11731
  // src/git/pathspec.ts
11122
- import { posix as posix3 } from "node:path";
11732
+ import { posix as posix4 } from "node:path";
11123
11733
  var MAGIC = /^:(?:\(([^)]*)\)|([!^/]*))(.*)$/s;
11124
11734
  var GLOB = /[*?[]/;
11125
11735
  var specOf = (raw, prefix) => {
@@ -11129,7 +11739,7 @@ var specOf = (raw, prefix) => {
11129
11739
  const rest = magic === null ? posixRaw : magic[3] ?? "";
11130
11740
  const exclude = /(?:^|,)exclude(?:,|$)|[!^]/.test(words);
11131
11741
  const top = /(?:^|,)top(?:,|$)|\//.test(words);
11132
- const joined = posix3.normalize(top ? rest : posix3.join(prefix === "" ? "." : prefix, rest));
11742
+ const joined = posix4.normalize(top ? rest : posix4.join(prefix === "" ? "." : prefix, rest));
11133
11743
  return { pattern: joined === "./" ? "." : joined.replace(/\/$/, ""), exclude };
11134
11744
  };
11135
11745
  var globRegex3 = (pattern) => {
@@ -11162,7 +11772,7 @@ var discard = (op, scope2, pathspecs = []) => ({
11162
11772
  discard: { scope: scope2, pathspecs }
11163
11773
  });
11164
11774
  var SEVERITY = { pass: 0, snapshot: 1, discard: 2, block: 3 };
11165
- var worst = (verdicts) => verdicts.reduce((current, next2) => SEVERITY[next2.kind] > SEVERITY[current.kind] ? next2 : current, PASS);
11775
+ var worst = (verdicts) => verdicts.reduce((current2, next2) => SEVERITY[next2.kind] > SEVERITY[current2.kind] ? next2 : current2, PASS);
11166
11776
 
11167
11777
  // src/git/options.ts
11168
11778
  var canonical = (name, spec2) => {
@@ -11523,24 +12133,24 @@ var branchName = (ref) => {
11523
12133
  if (ref.startsWith("refs/tags/")) return void 0;
11524
12134
  return ref.replace(/^refs\/heads\//, "");
11525
12135
  };
11526
- var refspecTarget = (refspec, current) => {
12136
+ var refspecTarget = (refspec, current2) => {
11527
12137
  const plain = refspec.replace(/^\+/, "");
11528
12138
  const colon = plain.indexOf(":");
11529
12139
  const destination = colon < 0 ? plain : plain.slice(colon + 1);
11530
- if (destination === "HEAD" || colon < 0 && plain === "HEAD") return current;
12140
+ if (destination === "HEAD" || colon < 0 && plain === "HEAD") return current2;
11531
12141
  if (destination === "" || destination.startsWith("refs/tags/")) return void 0;
11532
12142
  return branchName(destination);
11533
12143
  };
11534
12144
  var pushDestinations = (parsed, facts) => {
11535
12145
  if (hasAny(parsed, "--all", "--branches", "--mirror")) return "all";
11536
12146
  const refspecs = parsed.positionals.slice(1).filter((refspec) => refspec !== ":");
11537
- const current = facts.currentBranch();
12147
+ const current2 = facts.currentBranch();
11538
12148
  if (refspecs.length === 0) {
11539
- if (hasAny(parsed, "--tags") || current === void 0) return [];
11540
- return [facts.upstream(current)?.branch ?? current];
12149
+ if (hasAny(parsed, "--tags") || current2 === void 0) return [];
12150
+ return [facts.upstream(current2)?.branch ?? current2];
11541
12151
  }
11542
12152
  return refspecs.flatMap((refspec) => {
11543
- const target = refspecTarget(refspec, current);
12153
+ const target = refspecTarget(refspec, current2);
11544
12154
  return target === void 0 ? [] : [target];
11545
12155
  });
11546
12156
  };
@@ -11548,10 +12158,10 @@ var leaseVerdict = (parsed, facts) => {
11548
12158
  const leases = valuesOf(parsed, "--force-with-lease");
11549
12159
  const explicit = leases.length > 0 && leases.every((lease) => /^[^:]+:.+$/.test(lease));
11550
12160
  if (!explicit || !hasAny(parsed, "--force-if-includes")) return block2("force-push", "--force-with-lease");
11551
- const current = facts.currentBranch();
12161
+ const current2 = facts.currentBranch();
11552
12162
  const leased = leases.map((lease) => branchName(lease.slice(0, lease.indexOf(":"))));
11553
12163
  const destinations = pushDestinations(parsed, facts);
11554
- const own = current !== void 0 && !facts.isProtected(current) && leased.every((name) => name === current) && destinations !== "all" && destinations.every((name) => name === current);
12164
+ const own = current2 !== void 0 && !facts.isProtected(current2) && leased.every((name) => name === current2) && destinations !== "all" && destinations.every((name) => name === current2);
11555
12165
  return own ? PASS : block2("lease-not-own", leased.join(", "));
11556
12166
  };
11557
12167
  var pushVerdict = (invocation, facts) => {
@@ -11574,8 +12184,8 @@ var LEAVES = /* @__PURE__ */ new Set(["--abort", "--quit", "--skip", "--dry-run"
11574
12184
  var commitVerdict = (invocation, facts) => {
11575
12185
  if (invocation.args.some((arg) => LEAVES.has(arg))) return PASS;
11576
12186
  if (invocation.subcommand === "rebase") return PASS;
11577
- const current = facts.currentBranch();
11578
- return current !== void 0 && facts.isProtected(current) ? block2("protected-commit", current) : PASS;
12187
+ const current2 = facts.currentBranch();
12188
+ return current2 !== void 0 && facts.isProtected(current2) ? block2("protected-commit", current2) : PASS;
11579
12189
  };
11580
12190
  var pushVerdict2 = (invocation, facts) => {
11581
12191
  const parsed = parseOptions(invocation.args, PUSH_OPTIONS);
@@ -11796,33 +12406,33 @@ var listWorktrees = (runtime, cwd) => {
11796
12406
  };
11797
12407
 
11798
12408
  // src/git/journal.ts
11799
- import { join as join27 } from "node:path";
12409
+ import { join as join29 } from "node:path";
11800
12410
  var JOURNAL_FILE = ".finitemonkey/git-journal.jsonl";
11801
12411
  var JOURNAL_SCHEMA = "finitemonkey/git-journal@1";
11802
- var journalPath = (root) => toAbsolutePath(join27(root, JOURNAL_FILE));
12412
+ var journalPath = (root) => toAbsolutePath(join29(root, JOURNAL_FILE));
11803
12413
  var headsOf = (value) => isRecord(value) ? Object.fromEntries(
11804
12414
  Object.entries(value).filter((entry2) => typeof entry2[1] === "string")
11805
12415
  ) : {};
11806
12416
  var parseJournalLine = (line2) => {
11807
12417
  const parsed = parseJsonObject(line2);
11808
12418
  if (!parsed.ok) return void 0;
11809
- const record2 = parsed.value;
11810
- const ref = optionalString(record2, "ref");
11811
- const commit2 = optionalString(record2, "commit");
11812
- const at = optionalString(record2, "at");
12419
+ const record3 = parsed.value;
12420
+ const ref = optionalString(record3, "ref");
12421
+ const commit2 = optionalString(record3, "commit");
12422
+ const at = optionalString(record3, "at");
11813
12423
  if (ref === void 0 || commit2 === void 0 || at === void 0) return void 0;
11814
12424
  return {
11815
12425
  schema: JOURNAL_SCHEMA,
11816
12426
  at,
11817
- op: optionalString(record2, "op") ?? "",
12427
+ op: optionalString(record3, "op") ?? "",
11818
12428
  ref,
11819
12429
  commit: commit2,
11820
- head: optionalString(record2, "head"),
11821
- branch: optionalString(record2, "branch"),
11822
- index: optionalString(record2, "index"),
11823
- owner: optionalString(record2, "owner"),
11824
- command: optionalString(record2, "command"),
11825
- heads: headsOf(record2["heads"])
12430
+ head: optionalString(record3, "head"),
12431
+ branch: optionalString(record3, "branch"),
12432
+ index: optionalString(record3, "index"),
12433
+ owner: optionalString(record3, "owner"),
12434
+ command: optionalString(record3, "command"),
12435
+ heads: headsOf(record3["heads"])
11826
12436
  };
11827
12437
  };
11828
12438
  var readJournal = (runtime, root) => {
@@ -11835,7 +12445,7 @@ var readJournal = (runtime, root) => {
11835
12445
  };
11836
12446
  var appendJournal = (runtime, root, entry2) => {
11837
12447
  const path = journalPath(root);
11838
- const made = runtime.mkdir(toAbsolutePath(join27(root, ".finitemonkey")));
12448
+ const made = runtime.mkdir(toAbsolutePath(join29(root, ".finitemonkey")));
11839
12449
  if (!made.ok) return made;
11840
12450
  const previous = runtime.readText(path);
11841
12451
  const before = previous.ok ? previous.value : "";
@@ -12077,6 +12687,83 @@ var GIT_HANDLERS = {
12077
12687
  PostToolUse: [gitPostToolUse]
12078
12688
  };
12079
12689
 
12690
+ // src/policy/guards.ts
12691
+ var SHADOW_FILE = "shadow.json";
12692
+ var numberOf = (value) => typeof value === "number" && Number.isInteger(value) ? value : 0;
12693
+ var readShadow = (runtime, paths) => {
12694
+ const text6 = runtime.readText(paths.cache(SHADOW_FILE));
12695
+ const parsed = text6.ok ? parseJsonObject(text6.value) : void 0;
12696
+ if (parsed?.ok !== true) return {};
12697
+ return Object.fromEntries(
12698
+ Object.entries(parsed.value).flatMap(
12699
+ ([id, value]) => isRecord(value) ? [
12700
+ [
12701
+ id,
12702
+ {
12703
+ firings: numberOf(value["firings"]),
12704
+ falsePositives: numberOf(value["falsePositives"]),
12705
+ promoted: value["promoted"] === true
12706
+ }
12707
+ ]
12708
+ ] : []
12709
+ )
12710
+ );
12711
+ };
12712
+ var writeShadow = (runtime, paths, state) => {
12713
+ runtime.writeText(paths.cache(SHADOW_FILE), `${JSON.stringify(state, null, 2)}
12714
+ `);
12715
+ };
12716
+ var modeOf2 = (set, policy, shadow) => {
12717
+ if (set.damage) return "active";
12718
+ const chosen = policy.guards[set.id];
12719
+ if (chosen !== void 0) return chosen;
12720
+ if (!policy.exists) return "active";
12721
+ return shadow[set.id]?.promoted === true ? "active" : "shadow";
12722
+ };
12723
+ var recordShadow = (input, paths, set, policy, reason) => {
12724
+ const { runtime } = input;
12725
+ const state = readShadow(runtime, paths);
12726
+ const before = state[set.id] ?? { firings: 0, falsePositives: 0, promoted: false };
12727
+ const firings = before.firings + 1;
12728
+ const promoted = !before.promoted && firings >= policy.promoteAfter && before.falsePositives === 0;
12729
+ writeShadow(runtime, paths, { ...state, [set.id]: { ...before, firings, promoted: before.promoted || promoted } });
12730
+ logEvent(runtime, paths, "guard_shadow", {
12731
+ guard: set.id,
12732
+ hook: input.event,
12733
+ tool: toolName2(input.payload),
12734
+ wouldHave: "block",
12735
+ reason
12736
+ });
12737
+ if (promoted) {
12738
+ logEvent(runtime, paths, "guard_promoted", {
12739
+ guard: set.id,
12740
+ firings,
12741
+ falsePositives: before.falsePositives,
12742
+ promoteAfter: policy.promoteAfter
12743
+ });
12744
+ }
12745
+ };
12746
+ var governed = (set, handler) => async (input) => {
12747
+ const paths = projectPaths(input);
12748
+ if (paths === void 0) return handler(input);
12749
+ const policy = loadPolicy(input.runtime, paths.root);
12750
+ const mode = modeOf2(set, policy, readShadow(input.runtime, paths));
12751
+ if (mode === "off") return { kind: "allow" };
12752
+ const decision = await handler(input);
12753
+ if (mode === "active" || decision.kind !== "block") return decision;
12754
+ recordShadow(input, paths, set, policy, decision.reason);
12755
+ return { kind: "allow" };
12756
+ };
12757
+ var withPolicy = (set) => {
12758
+ if (set.damage) return set.handlers;
12759
+ const wrapped = {};
12760
+ for (const event of HOOK_EVENTS) {
12761
+ const list4 = set.handlers[event];
12762
+ if (list4 !== void 0) wrapped[event] = list4.map((handler) => governed(set, handler));
12763
+ }
12764
+ return wrapped;
12765
+ };
12766
+
12080
12767
  // src/prose/marker-guard.ts
12081
12768
  import { relative as relative6 } from "node:path";
12082
12769
 
@@ -12337,10 +13024,10 @@ var textOf2 = (input, found) => {
12337
13024
  }
12338
13025
  return accept(parts.join("\n\n"));
12339
13026
  };
12340
- var decisionFor = (input, found, blocking) => {
13027
+ var decisionFor = (input, found, blocking2) => {
12341
13028
  const text6 = textOf2(input, found);
12342
13029
  if (!text6.ok) {
12343
- return blocking ? { kind: "block", reason: input.t("prose.block.unreadable", { ...text6.error }) } : void 0;
13030
+ return blocking2 ? { kind: "block", reason: input.t("prose.block.unreadable", { ...text6.error }) } : void 0;
12344
13031
  }
12345
13032
  const report2 = scoreProse(text6.value, { context: "record" });
12346
13033
  if (report2.allowed || report2.spans.length === 0) return void 0;
@@ -12349,7 +13036,7 @@ var decisionFor = (input, found, blocking) => {
12349
13036
  spans: describeSpans(input.t, report2.spans),
12350
13037
  fixes: describeFixes(input.t, report2.spans)
12351
13038
  };
12352
- if (blocking) return { kind: "block", reason: input.t("prose.block.command", params2) };
13039
+ if (blocking2) return { kind: "block", reason: input.t("prose.block.command", params2) };
12353
13040
  const additionalContext = `finitemonkey: ${input.t("prose.warn.command", params2)}`;
12354
13041
  return { kind: "allow", output: { hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext } } };
12355
13042
  };
@@ -12357,9 +13044,9 @@ var proseCommandGuard = (input) => {
12357
13044
  if (!SHELLS3.has(toolName2(input.payload))) return ALLOW4;
12358
13045
  const found = proseInCommand(optionalString(toolInput2(input.payload), "command") ?? "");
12359
13046
  if (found.length === 0) return ALLOW4;
12360
- const blocking = projectPreset(input.runtime, projectRoot(input)) === "gabriel";
13047
+ const blocking2 = projectPreset(input.runtime, projectRoot(input)) === "gabriel";
12361
13048
  for (const item of found) {
12362
- const decision = decisionFor(input, item, blocking);
13049
+ const decision = decisionFor(input, item, blocking2);
12363
13050
  if (decision !== void 0) return decision;
12364
13051
  }
12365
13052
  return ALLOW4;
@@ -12386,7 +13073,7 @@ var liveFindings = (input, texts) => {
12386
13073
  const accepted2 = acceptedFingerprints(baseline.ok ? baseline.value : EMPTY_BASELINE, input.runtime.now());
12387
13074
  return texts.flatMap((text6) => scanText(text6, { rules: CREDENTIAL_RULES })).filter((finding3) => !accepted2.has(finding3.fingerprint));
12388
13075
  };
12389
- var record = (input, findings, draft, where) => {
13076
+ var record2 = (input, findings, draft, where) => {
12390
13077
  const ids2 = findings.map((finding3) => {
12391
13078
  const saved = recordIncident(
12392
13079
  input.runtime,
@@ -12402,7 +13089,7 @@ var secretsUserPromptSubmit = (input) => {
12402
13089
  const prompt = optionalString(input.payload.raw, "prompt") ?? "";
12403
13090
  const findings = liveFindings(input, [prompt]);
12404
13091
  if (findings.length === 0) return { kind: "allow" };
12405
- const incident = record(input, findings, { source: "prompt", state: "exposicao-local" }, "prompt");
13092
+ const incident = record2(input, findings, { source: "prompt", state: "exposicao-local" }, "prompt");
12406
13093
  const items = findings.map((finding3) => ({ ruleId: finding3.ruleId, where: lineWhere(input.t, finding3) }));
12407
13094
  return {
12408
13095
  kind: "block",
@@ -12413,7 +13100,7 @@ var secretsPostToolUse = (input) => {
12413
13100
  const findings = liveFindings(input, stringLeaves(input.payload.raw["tool_response"]));
12414
13101
  if (findings.length === 0) return { kind: "allow" };
12415
13102
  const tool = toolName2(input.payload) || "tool";
12416
- const incident = record(input, findings, { source: "tool-output", state: "aberto" }, `tool-output:${tool}`);
13103
+ const incident = record2(input, findings, { source: "tool-output", state: "aberto" }, `tool-output:${tool}`);
12417
13104
  const items = findings.map((finding3) => ({ ruleId: finding3.ruleId, where: lineWhere(input.t, finding3) }));
12418
13105
  const context = input.t("secrets.post.context", { tool, found: describeFindings(input.t, items), incident });
12419
13106
  return {
@@ -12523,11 +13210,24 @@ var relativeFile = (cwd, file) => {
12523
13210
  const rel = relative7(cwd, file).split("\\").join("/");
12524
13211
  return rel.startsWith("..") || rel === "" ? file : rel;
12525
13212
  };
13213
+ var warningDecision = (t, warnings) => {
13214
+ if (warnings.length === 0) return ALLOW5;
13215
+ const context = t("secrets.warn.write", { found: describeFindings(t, warnings) });
13216
+ return { kind: "allow", output: { hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext: context } } };
13217
+ };
12526
13218
  var writeDecision = (input, tool) => {
12527
13219
  const { t } = input;
13220
+ const warnings = [];
12528
13221
  for (const written of writtenTexts(tool, toolInput2(input.payload))) {
12529
13222
  const file = relativeFile(input.cwd, written.file);
12530
- const found = scanText(written.text, { path: file });
13223
+ const all = scanText(written.text, { path: file });
13224
+ warnings.push(
13225
+ ...all.filter(isWarning).map((finding3) => ({
13226
+ ruleId: finding3.ruleId,
13227
+ where: t("secrets.where.file", { file, line: finding3.line, column: finding3.column })
13228
+ }))
13229
+ );
13230
+ const found = blocking(all);
12531
13231
  if (found.length === 0) continue;
12532
13232
  const items = found.map((finding3) => ({
12533
13233
  ruleId: finding3.ruleId,
@@ -12535,7 +13235,7 @@ var writeDecision = (input, tool) => {
12535
13235
  }));
12536
13236
  return { kind: "block", reason: t("secrets.block.write", { file, found: describeFindings(t, items) }) };
12537
13237
  }
12538
- return ALLOW5;
13238
+ return warningDecision(t, warnings);
12539
13239
  };
12540
13240
  var bashMessage = (t, finding3) => {
12541
13241
  switch (finding3.kind) {
@@ -12549,8 +13249,9 @@ var bashMessage = (t, finding3) => {
12549
13249
  };
12550
13250
  var commitDecision = (input, includeUnstaged) => {
12551
13251
  const staged = scanStaged(input.runtime, input.cwd, includeUnstaged);
12552
- if (!staged.ok || staged.value.length === 0) return ALLOW5;
12553
- const items = staged.value.map((finding3) => ({
13252
+ const found = staged.ok ? blocking(staged.value) : [];
13253
+ if (found.length === 0) return ALLOW5;
13254
+ const items = found.map((finding3) => ({
12554
13255
  ruleId: finding3.ruleId,
12555
13256
  where: input.t("secrets.where.file", { file: finding3.file, line: finding3.line, column: finding3.column })
12556
13257
  }));
@@ -12562,7 +13263,7 @@ var bashDecision = (input) => {
12562
13263
  const segments = segmentsOf(command);
12563
13264
  const [first] = bashFindings(command, segments);
12564
13265
  if (first !== void 0) return { kind: "block", reason: bashMessage(input.t, first) };
12565
- const found = scanText(command);
13266
+ const found = blocking(scanText(command));
12566
13267
  if (found.length > 0) {
12567
13268
  const items = found.map((finding3) => ({ ruleId: finding3.ruleId, where: lineWhere(input.t, finding3) }));
12568
13269
  return { kind: "block", reason: input.t("secrets.block.bash", { found: describeFindings(input.t, items) }) };
@@ -12587,6 +13288,44 @@ var secretsPreToolUse = (input) => {
12587
13288
  return ALLOW5;
12588
13289
  };
12589
13290
 
13291
+ // src/secrets/remotes.ts
13292
+ var USER_AND_SECRET = /^[a-z][a-z0-9+.-]*:\/\/[^\s/@:]+:[^\s/@]+@/i;
13293
+ var USERINFO = /^[a-z][a-z0-9+.-]*:\/\/(?<user>[^\s/@]+)@/i;
13294
+ var hasCredential = (url) => {
13295
+ if (USER_AND_SECRET.test(url)) return true;
13296
+ const user = USERINFO.exec(url)?.groups?.["user"];
13297
+ return user !== void 0 && scanText(user, { rules: CREDENTIAL_RULES }).length > 0;
13298
+ };
13299
+ var parseRemotes = (output) => {
13300
+ const remotes = /* @__PURE__ */ new Map();
13301
+ for (const line2 of output.split(/\r?\n/)) {
13302
+ const [name, url] = line2.trim().split(/\s+/);
13303
+ if (name === void 0 || url === void 0 || name === "") continue;
13304
+ remotes.set(name, [...remotes.get(name) ?? [], url]);
13305
+ }
13306
+ return remotes;
13307
+ };
13308
+ var remotesWithCredentials = (runtime, root) => {
13309
+ const listed4 = runtime.git(["remote", "-v"], root);
13310
+ if (!listed4.ok) return [];
13311
+ return [...parseRemotes(listed4.stdout)].filter(([, urls]) => urls.some(hasCredential)).map(([name]) => name).sort();
13312
+ };
13313
+
13314
+ // src/secrets/guards/session.ts
13315
+ var secretsSessionStart = (input) => {
13316
+ const remotes = remotesWithCredentials(input.runtime, input.cwd);
13317
+ if (remotes.length === 0) return { kind: "allow" };
13318
+ const params2 = { remotes: remotes.join(", "), remote: remotes[0] ?? "origin" };
13319
+ const text6 = input.t("secrets.session.remote_credential", params2);
13320
+ return {
13321
+ kind: "allow",
13322
+ output: {
13323
+ systemMessage: `finitemonkey: ${text6}`,
13324
+ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: text6 }
13325
+ }
13326
+ };
13327
+ };
13328
+
12590
13329
  // src/hooks/handlers.ts
12591
13330
  var allow = () => ({ kind: "allow" });
12592
13331
  var DECISION_RANK = { allow: 0, defer: 0, ask: 1, deny: 2 };
@@ -12640,14 +13379,16 @@ var mergeHandlers = (...sets) => {
12640
13379
  var SECRETS_HANDLERS = {
12641
13380
  PreToolUse: [secretsPreToolUse],
12642
13381
  UserPromptSubmit: [secretsUserPromptSubmit],
12643
- PostToolUse: [secretsPostToolUse]
12644
- };
12645
- var DEFAULT_HANDLERS = mergeHandlers(
12646
- SECRETS_HANDLERS,
12647
- PORTAO_HANDLERS,
12648
- PROSE_HANDLERS,
12649
- GIT_HANDLERS
12650
- );
13382
+ PostToolUse: [secretsPostToolUse],
13383
+ SessionStart: [secretsSessionStart]
13384
+ };
13385
+ var GUARD_SETS = [
13386
+ { id: "secrets", damage: true, handlers: SECRETS_HANDLERS },
13387
+ { id: "gate", damage: true, handlers: PORTAO_HANDLERS },
13388
+ { id: "prose", damage: false, handlers: PROSE_HANDLERS },
13389
+ { id: "git", damage: true, handlers: GIT_HANDLERS }
13390
+ ];
13391
+ var DEFAULT_HANDLERS = mergeHandlers(...GUARD_SETS.map(withPolicy));
12651
13392
 
12652
13393
  // src/hooks/payload.ts
12653
13394
  var EMPTY_PAYLOAD = {
@@ -13058,12 +13799,12 @@ var hookSchemaProblems = (hooks, claude) => {
13058
13799
  };
13059
13800
 
13060
13801
  // src/doctor/hook-sources.ts
13061
- import { join as join28 } from "node:path";
13802
+ import { join as join30 } from "node:path";
13062
13803
  var entriesOf = (source, hooks, own) => Object.entries(asRecord(hooks)).flatMap(
13063
13804
  ([event, groups]) => asList(groups).flatMap((group) => {
13064
- const record2 = asRecord(group);
13065
- const matcher = optionalString(record2, "matcher");
13066
- return asList(record2["hooks"]).filter((handler) => isRecord(handler) && own(handler)).map((handler) => ({ source, event, matcher, handler }));
13805
+ const record3 = asRecord(group);
13806
+ const matcher = optionalString(record3, "matcher");
13807
+ return asList(record3["hooks"]).filter((handler) => isRecord(handler) && own(handler)).map((handler) => ({ source, event, matcher, handler }));
13067
13808
  })
13068
13809
  );
13069
13810
  var PROJECT_SCOPES = { project: "project", local: "local" };
@@ -13071,10 +13812,10 @@ var projectHookFiles = (env) => readSettings(env).flatMap(({ scope: scope2, path
13071
13812
  const source = PROJECT_SCOPES[scope2];
13072
13813
  return source === void 0 || !("hooks" in settings) ? [] : [{ source, path, hooks: settings["hooks"] }];
13073
13814
  });
13074
- var pluginsDir = ({ env, runtime }) => join28(env["CLAUDE_CONFIG_DIR"] ?? join28(runtime.homeDir(), ".claude"), "plugins");
13815
+ var pluginsDir = ({ env, runtime }) => join30(env["CLAUDE_CONFIG_DIR"] ?? join30(runtime.homeDir(), ".claude"), "plugins");
13075
13816
  var installRecord = (value, root) => {
13076
13817
  const records = Array.isArray(value) ? value.filter(isRecord) : isRecord(value) ? [value] : [];
13077
- return records.find((record2) => record2["projectPath"] === root) ?? records[0];
13818
+ return records.find((record3) => record3["projectPath"] === root) ?? records[0];
13078
13819
  };
13079
13820
  var readJson = (env, path) => {
13080
13821
  const text6 = env.runtime.readText(path);
@@ -13082,18 +13823,18 @@ var readJson = (env, path) => {
13082
13823
  return parsed?.ok === true ? parsed.value : void 0;
13083
13824
  };
13084
13825
  var installedPlugin = (env) => {
13085
- const index = readJson(env, toAbsolutePath(join28(pluginsDir(env), "installed_plugins.json")));
13086
- const record2 = installRecord(asRecord(index?.["plugins"])[PLUGIN_ID], env.root);
13087
- const installPath = record2 === void 0 ? void 0 : optionalString(record2, "installPath");
13088
- if (record2 === void 0 || installPath === void 0) return void 0;
13826
+ const index = readJson(env, toAbsolutePath(join30(pluginsDir(env), "installed_plugins.json")));
13827
+ const record3 = installRecord(asRecord(index?.["plugins"])[PLUGIN_ID], env.root);
13828
+ const installPath = record3 === void 0 ? void 0 : optionalString(record3, "installPath");
13829
+ if (record3 === void 0 || installPath === void 0) return void 0;
13089
13830
  const root = toAbsolutePath(installPath);
13090
- const manifest = readJson(env, toAbsolutePath(join28(root, ".claude-plugin", "plugin.json")));
13091
- const hooksPath = toAbsolutePath(join28(root, "hooks", "hooks.json"));
13831
+ const manifest = readJson(env, toAbsolutePath(join30(root, ".claude-plugin", "plugin.json")));
13832
+ const hooksPath = toAbsolutePath(join30(root, "hooks", "hooks.json"));
13092
13833
  const text6 = env.runtime.readText(hooksPath);
13093
13834
  const parsed = text6.ok ? parseJsonObject(text6.value) : void 0;
13094
13835
  return {
13095
13836
  root,
13096
- version: (manifest === void 0 ? void 0 : optionalString(manifest, "version")) ?? optionalString(record2, "version"),
13837
+ version: (manifest === void 0 ? void 0 : optionalString(manifest, "version")) ?? optionalString(record3, "version"),
13097
13838
  hooksFile: parsed?.ok === true ? { source: "plugin", path: hooksPath, hooks: parsed.value["hooks"] } : void 0,
13098
13839
  hooksText: text6.ok ? text6.value : void 0
13099
13840
  };
@@ -13133,7 +13874,7 @@ var schemaFindings = (env, plugin, claude) => {
13133
13874
  const files = [...projectHookFiles(env), ...plugin?.hooksFile === void 0 ? [] : [plugin.hooksFile]];
13134
13875
  const invalidJson = [
13135
13876
  ...unreadableProjectFiles(env),
13136
- ...plugin?.hooksText !== void 0 && plugin.hooksFile === void 0 ? [toAbsolutePath(join29(plugin.root, "hooks", "hooks.json"))] : []
13877
+ ...plugin?.hooksText !== void 0 && plugin.hooksFile === void 0 ? [toAbsolutePath(join31(plugin.root, "hooks", "hooks.json"))] : []
13137
13878
  ];
13138
13879
  return [
13139
13880
  ...invalidJson.flatMap((file) => schemaFinding(file, ["not valid JSON"], label2)),
@@ -13154,7 +13895,7 @@ var pluginFindings = (env, projectOwn) => {
13154
13895
  ];
13155
13896
  };
13156
13897
  var coverageFindings = (env, entries) => {
13157
- const installed = env.runtime.stat(toAbsolutePath(join29(env.root, ...LOCK_FILE.split("/")))) !== void 0;
13898
+ const installed = env.runtime.stat(toAbsolutePath(join31(env.root, ...LOCK_FILE.split("/")))) !== void 0;
13158
13899
  if (entries.length === 0 && !installed) {
13159
13900
  return [finding("hooks.registered", "warn", "doctor.hooks.none", {}, "doctor.hooks.fix_none")];
13160
13901
  }
@@ -13206,17 +13947,17 @@ var checkHooks = (env) => {
13206
13947
  };
13207
13948
 
13208
13949
  // src/doctor/instructions.ts
13209
- import { join as join30 } from "node:path";
13950
+ import { join as join32 } from "node:path";
13210
13951
  var INSTRUCTION_LINE_CEILING = 200;
13211
13952
  var TOP_INSTRUCTIONS = ["AGENTS.md", "CLAUDE.md", ".claude/CLAUDE.md", "CLAUDE.local.md"];
13212
13953
  var RULES_DIR = ".claude/rules";
13213
13954
  var SHOWN = 3;
13214
13955
  var readProjectFile = (env, relative9) => {
13215
- const text6 = env.runtime.readText(toAbsolutePath(join30(env.root, ...relative9.split("/"))));
13956
+ const text6 = env.runtime.readText(toAbsolutePath(join32(env.root, ...relative9.split("/"))));
13216
13957
  return text6.ok ? text6.value : void 0;
13217
13958
  };
13218
13959
  var instructionFiles = (env) => {
13219
- const listed4 = env.runtime.listDir(toAbsolutePath(join30(env.root, ...RULES_DIR.split("/"))));
13960
+ const listed4 = env.runtime.listDir(toAbsolutePath(join32(env.root, ...RULES_DIR.split("/"))));
13220
13961
  const rules = listed4.ok ? listed4.value.filter((name) => name.endsWith(".md")).map((name) => `${RULES_DIR}/${name}`) : [];
13221
13962
  return [...TOP_INSTRUCTIONS, ...rules].flatMap((path) => {
13222
13963
  const text6 = readProjectFile(env, path);
@@ -13321,12 +14062,12 @@ var checkInstructions = (env) => {
13321
14062
  };
13322
14063
 
13323
14064
  // src/doctor/line-endings.ts
13324
- import { join as join31 } from "node:path";
14065
+ import { join as join33 } from "node:path";
13325
14066
  var LF_RULE = /^\*\s.*\beol=lf\b/m;
13326
14067
  var checkLineEndings = ({ root, runtime }) => {
13327
14068
  const autocrlf = runtime.git(["config", "--get", "core.autocrlf"], root);
13328
14069
  if (!autocrlf.ok || autocrlf.stdout.trim().toLowerCase() !== "true") return [];
13329
- const attributes = runtime.readText(toAbsolutePath(join31(root, ".gitattributes")));
14070
+ const attributes = runtime.readText(toAbsolutePath(join33(root, ".gitattributes")));
13330
14071
  if (attributes.ok && LF_RULE.test(attributes.value)) return [];
13331
14072
  return [
13332
14073
  finding("git.autocrlf", "warn", "doctor.git.autocrlf", { file: ".gitattributes" }, "doctor.git.fix_autocrlf")
@@ -13334,7 +14075,7 @@ var checkLineEndings = ({ root, runtime }) => {
13334
14075
  };
13335
14076
 
13336
14077
  // src/doctor/memory.ts
13337
- import { join as join32 } from "node:path";
14078
+ import { join as join34 } from "node:path";
13338
14079
  var ceilingOf = (config) => {
13339
14080
  const value = configAt(parseConfigYaml(config ?? ""), "memory", "maxLines");
13340
14081
  const parsed = typeof value === "string" ? Number.parseInt(value, 10) : Number.NaN;
@@ -13342,9 +14083,9 @@ var ceilingOf = (config) => {
13342
14083
  };
13343
14084
  var lineCount2 = (text6) => text6.replace(/\r?\n$/, "").split(/\r?\n/).length;
13344
14085
  var checkAgentsFile = ({ root, runtime }) => {
13345
- const text6 = runtime.readText(toAbsolutePath(join32(root, AGENTS_FILE)));
14086
+ const text6 = runtime.readText(toAbsolutePath(join34(root, AGENTS_FILE)));
13346
14087
  if (!text6.ok) return [];
13347
- const config = runtime.readText(toAbsolutePath(join32(root, CONFIG_FILE)));
14088
+ const config = runtime.readText(toAbsolutePath(join34(root, CONFIG_FILE)));
13348
14089
  const max = ceilingOf(config.ok ? config.value : void 0);
13349
14090
  const lines3 = lineCount2(text6.value);
13350
14091
  const params2 = { file: AGENTS_FILE, lines: lines3, max };
@@ -13352,7 +14093,7 @@ var checkAgentsFile = ({ root, runtime }) => {
13352
14093
  };
13353
14094
 
13354
14095
  // src/doctor/project-files.ts
13355
- import { join as join33 } from "node:path";
14096
+ import { join as join35 } from "node:path";
13356
14097
  var variants = (base, extensions) => extensions.map((extension) => `${base}${extension}`);
13357
14098
  var SCRIPT = [".js", ".cjs", ".mjs", ".ts", ".cts", ".mts"];
13358
14099
  var DATA = ["", ".json", ".yml", ".yaml"];
@@ -13383,7 +14124,7 @@ var CONFIG_GROUPS = [
13383
14124
  files: [...variants("commitlint.config", SCRIPT), ...variants(".commitlintrc", [...DATA, ".js"])]
13384
14125
  }
13385
14126
  ];
13386
- var present = (env, files) => files.filter((file) => env.runtime.stat(toAbsolutePath(join33(env.root, file)))?.kind === "file");
14127
+ var present = (env, files) => files.filter((file) => env.runtime.stat(toAbsolutePath(join35(env.root, file)))?.kind === "file");
13387
14128
  var crowded = (env, groups, build) => groups.flatMap(({ name, files }) => {
13388
14129
  const found = present(env, files);
13389
14130
  return found.length > 1 ? [build(name, found.join(", "))] : [];
@@ -13407,6 +14148,22 @@ var checkProjectFiles = (env) => [
13407
14148
  )
13408
14149
  ];
13409
14150
 
14151
+ // src/doctor/remotes.ts
14152
+ var checkRemoteCredentials = ({ root, runtime }) => {
14153
+ const remotes = remotesWithCredentials(runtime, root);
14154
+ if (remotes.length === 0) return [];
14155
+ const params2 = { remotes: remotes.join(", "), remote: remotes[0] ?? "origin" };
14156
+ return [
14157
+ finding(
14158
+ "secrets.remote_credential",
14159
+ "warn",
14160
+ "doctor.secrets.remote_credential",
14161
+ params2,
14162
+ "doctor.secrets.fix_remote_credential"
14163
+ )
14164
+ ];
14165
+ };
14166
+
13410
14167
  // src/doctor/run.ts
13411
14168
  var DOCTOR_SCHEMA = "finitemonkey/doctor@1";
13412
14169
  var CHECKS = [
@@ -13422,6 +14179,7 @@ var CHECKS = [
13422
14179
  checkInstructions,
13423
14180
  checkProjectFiles,
13424
14181
  checkLineEndings,
14182
+ checkRemoteCredentials,
13425
14183
  checkAgentsFile,
13426
14184
  checkHooks
13427
14185
  ];
@@ -13449,7 +14207,7 @@ var toItem = (ctx, finding3) => ({
13449
14207
  detail: ctx.t(finding3.messageKey, finding3.params),
13450
14208
  ...finding3.fixKey === void 0 ? {} : { fix: ctx.t(finding3.fixKey, finding3.params) }
13451
14209
  });
13452
- var count2 = (items, status) => items.filter((item) => item.status === status).length;
14210
+ var count3 = (items, status) => items.filter((item) => item.status === status).length;
13453
14211
  var hookItems = (ctx, findings) => findings.flatMap(
13454
14212
  (finding3) => finding3.hook === void 0 ? [] : [
13455
14213
  {
@@ -13465,7 +14223,7 @@ var buildReport2 = (ctx, findings) => {
13465
14223
  schema: DOCTOR_SCHEMA,
13466
14224
  checks,
13467
14225
  hooks: hookItems(ctx, findings),
13468
- summary: { ok: count2(checks, "ok"), warn: count2(checks, "warn"), fail: count2(checks, "fail") }
14226
+ summary: { ok: count3(checks, "ok"), warn: count3(checks, "warn"), fail: count3(checks, "fail") }
13469
14227
  };
13470
14228
  };
13471
14229
 
@@ -13782,7 +14540,7 @@ var resultOf = (ctx, checked) => {
13782
14540
  const problem4 = gitProblem("merge_unanalyzable", "git.error.merge_unanalyzable", detail, "git status");
13783
14541
  return failureFrom(ctx.t, { ...problem4, exitCode: ExitCode.Usage, data }, human2);
13784
14542
  }
13785
- const lost = checked.reduce((count3, item) => count3 + item.report.findings.length, 0);
14543
+ const lost = checked.reduce((count4, item) => count4 + item.report.findings.length, 0);
13786
14544
  if (lost === 0) return success(data, human2);
13787
14545
  return failureFrom(
13788
14546
  ctx.t,
@@ -13845,11 +14603,11 @@ var unreachable = (runtime, repo, shas) => {
13845
14603
  var fsckDangling = (runtime, repo) => {
13846
14604
  const result = runtime.git(["fsck", "--no-reflogs", "--no-progress", "--dangling"], repo.root);
13847
14605
  const lines3 = linesOf(result.stdout);
13848
- const pick3 = (kind) => lines3.flatMap((line2) => {
14606
+ const pick4 = (kind) => lines3.flatMap((line2) => {
13849
14607
  const [label2, type, sha] = line2.split(" ");
13850
14608
  return label2 === "dangling" && type === kind && sha !== void 0 ? [sha] : [];
13851
14609
  });
13852
- return { commits: pick3("commit"), blobs: pick3("blob").length };
14610
+ return { commits: pick4("commit"), blobs: pick4("blob").length };
13853
14611
  };
13854
14612
  var describe = (runtime, repo, shas) => {
13855
14613
  if (shas.length === 0) return /* @__PURE__ */ new Map();
@@ -13949,7 +14707,7 @@ var rescueCommand = {
13949
14707
  };
13950
14708
 
13951
14709
  // src/git/undo.ts
13952
- import { basename as basename3, dirname as dirname8, join as join34 } from "node:path";
14710
+ import { basename as basename3, dirname as dirname8, join as join36 } from "node:path";
13953
14711
  var INDEX_SUBJECT = "finitemonkey index:";
13954
14712
  var gitLine = (runtime, repo, args) => {
13955
14713
  const result = runtime.git(args, repo.root);
@@ -13992,7 +14750,7 @@ var findSnapshot = (views, wanted) => {
13992
14750
  };
13993
14751
  var stampOf2 = (iso) => iso.replace(/[:.]/g, "-");
13994
14752
  var worktreePath = (runtime, repo) => {
13995
- const base = join34(dirname8(repo.root), `${basename3(repo.root)}-undo-${stampOf2(runtime.now())}`);
14753
+ const base = join36(dirname8(repo.root), `${basename3(repo.root)}-undo-${stampOf2(runtime.now())}`);
13996
14754
  return toAbsolutePath(runtime.stat(toAbsolutePath(base)) === void 0 ? base : `${base}-2`);
13997
14755
  };
13998
14756
  var intoWorktree = (runtime, repo, view2) => {
@@ -14005,8 +14763,8 @@ var step2 = (runtime, repo, args) => {
14005
14763
  return result.ok ? accept(void 0) : reject(failure2(args.join(" "), result));
14006
14764
  };
14007
14765
  var inPlace = (runtime, repo, view2) => {
14008
- const current = gitLine(runtime, repo, ["rev-parse", "--verify", "--quiet", "HEAD"]);
14009
- if (view2.head !== void 0 && view2.head !== current) {
14766
+ const current2 = gitLine(runtime, repo, ["rev-parse", "--verify", "--quiet", "HEAD"]);
14767
+ if (view2.head !== void 0 && view2.head !== current2) {
14010
14768
  const moved = step2(runtime, repo, ["reset", "--soft", view2.head]);
14011
14769
  if (!moved.ok) return moved;
14012
14770
  }
@@ -14227,7 +14985,7 @@ var hookCommand = {
14227
14985
  };
14228
14986
 
14229
14987
  // src/commands/i18n-check.ts
14230
- import { join as join35 } from "node:path";
14988
+ import { join as join37 } from "node:path";
14231
14989
 
14232
14990
  // src/i18n/message.ts
14233
14991
  var ARG = /^\s*([a-zA-Z][a-zA-Z0-9_]*)\s*(?:,\s*(plural|select)\s*,)?/;
@@ -14313,17 +15071,17 @@ var rulesFor = (locale) => {
14313
15071
  var branchFor = (node, value, locale) => {
14314
15072
  const { branches } = node;
14315
15073
  if (node.kind === "select") return branches[String(value)] ?? branches["other"];
14316
- const count3 = typeof value === "number" ? value : Number(value);
14317
- if (!Number.isFinite(count3)) return branches["other"];
14318
- return branches[`=${String(count3)}`] ?? branches[rulesFor(locale).select(count3)] ?? branches["other"];
15074
+ const count4 = typeof value === "number" ? value : Number(value);
15075
+ if (!Number.isFinite(count4)) return branches["other"];
15076
+ return branches[`=${String(count4)}`] ?? branches[rulesFor(locale).select(count4)] ?? branches["other"];
14319
15077
  };
14320
- var render = (nodes, params2, locale, count3) => nodes.map((node) => {
15078
+ var render = (nodes, params2, locale, count4) => nodes.map((node) => {
14321
15079
  if (node.kind === "text") return node.text;
14322
- if (node.kind === "count") return count3 === void 0 ? "#" : String(count3);
15080
+ if (node.kind === "count") return count4 === void 0 ? "#" : String(count4);
14323
15081
  const value = params2[node.name];
14324
15082
  if (node.kind === "arg") return value === void 0 ? `{${node.name}}` : String(value);
14325
15083
  const branch = branchFor(node, value, locale) ?? [];
14326
- return render(branch, params2, locale, node.kind === "plural" ? value : count3);
15084
+ return render(branch, params2, locale, node.kind === "plural" ? value : count4);
14327
15085
  }).join("");
14328
15086
  var formatMessage = (source, params2, locale) => render(parseMessage(source), params2, locale, void 0);
14329
15087
  var walk = (nodes, visit2) => {
@@ -14353,7 +15111,7 @@ var choicesOf = (source) => {
14353
15111
  var requiredPluralBranches = (locale) => {
14354
15112
  const rules = rulesFor(locale);
14355
15113
  const seen = /* @__PURE__ */ new Set(["other"]);
14356
- for (let count3 = 0; count3 <= 1e3; count3 += 1) seen.add(rules.select(count3));
15114
+ for (let count4 = 0; count4 <= 1e3; count4 += 1) seen.add(rules.select(count4));
14357
15115
  return [...seen].sort();
14358
15116
  };
14359
15117
 
@@ -14429,7 +15187,7 @@ var unreadable = (dir, file, detail) => ({
14429
15187
  exitCode: ExitCode.NotConfigured
14430
15188
  });
14431
15189
  var readCatalog = (ctx, dir, lang) => {
14432
- const file = toAbsolutePath(join35(dir, `${lang}.json`));
15190
+ const file = toAbsolutePath(join37(dir, `${lang}.json`));
14433
15191
  const text6 = ctx.runtime.readText(file);
14434
15192
  if (!text6.ok) return reject(unreadable(dir, file, text6.error));
14435
15193
  const parsed = parseJsonObject(text6.value);
@@ -14452,7 +15210,7 @@ var describeProblem = (ctx, problem4) => {
14452
15210
  return ` ${problem4.lang} ${problem4.key} ${kind}`;
14453
15211
  };
14454
15212
  var loadReview = (ctx, dir) => {
14455
- const text6 = ctx.runtime.readText(toAbsolutePath(join35(dir, "review.json")));
15213
+ const text6 = ctx.runtime.readText(toAbsolutePath(join37(dir, "review.json")));
14456
15214
  const parsed = text6.ok ? parseJsonObject(text6.value) : void 0;
14457
15215
  if (parsed?.ok !== true) return void 0;
14458
15216
  const statuses = (lang) => {
@@ -14493,10 +15251,10 @@ var i18nCheckCommand = {
14493
15251
  };
14494
15252
 
14495
15253
  // src/history/look-back.ts
14496
- import { join as join36 } from "node:path";
15254
+ import { join as join38 } from "node:path";
14497
15255
 
14498
15256
  // src/history/patterns.ts
14499
- import { posix as posix4 } from "node:path";
15257
+ import { posix as posix5 } from "node:path";
14500
15258
  var PATTERNS = ["done_after_failing_tests", "env_read", "no_verify", "force_push", "test_skip"];
14501
15259
  var TEST_COMMAND = /(^|[\s;&|(])((npm|pnpm|yarn|bun)\s+(run\s+)?(test|check)\b|npx\s+(vitest|jest)\b|vitest\b|jest\b|pytest\b|go\s+test\b|cargo\s+test\b|node\s+--test\b|mocha\b)/;
14502
15260
  var DONE_CLAIM = /\b(done|all (tests|checks) (pass|passed|are passing)|everything (passes|works)|it works|pronto|feito|conclu[ií]do|listo|terminado)\b/i;
@@ -14518,7 +15276,7 @@ var stringField = (tool, name) => {
14518
15276
  return typeof value === "string" ? value : "";
14519
15277
  };
14520
15278
  var isEnvPath = (path) => {
14521
- const name = posix4.basename(path.replace(/\\/g, "/"));
15279
+ const name = posix5.basename(path.replace(/\\/g, "/"));
14522
15280
  return /^\.env(\.[A-Za-z0-9_-]+)?$/.test(name) && !ENV_TEMPLATE.test(name);
14523
15281
  };
14524
15282
  var envRead = (tool) => {
@@ -14606,7 +15364,7 @@ var addResults = (draft, blocks) => {
14606
15364
  };
14607
15365
  var parseTranscript = (jsonl) => {
14608
15366
  const turns = [];
14609
- let current;
15367
+ let current2;
14610
15368
  for (const line2 of jsonl.split(/\r?\n/)) {
14611
15369
  if (line2.trim() === "") continue;
14612
15370
  const parsed = parseJsonObject(line2);
@@ -14614,13 +15372,13 @@ var parseTranscript = (jsonl) => {
14614
15372
  const entry2 = parsed.value;
14615
15373
  const blocks = blocksOf(entry2["message"]);
14616
15374
  if (entry2["type"] === "user") {
14617
- if (current !== void 0 && addResults(current, blocks)) continue;
15375
+ if (current2 !== void 0 && addResults(current2, blocks)) continue;
14618
15376
  const prompt = blocks.map(textOf3).join("\n").trim();
14619
15377
  if (prompt === "") continue;
14620
- current = startTurn(prompt);
14621
- turns.push(current);
14622
- } else if (entry2["type"] === "assistant" && current !== void 0) {
14623
- addAssistant(current, blocks);
15378
+ current2 = startTurn(prompt);
15379
+ turns.push(current2);
15380
+ } else if (entry2["type"] === "assistant" && current2 !== void 0) {
15381
+ addAssistant(current2, blocks);
14624
15382
  }
14625
15383
  }
14626
15384
  return turns;
@@ -14629,7 +15387,7 @@ var parseTranscript = (jsonl) => {
14629
15387
  // src/history/look-back.ts
14630
15388
  var SAMPLES = 3;
14631
15389
  var projectLogKey = (root) => root.replace(/[^A-Za-z0-9-]/g, "-");
14632
- var projectLogDir = (runtime, root, env) => join36(env["CLAUDE_CONFIG_DIR"] ?? join36(runtime.homeDir(), ".claude"), "projects", projectLogKey(root));
15390
+ var projectLogDir = (runtime, root, env) => join38(env["CLAUDE_CONFIG_DIR"] ?? join38(runtime.homeDir(), ".claude"), "projects", projectLogKey(root));
14633
15391
  var lookBack = (runtime, root, env) => {
14634
15392
  const dir = projectLogDir(runtime, root, env);
14635
15393
  const listed4 = runtime.listDir(toAbsolutePath(dir));
@@ -14650,7 +15408,7 @@ var lookBack = (runtime, root, env) => {
14650
15408
  };
14651
15409
  let turns = 0;
14652
15410
  for (const name of files) {
14653
- const text6 = runtime.readText(toAbsolutePath(join36(dir, name)));
15411
+ const text6 = runtime.readText(toAbsolutePath(join38(dir, name)));
14654
15412
  if (!text6.ok) continue;
14655
15413
  for (const turn of parseTranscript(text6.value)) {
14656
15414
  turns += 1;
@@ -14666,6 +15424,7 @@ var lookBack = (runtime, root, env) => {
14666
15424
 
14667
15425
  // src/install/diff.ts
14668
15426
  var MAX_DIFF_LINES = 120;
15427
+ var MAX_LCS_CELLS = 4e6;
14669
15428
  var table = (a, b) => {
14670
15429
  const rows = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
14671
15430
  for (let i = a.length - 1; i >= 0; i -= 1) {
@@ -14700,8 +15459,27 @@ var walk2 = (a, b) => {
14700
15459
  return ops;
14701
15460
  };
14702
15461
  var splitLines = (text6) => text6 === "" ? [] : text6.replace(/\r?\n$/, "").split(/\r?\n/);
15462
+ var sharedPrefix = (a, b) => {
15463
+ let count4 = 0;
15464
+ while (count4 < a.length && count4 < b.length && a[count4] === b[count4]) count4 += 1;
15465
+ return count4;
15466
+ };
15467
+ var sharedSuffix = (a, b, prefix) => {
15468
+ let count4 = 0;
15469
+ while (count4 < a.length - prefix && count4 < b.length - prefix && a[a.length - 1 - count4] === b[b.length - 1 - count4])
15470
+ count4 += 1;
15471
+ return count4;
15472
+ };
15473
+ var changedOps = (a, b) => {
15474
+ const prefix = sharedPrefix(a, b);
15475
+ const suffix = sharedSuffix(a, b, prefix);
15476
+ const left = a.slice(prefix, a.length - suffix);
15477
+ const right = b.slice(prefix, b.length - suffix);
15478
+ if (left.length * right.length <= MAX_LCS_CELLS) return walk2(left, right).filter(([op]) => op !== "=");
15479
+ return [...left.map((line2) => ["-", line2]), ...right.map((line2) => ["+", line2])];
15480
+ };
14703
15481
  var lineDiff = (path, before, after) => {
14704
- const changes = walk2(splitLines(before), splitLines(after)).filter(([op]) => op !== "=");
15482
+ const changes = changedOps(splitLines(before), splitLines(after));
14705
15483
  if (changes.length === 0) return [];
14706
15484
  const lines3 = changes.map(([op, line2]) => `${op} ${line2}`);
14707
15485
  const shown3 = lines3.slice(0, MAX_DIFF_LINES);
@@ -14846,6 +15624,32 @@ var initCommand = {
14846
15624
  }
14847
15625
  };
14848
15626
 
15627
+ // src/commands/policy.ts
15628
+ var FIX = `finitemonkey policy false-positive <${OPTIONAL_GUARDS.join("|")}>`;
15629
+ var recordFalsePositive = (ctx, guard) => {
15630
+ if (!isPerson(ctx)) return failureFrom(ctx.t, needsPersonProblem("policy.error.person_only", FIX));
15631
+ if (!OPTIONAL_GUARDS.some((id) => id === guard)) {
15632
+ return failureFrom(ctx.t, usageProblem("invalid_arguments", "error.invalid_arguments", FIX, { detail: guard }));
15633
+ }
15634
+ const paths = fmPaths(findRepoRoot(ctx.runtime, ctx.cwd) ?? ctx.cwd);
15635
+ const state = readShadow(ctx.runtime, paths);
15636
+ const before = state[guard] ?? { firings: 0, falsePositives: 0, promoted: false };
15637
+ const falsePositives = before.falsePositives + 1;
15638
+ writeShadow(ctx.runtime, paths, { ...state, [guard]: { ...before, falsePositives } });
15639
+ logEvent(ctx.runtime, paths, "guard_false_positive", { guard, falsePositives });
15640
+ return success({ guard, falsePositives }, [
15641
+ ctx.t("policy.false_positive.recorded", { guard, count: falsePositives })
15642
+ ]);
15643
+ };
15644
+ var policyFalsePositiveCommand = {
15645
+ path: ["policy", "false-positive"],
15646
+ summaryKey: "command.policy_false_positive.summary",
15647
+ usage: FIX,
15648
+ status: "ready",
15649
+ maxPositionals: 1,
15650
+ run: (ctx, args) => Promise.resolve(recordFalsePositive(ctx, args.positionals[0] ?? ""))
15651
+ };
15652
+
14849
15653
  // src/commands/prose-check.ts
14850
15654
  import { relative as relative8 } from "node:path";
14851
15655
 
@@ -15208,7 +16012,7 @@ var view = (finding3, status) => ({
15208
16012
  status,
15209
16013
  ...finding3.commit === void 0 ? {} : { commit: finding3.commit }
15210
16014
  });
15211
- var modeOf2 = (values) => {
16015
+ var modeOf3 = (values) => {
15212
16016
  const chosen = [values["staged"] === true, values["history"] === true, stringOption(values, "path") !== void 0];
15213
16017
  if (chosen.filter(Boolean).length > 1) return void 0;
15214
16018
  if (chosen[0] === true) return "staged";
@@ -15274,7 +16078,7 @@ var humanOf2 = (ctx, data) => [
15274
16078
  ];
15275
16079
  var resultOf3 = (ctx, data) => {
15276
16080
  const human2 = humanOf2(ctx, data);
15277
- const failing = data.summary.new + data.summary.expired;
16081
+ const failing = data.findings.filter((item) => item.status !== "known" && item.severity !== "warning").length;
15278
16082
  if (failing === 0) return success(data, human2);
15279
16083
  return failureFrom(
15280
16084
  ctx.t,
@@ -15299,7 +16103,7 @@ var secretsScanCommand = {
15299
16103
  status: "ready",
15300
16104
  options: { staged: { type: "boolean" }, history: { type: "boolean" }, path: { type: "string" } },
15301
16105
  run: async (ctx, args) => {
15302
- const mode = modeOf2(args.values);
16106
+ const mode = modeOf3(args.values);
15303
16107
  const fix = "finitemonkey help secrets scan";
15304
16108
  if (mode === void 0) return failureFrom(ctx.t, usageProblem("invalid_arguments", "secrets.error.one_mode", fix));
15305
16109
  const root = projectRoot(ctx);
@@ -15321,7 +16125,7 @@ var secretsScanCommand = {
15321
16125
  };
15322
16126
 
15323
16127
  // src/task/create.ts
15324
- import { join as join37 } from "node:path";
16128
+ import { join as join39 } from "node:path";
15325
16129
 
15326
16130
  // src/core/yaml/write.ts
15327
16131
  var PLAIN_SAFE = /^[A-Za-z_./][A-Za-z0-9_./ ()-]*$/;
@@ -15380,7 +16184,8 @@ var contractYaml = (contract) => ({
15380
16184
  tests_protected: contract.scope.testsProtected
15381
16185
  },
15382
16186
  acceptance: contract.acceptance.map(checkYaml),
15383
- decisions_needed: [...contract.decisionsNeeded]
16187
+ decisions_needed: [...contract.decisionsNeeded],
16188
+ ...contract.callersIgnore === void 0 ? {} : { callers_ignore: [...contract.callersIgnore] }
15384
16189
  });
15385
16190
  var contractJson = (contract) => ({
15386
16191
  id: contract.id,
@@ -15405,10 +16210,10 @@ var explicitChecks = (draft) => [
15405
16210
  var draftChecks = (runtime, paths, draft) => {
15406
16211
  const disk = {
15407
16212
  read: (relative9) => {
15408
- const text6 = runtime.readText(toAbsolutePath(join37(paths.root, relative9)));
16213
+ const text6 = runtime.readText(toAbsolutePath(join39(paths.root, relative9)));
15409
16214
  return text6.ok ? text6.value : void 0;
15410
16215
  },
15411
- exists: (relative9) => runtime.stat(toAbsolutePath(join37(paths.root, relative9))) !== void 0
16216
+ exists: (relative9) => runtime.stat(toAbsolutePath(join39(paths.root, relative9))) !== void 0
15412
16217
  };
15413
16218
  const detected = detectChecks(disk);
15414
16219
  const base = draft.runs.length > 0 || draft.tests !== void 0 ? explicitChecks(draft) : detected;
@@ -15421,6 +16226,7 @@ var draftChecks = (runtime, paths, draft) => {
15421
16226
  { id: "prose", kind: "prose" },
15422
16227
  { id: "worktree", kind: "worktree" },
15423
16228
  { id: "merge", kind: "merge" },
16229
+ { id: "callers", kind: "callers" },
15424
16230
  ...draft.paths.length > 0 || draft.budget !== void 0 ? [{ id: "diff", kind: "diff" }] : [],
15425
16231
  ...draft.git.length > 0 ? [{ id: "git", kind: "git", require: draft.git }] : [],
15426
16232
  { id: "pr", kind: "pr", requireOpen: draft.git.includes("pr_open") },
@@ -15610,15 +16416,15 @@ var taskDoneCommand = {
15610
16416
  };
15611
16417
 
15612
16418
  // src/install/uninstall.ts
15613
- var structural = (entry2, current) => {
16419
+ var structural = (entry2, current2) => {
15614
16420
  if (entry2.kind === "gitignore" || entry2.kind === "markdown") {
15615
- const text6 = entry2.kind === "gitignore" ? withoutBlock(current) : withoutMarkdownBlock(current);
16421
+ const text6 = entry2.kind === "gitignore" ? withoutBlock(current2) : withoutMarkdownBlock(current2);
15616
16422
  return text6 === "" && !entry2.existed ? void 0 : text6;
15617
16423
  }
15618
- const parsed = parseSettings(current);
15619
- if (!parsed.ok || entry2.added === void 0) return current;
16424
+ const parsed = parseSettings(current2);
16425
+ if (!parsed.ok || entry2.added === void 0) return current2;
15620
16426
  const settings = unmergeSettings(parsed.value, entry2.added, entry2.handlers ?? []);
15621
- return Object.keys(settings).length === 0 && !entry2.existed ? void 0 : renderJson(settings, styleOf(current));
16427
+ return Object.keys(settings).length === 0 && !entry2.existed ? void 0 : renderJson(settings, styleOf(current2));
15622
16428
  };
15623
16429
  var unchanged = (project, entry2) => {
15624
16430
  if (!entry2.existed) return { path: entry2.path, action: "delete", after: void 0 };
@@ -15626,17 +16432,17 @@ var unchanged = (project, entry2) => {
15626
16432
  return original === void 0 ? void 0 : { path: entry2.path, action: "restore", after: original };
15627
16433
  };
15628
16434
  var removalFor = (project, entry2, options) => {
15629
- const current = project.read(entry2.path);
15630
- if (current === void 0) return { path: entry2.path, action: "gone", after: void 0 };
16435
+ const current2 = project.read(entry2.path);
16436
+ if (current2 === void 0) return { path: entry2.path, action: "gone", after: void 0 };
15631
16437
  if (entry2.kind === "generated" && options.keepGenerated) {
15632
- return { path: entry2.path, action: "keep-generated", after: current };
16438
+ return { path: entry2.path, action: "keep-generated", after: current2 };
15633
16439
  }
15634
- if (hashText(current) === entry2.hash) {
16440
+ if (hashText(current2) === entry2.hash) {
15635
16441
  const exact2 = unchanged(project, entry2);
15636
16442
  if (exact2 !== void 0) return exact2;
15637
16443
  }
15638
- if (entry2.kind === "generated") return { path: entry2.path, action: "keep-edited", after: current };
15639
- const after = structural(entry2, current);
16444
+ if (entry2.kind === "generated") return { path: entry2.path, action: "keep-edited", after: current2 };
16445
+ const after = structural(entry2, current2);
15640
16446
  return { path: entry2.path, action: after === void 0 ? "delete" : "unmerge", after };
15641
16447
  };
15642
16448
  var planRemoval = (project, lock, options) => ({
@@ -15801,6 +16607,7 @@ var REGISTRY = [
15801
16607
  taskDoneCommand,
15802
16608
  secretsScanCommand,
15803
16609
  secretsAcceptCommand,
16610
+ policyFalsePositiveCommand,
15804
16611
  secretsIncidentCommand,
15805
16612
  proseCheckCommand,
15806
16613
  snapshotCommand,
@@ -15812,7 +16619,7 @@ var REGISTRY = [
15812
16619
  ];
15813
16620
 
15814
16621
  // src/i18n/lang-settings.ts
15815
- import { join as join38 } from "node:path";
16622
+ import { join as join40 } from "node:path";
15816
16623
  var read3 = (runtime, path) => {
15817
16624
  const text6 = runtime.readText(toAbsolutePath(path));
15818
16625
  return text6.ok ? text6.value : void 0;
@@ -15827,14 +16634,14 @@ var languageIn = (text6) => {
15827
16634
  const value = parsed.ok && isRecord(parsed.value) ? parsed.value["language"] : void 0;
15828
16635
  return typeof value === "string" && value.trim() !== "" ? value : void 0;
15829
16636
  };
15830
- var claudeDir = (runtime, env) => env["CLAUDE_CONFIG_DIR"] ?? join38(runtime.homeDir(), ".claude");
16637
+ var claudeDir = (runtime, env) => env["CLAUDE_CONFIG_DIR"] ?? join40(runtime.homeDir(), ".claude");
15831
16638
  var langSettings = (runtime, cwd, env, platform) => {
15832
16639
  const root = findGitRoot(cwd, runtime) ?? cwd;
15833
- const config = parseConfigYaml(read3(runtime, join38(root, CONFIG_FILE)) ?? "");
16640
+ const config = parseConfigYaml(read3(runtime, join40(root, CONFIG_FILE)) ?? "");
15834
16641
  const claudeFiles = [
15835
- join38(root, ".claude", "settings.local.json"),
15836
- join38(root, ".claude", "settings.json"),
15837
- join38(claudeDir(runtime, env), "settings.json")
16642
+ join40(root, ".claude", "settings.local.json"),
16643
+ join40(root, ".claude", "settings.json"),
16644
+ join40(claudeDir(runtime, env), "settings.json")
15838
16645
  ];
15839
16646
  return {
15840
16647
  config: configText(config, "lang"),