finitemonkey 0.1.0-next.15 → 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 })}
@@ -2014,11 +2170,11 @@ var maskDeep = (value) => {
2014
2170
  };
2015
2171
 
2016
2172
  // src/task/paths.ts
2017
- import { join as join4 } from "node:path";
2173
+ import { join as join6 } from "node:path";
2018
2174
  var FM_DIR = ".finitemonkey";
2019
2175
  var fmPaths = (root) => {
2020
- const dir = toAbsolutePath(join4(root, FM_DIR));
2021
- const at = (...parts) => toAbsolutePath(join4(dir, ...parts));
2176
+ const dir = toAbsolutePath(join6(root, FM_DIR));
2177
+ const at = (...parts) => toAbsolutePath(join6(dir, ...parts));
2022
2178
  return {
2023
2179
  root,
2024
2180
  dir,
@@ -2254,16 +2410,16 @@ var reportMarkdown = (env, ctx, envelope) => {
2254
2410
  ];
2255
2411
  return maskText(withoutHome(lines3.join("\n"), env.runtime.homeDir()));
2256
2412
  };
2257
- 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`));
2258
2414
 
2259
2415
  // src/install/plugin-yield.ts
2260
- import { join as join6 } from "node:path";
2416
+ import { join as join8 } from "node:path";
2261
2417
  var PLUGIN_VIA = "plugin";
2262
2418
  var YIELD_STDOUT = `${JSON.stringify({ continue: true })}
2263
2419
  `;
2264
2420
  var projectOwnsHooks = (ctx) => {
2265
2421
  const root = ctx.env["CLAUDE_PROJECT_DIR"] ?? ctx.cwd;
2266
- const text6 = ctx.runtime.readText(toAbsolutePath(join6(root, ...SETTINGS_FILE.split("/"))));
2422
+ const text6 = ctx.runtime.readText(toAbsolutePath(join8(root, ...SETTINGS_FILE.split("/"))));
2267
2423
  if (!text6.ok) return false;
2268
2424
  const parsed = parseJsonObject(text6.value);
2269
2425
  return parsed.ok && ownHandlersIn(parsed.value) > 0;
@@ -2277,7 +2433,7 @@ var STABLE_PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
2277
2433
  var NEXT_PLUGIN_ID = `${PLUGIN_NAME}@${NEXT_MARKETPLACE_NAME}`;
2278
2434
 
2279
2435
  // src/doctor/settings.ts
2280
- import { join as join7 } from "node:path";
2436
+ import { join as join9 } from "node:path";
2281
2437
  var MANAGED = {
2282
2438
  darwin: "/Library/Application Support/ClaudeCode/managed-settings.json",
2283
2439
  linux: "/etc/claude-code/managed-settings.json",
@@ -2286,7 +2442,7 @@ var MANAGED = {
2286
2442
  var userSettingsPath = ({ env, runtime }) => {
2287
2443
  const configDir = env["CLAUDE_CONFIG_DIR"];
2288
2444
  return toAbsolutePath(
2289
- 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")
2290
2446
  );
2291
2447
  };
2292
2448
  var settingsPaths = (env) => {
@@ -2294,8 +2450,8 @@ var settingsPaths = (env) => {
2294
2450
  return [
2295
2451
  ...managed === void 0 ? [] : [{ scope: "managed", path: toAbsolutePath(managed) }],
2296
2452
  { scope: "user", path: userSettingsPath(env) },
2297
- { scope: "project", path: toAbsolutePath(join7(env.root, ".claude", "settings.json")) },
2298
- { 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")) }
2299
2455
  ];
2300
2456
  };
2301
2457
  var readSettings = (env) => settingsPaths(env).flatMap(({ scope: scope2, path }) => {
@@ -2439,7 +2595,7 @@ var checkClaudeSettings = (env) => {
2439
2595
  };
2440
2596
 
2441
2597
  // src/doctor/environment.ts
2442
- import { join as join8 } from "node:path";
2598
+ import { join as join10 } from "node:path";
2443
2599
  var AUTH_VARIABLES = [
2444
2600
  "ANTHROPIC_API_KEY",
2445
2601
  "ANTHROPIC_AUTH_TOKEN",
@@ -2477,7 +2633,7 @@ var pathDirs = ({ env, platform }) => {
2477
2633
  var copiesOnPath = (env) => {
2478
2634
  const names = EXECUTABLES[env.platform] ?? ["finitemonkey"];
2479
2635
  const found = pathDirs(env).flatMap(
2480
- (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))
2481
2637
  );
2482
2638
  const byDir = new Map(found.map((path) => [path.replace(/\.(cmd|exe|ps1)$/i, ""), path]));
2483
2639
  return [...byDir.values()];
@@ -2511,7 +2667,7 @@ var checkAuthVariables = ({ env, platform }) => {
2511
2667
  };
2512
2668
 
2513
2669
  // src/doctor/hooks.ts
2514
- import { join as join29 } from "node:path";
2670
+ import { join as join31 } from "node:path";
2515
2671
 
2516
2672
  // src/hooks/events.ts
2517
2673
  var HOOK_EVENTS = [
@@ -2558,9 +2714,9 @@ var deadlineFor = (event, raw) => {
2558
2714
  };
2559
2715
 
2560
2716
  // src/hooks/fields.ts
2561
- var textField = (record2, ...names) => {
2717
+ var textField = (record3, ...names) => {
2562
2718
  for (const name of names) {
2563
- const value = record2[name];
2719
+ const value = record3[name];
2564
2720
  if (typeof value === "string") return value;
2565
2721
  }
2566
2722
  return void 0;
@@ -2655,8 +2811,8 @@ var closeTask = (state, id) => assemble({
2655
2811
 
2656
2812
  // src/guards/release/grants.ts
2657
2813
  var GRANT_MINUTES = 30;
2658
- var text2 = (record2, key) => {
2659
- const value = record2[key];
2814
+ var text2 = (record3, key) => {
2815
+ const value = record3[key];
2660
2816
  return typeof value === "string" ? value : void 0;
2661
2817
  };
2662
2818
  var toGrant = (value) => {
@@ -3298,10 +3454,10 @@ var PS_ENV = /^\$env:([A-Za-z_][A-Za-z0-9_]*)=?(.*)$/is;
3298
3454
  var CHANGES_DIR = /* @__PURE__ */ new Set(["cd", "pushd", "set-location", "push-location"]);
3299
3455
  var lex = (text6, language) => language === "bash" ? lexBash(text6) : lexPowerShell(text6, language === "cmd");
3300
3456
  var isWindowsAbsolute = (path) => /^[A-Za-z]:\//.test(path);
3301
- var joinDir = (current, target) => {
3457
+ var joinDir = (current2, target) => {
3302
3458
  const next2 = toPosixPath(target);
3303
3459
  if (next2.startsWith("/") || isWindowsAbsolute(next2) || next2.startsWith("~")) return next2;
3304
- return current === void 0 ? posix.normalize(next2) : posix.join(current, next2);
3460
+ return current2 === void 0 ? posix.normalize(next2) : posix.join(current2, next2);
3305
3461
  };
3306
3462
  var assignments = (words, language) => {
3307
3463
  if (language !== "bash") {
@@ -3414,9 +3570,9 @@ var COMMIT_VALUE_SHORTS = /* @__PURE__ */ new Set(["m", "F", "C", "c", "t"]);
3414
3570
  var VERIFY_SUBCOMMANDS = /* @__PURE__ */ new Set(["commit", "push", "merge", "am", "rebase", "cherry-pick", "revert", "pull"]);
3415
3571
  var isHooksPath = (value) => /^core\.hookspath(=|$)/i.test(value);
3416
3572
  var envFindings = (command) => {
3417
- const pairs = Object.entries(command.env);
3573
+ const pairs2 = Object.entries(command.env);
3418
3574
  const exported = command.argv[0] === "export" ? command.argv.slice(1).map((arg) => arg.split("=", 2)) : [];
3419
- 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}` }));
3420
3576
  };
3421
3577
  var splitGit = (args) => {
3422
3578
  let index = 0;
@@ -3574,7 +3730,7 @@ var selfGuard = (input) => {
3574
3730
  };
3575
3731
 
3576
3732
  // src/guards/tests/hook.ts
3577
- import { join as join9, relative as relative3 } from "node:path";
3733
+ import { join as join11, relative as relative3 } from "node:path";
3578
3734
 
3579
3735
  // src/guards/tests/patterns.ts
3580
3736
  var JS_TEST = /(^|\/)(__tests__|__test__)\/|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)tests?\/.*\.[cm]?[jt]sx?$/;
@@ -3764,7 +3920,7 @@ var shellViolations = (input, root) => {
3764
3920
  return parseShell(command).flatMap(
3765
3921
  (simple) => writtenPaths(simple).flatMap((path) => {
3766
3922
  const local = localPath(root, path);
3767
- const exists = input.runtime.stat(toAbsolutePath(join9(root, local))) !== void 0;
3923
+ const exists = input.runtime.stat(toAbsolutePath(join11(root, local))) !== void 0;
3768
3924
  if (!exists) return [];
3769
3925
  if (isProtectedConfig(local)) return [{ kind: "config_changed", path: local }];
3770
3926
  return isTestFile(local) ? [{ kind: "test_removed", path: local }] : [];
@@ -3784,20 +3940,20 @@ var testsGuard = (input) => {
3784
3940
  };
3785
3941
 
3786
3942
  // src/gate/hooks/config.ts
3787
- import { join as join11 } from "node:path";
3943
+ import { join as join13 } from "node:path";
3788
3944
 
3789
3945
  // src/gate/hooks/common.ts
3790
- import { dirname as dirname5, join as join10 } from "node:path";
3946
+ import { dirname as dirname5, join as join12 } from "node:path";
3791
3947
  var DEFAULT_GATE_BUDGET_MS = 3e5;
3792
3948
  var sessionCwd = (input) => input.payload.cwd === void 0 ? input.cwd : toAbsolutePath(input.payload.cwd, input.cwd);
3793
3949
  var MAX_DEPTH2 = 64;
3794
3950
  var findRepoRoot = (runtime, start) => {
3795
- let current = start;
3951
+ let current2 = start;
3796
3952
  for (let depth = 0; depth < MAX_DEPTH2; depth += 1) {
3797
- if (runtime.stat(toAbsolutePath(join10(current, ".git"))) !== void 0) return current;
3798
- const parent = toAbsolutePath(dirname5(current));
3799
- if (parent === current) return void 0;
3800
- 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;
3801
3957
  }
3802
3958
  return void 0;
3803
3959
  };
@@ -3817,7 +3973,7 @@ var contextOutput = (event, additionalContext, systemMessage) => ({
3817
3973
  var human = (text6) => `finitemonkey: ${text6}`;
3818
3974
 
3819
3975
  // src/gate/hooks/config.ts
3820
- var CONFIG_FILE2 = "config-hooks.json";
3976
+ var CONFIG_FILE3 = "config-hooks.json";
3821
3977
  var GUARDED_SOURCES = /* @__PURE__ */ new Set(["user_settings", "project_settings", "local_settings"]);
3822
3978
  var countIn = (value) => {
3823
3979
  if (typeof value === "string") return /\bfinitemonkey\b|\bfm hook\b/.test(value) ? 1 : 0;
@@ -3831,20 +3987,20 @@ var censusOf = (text6) => {
3831
3987
  return { hooks: countIn(parsed.value["hooks"]), disabled: parsed.value["disableAllHooks"] === true };
3832
3988
  };
3833
3989
  var settingsFiles = (paths, env) => [
3834
- toAbsolutePath(join11(paths.root, ".claude", "settings.json")),
3835
- toAbsolutePath(join11(paths.root, ".claude", "settings.local.json")),
3836
- ...env["HOME"] === void 0 ? [] : [toAbsolutePath(join11(env["HOME"], ".claude", "settings.json"))]
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"))]
3837
3993
  ];
3838
3994
  var read2 = (runtime, path) => {
3839
3995
  const text6 = runtime.readText(path);
3840
3996
  return text6.ok ? text6.value : void 0;
3841
3997
  };
3842
3998
  var known = (runtime, paths) => {
3843
- const raw = readJsonFile(runtime, paths.cache(CONFIG_FILE2))?.["hooks"];
3999
+ const raw = readJsonFile(runtime, paths.cache(CONFIG_FILE3))?.["hooks"];
3844
4000
  return isRecord(raw) ? Object.fromEntries(Object.entries(raw).filter((entry2) => typeof entry2[1] === "number")) : {};
3845
4001
  };
3846
4002
  var save = (runtime, paths, counts) => {
3847
- writeJsonFile(runtime, paths.cache(CONFIG_FILE2), { hooks: { ...counts } });
4003
+ writeJsonFile(runtime, paths.cache(CONFIG_FILE3), { hooks: { ...counts } });
3848
4004
  };
3849
4005
  var recordConfigHooks = (runtime, paths, env) => {
3850
4006
  const counts = Object.fromEntries(
@@ -3969,6 +4125,7 @@ var implicitContract = (files) => {
3969
4125
  { id: "prose", kind: "prose" },
3970
4126
  { id: "worktree", kind: "worktree" },
3971
4127
  { id: "merge", kind: "merge" },
4128
+ { id: "callers", kind: "callers" },
3972
4129
  { id: "pr", kind: "pr", requireOpen: false }
3973
4130
  ],
3974
4131
  decisionsNeeded: [],
@@ -3977,93 +4134,7 @@ var implicitContract = (files) => {
3977
4134
  };
3978
4135
 
3979
4136
  // src/gate/prepare.ts
3980
- import { join as join14 } from "node:path";
3981
-
3982
- // src/git/project-config.ts
3983
- import { join as join12 } from "node:path";
3984
- var CONFIG_FILE3 = ".finitemonkey/config.yml";
3985
- var unquote = (value) => value.trim().replace(/^(["'])(.*)\1$/s, "$2");
3986
- var stripComment = (line2) => {
3987
- let quote;
3988
- for (let index = 0; index < line2.length; index += 1) {
3989
- const char = line2.charAt(index);
3990
- if (quote === void 0 && (char === '"' || char === "'")) quote = char;
3991
- else if (char === quote) quote = void 0;
3992
- else if (quote === void 0 && char === "#" && (index === 0 || /\s/.test(line2.charAt(index - 1)))) {
3993
- return line2.slice(0, index);
3994
- }
3995
- }
3996
- return line2;
3997
- };
3998
- var flowList = (value) => {
3999
- const trimmed = value.trim();
4000
- if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return void 0;
4001
- return trimmed.slice(1, -1).split(",").map(unquote).filter((item) => item !== "");
4002
- };
4003
- var keyValue = (body2) => {
4004
- const match = /^("[^"]*"|'[^']*'|[^:]+?)\s*:(?:\s+(.*))?$/.exec(body2);
4005
- if (match === null) return void 0;
4006
- return { key: unquote(match[1] ?? ""), value: match[2] ?? "" };
4007
- };
4008
- var addItem = (frame, item) => {
4009
- if (frame.key === void 0) return;
4010
- const parent = frame.tree[frame.key];
4011
- if (Array.isArray(parent)) parent.push(item);
4012
- else frame.tree[frame.key] = [item];
4013
- };
4014
- var placeLine = (stack, indent, body2) => {
4015
- while (stack.length > 1 && (stack[stack.length - 1]?.indent ?? 0) >= indent && !body2.startsWith("- ")) stack.pop();
4016
- const top = stack[stack.length - 1];
4017
- if (top === void 0) return;
4018
- if (body2.startsWith("- ")) {
4019
- addItem(top, unquote(body2.slice(2)));
4020
- return;
4021
- }
4022
- const pair = keyValue(body2);
4023
- if (pair === void 0) return;
4024
- const parentTree = top.key === void 0 ? top.tree : ensureTree(top.tree, top.key);
4025
- if (pair.value === "") {
4026
- stack.push({ indent, tree: parentTree, key: pair.key });
4027
- return;
4028
- }
4029
- parentTree[pair.key] = flowList(pair.value) ?? unquote(pair.value);
4030
- };
4031
- var ensureTree = (tree, key) => {
4032
- const existing = tree[key];
4033
- if (existing !== void 0 && typeof existing !== "string" && !Array.isArray(existing)) return existing;
4034
- const fresh3 = {};
4035
- tree[key] = fresh3;
4036
- return fresh3;
4037
- };
4038
- var parseConfigYaml = (text6) => {
4039
- const root = {};
4040
- const stack = [{ indent: -1, tree: root, key: void 0 }];
4041
- for (const raw of text6.split(/\r?\n/)) {
4042
- const line2 = stripComment(raw).trimEnd();
4043
- if (line2.trim() === "" || line2.trim() === "---") continue;
4044
- const indent = line2.length - line2.trimStart().length;
4045
- placeLine(stack, indent, line2.trim());
4046
- }
4047
- return root;
4048
- };
4049
- var isTree = (node) => typeof node === "object" && !Array.isArray(node);
4050
- var configAt = (tree, ...path) => path.reduce((node, key) => isTree(node) ? node[key] : void 0, tree);
4051
- var configList = (tree, ...path) => {
4052
- const node = configAt(tree, ...path);
4053
- if (typeof node === "string") return node === "" ? [] : [node];
4054
- return Array.isArray(node) ? node.filter((item) => typeof item === "string") : [];
4055
- };
4056
- var configMap = (tree, ...path) => {
4057
- const node = configAt(tree, ...path);
4058
- if (!isTree(node)) return {};
4059
- return Object.fromEntries(
4060
- Object.entries(node).filter((entry2) => typeof entry2[1] === "string")
4061
- );
4062
- };
4063
- var loadProjectConfig = (runtime, root) => {
4064
- const text6 = runtime.readText(toAbsolutePath(join12(root, CONFIG_FILE3)));
4065
- return text6.ok ? parseConfigYaml(text6.value) : {};
4066
- };
4137
+ import { join as join15 } from "node:path";
4067
4138
 
4068
4139
  // src/core/yaml/scalar.ts
4069
4140
  var ESCAPES = {
@@ -4343,8 +4414,8 @@ var TASK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
4343
4414
  var TASK_SCHEMA = "finitemonkey/task@1";
4344
4415
 
4345
4416
  // src/task/validate.ts
4346
- var str = (record2, key, at, issues, required) => {
4347
- const value = record2[key];
4417
+ var str = (record3, key, at, issues, required) => {
4418
+ const value = record3[key];
4348
4419
  if (value === void 0 || value === null) {
4349
4420
  if (required) issues.push(`${at}${key}: required`);
4350
4421
  return void 0;
@@ -4355,8 +4426,8 @@ var str = (record2, key, at, issues, required) => {
4355
4426
  }
4356
4427
  return value;
4357
4428
  };
4358
- var positive = (record2, key, at, issues) => {
4359
- const value = record2[key];
4429
+ var positive = (record3, key, at, issues) => {
4430
+ const value = record3[key];
4360
4431
  if (value === void 0 || value === null) return void 0;
4361
4432
  if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
4362
4433
  issues.push(`${at}${key}: must be a positive integer`);
@@ -4375,12 +4446,12 @@ var gitRequire = (value, at, issues) => {
4375
4446
  if (known2.some((item) => item === void 0)) issues.push(`${at}: use ${GIT_REQUIREMENTS.join(", ")}`);
4376
4447
  return known2.filter((item) => item !== void 0);
4377
4448
  };
4378
- var checkFields = (record2, kind, at, issues) => {
4379
- const run4 = str(record2, "run", at, issues, kind === "command");
4380
- const timeoutS = positive(record2, "timeout_s", at, issues);
4381
- const description = str(record2, "description", at, issues, kind === "manual");
4382
- const require2 = kind === "git" ? gitRequire(record2["require"] ?? ["committed"], `${at}require`, issues) : void 0;
4383
- 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"];
4384
4455
  if (open !== void 0 && typeof open !== "boolean") issues.push(`${at}require_open: must be true or false`);
4385
4456
  return {
4386
4457
  ...run4 === void 0 ? {} : { run: run4 },
@@ -4451,6 +4522,7 @@ var validateContract = (value, implicit = false) => {
4451
4522
  const contractScope = scope(value["scope"], issues);
4452
4523
  const checks = acceptance(value["acceptance"], issues);
4453
4524
  const decisionsNeeded = strings2(value["decisions_needed"], "decisions_needed", issues);
4525
+ const callersIgnore = strings2(value["callers_ignore"], "callers_ignore", issues);
4454
4526
  if (issues.length > 0 || id === void 0 || goal === void 0 || size === void 0) return reject(issues);
4455
4527
  return accept({
4456
4528
  id,
@@ -4460,6 +4532,7 @@ var validateContract = (value, implicit = false) => {
4460
4532
  scope: contractScope,
4461
4533
  acceptance: checks,
4462
4534
  decisionsNeeded,
4535
+ ...callersIgnore.length === 0 ? {} : { callersIgnore },
4463
4536
  implicit
4464
4537
  });
4465
4538
  };
@@ -4476,7 +4549,7 @@ var parseContract = (id, text6) => {
4476
4549
  if (valid.value.id !== id) return reject({ kind: "invalid", id, issues: [`id: must match the file name (${id})`] });
4477
4550
  return accept(valid.value);
4478
4551
  };
4479
- var pick = (lock, sources, id) => {
4552
+ var pick2 = (lock, sources, id) => {
4480
4553
  const approved = lock.approved[id];
4481
4554
  if (approved !== void 0) return { text: approved, source: "lock" };
4482
4555
  const base = sources.base(taskRelative(id));
@@ -4487,7 +4560,7 @@ var pick = (lock, sources, id) => {
4487
4560
  var loadTask = (files, paths, sources, id) => {
4488
4561
  if (!TASK_ID.test(id)) return reject({ kind: "not_found", id });
4489
4562
  const lock = readTrust(files, paths);
4490
- const chosen = pick(lock, sources, id);
4563
+ const chosen = pick2(lock, sources, id);
4491
4564
  if (chosen === void 0) return reject({ kind: "not_found", id });
4492
4565
  const parsed = parseContract(id, chosen.text);
4493
4566
  if (!parsed.ok) return parsed;
@@ -4542,7 +4615,7 @@ var readBaseline = (runtime, paths) => {
4542
4615
  var writeBaseline = (runtime, paths, treeHash2, checks) => writeJsonFile(runtime, paths.cache(BASELINE_FILE), { treeHash: treeHash2, checks: { ...checks } }).ok;
4543
4616
 
4544
4617
  // src/gate/diff-shape.ts
4545
- import { join as join13 } from "node:path";
4618
+ import { join as join14 } from "node:path";
4546
4619
 
4547
4620
  // src/gate/git.ts
4548
4621
  var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
@@ -4649,7 +4722,7 @@ var aheadOfUpstream = (git) => {
4649
4722
 
4650
4723
  // src/gate/diff-shape.ts
4651
4724
  var untrackedLines = (runtime, root, changes) => changes.filter((change) => change.status === "A").reduce((sum, change) => {
4652
- const text6 = runtime.readText(toAbsolutePath(join13(root, change.path)));
4725
+ const text6 = runtime.readText(toAbsolutePath(join14(root, change.path)));
4653
4726
  return sum + (text6.ok ? text6.value.split("\n").length : 0);
4654
4727
  }, 0);
4655
4728
  var diffShape = (runtime, git, base, changes) => ({
@@ -4676,18 +4749,18 @@ var ciChecks = (value) => Array.isArray(value) ? value.flatMap((item) => {
4676
4749
  var parsePrView = (text6) => {
4677
4750
  const parsed = parseJsonObject(text6);
4678
4751
  if (!parsed.ok) return void 0;
4679
- const record2 = parsed.value;
4680
- const number = record2["number"];
4752
+ const record3 = parsed.value;
4753
+ const number = record3["number"];
4681
4754
  if (typeof number !== "number") return void 0;
4682
4755
  return {
4683
4756
  number,
4684
- url: optionalString(record2, "url") ?? "",
4685
- state: optionalString(record2, "state") ?? "",
4686
- mergeable: optionalString(record2, "mergeable") ?? "UNKNOWN",
4687
- mergeStateStatus: optionalString(record2, "mergeStateStatus") ?? "UNKNOWN",
4688
- baseRefName: optionalString(record2, "baseRefName") ?? "",
4689
- headRefOid: optionalString(record2, "headRefOid") ?? "",
4690
- 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"])
4691
4764
  };
4692
4765
  };
4693
4766
  var isConflicting = (pr) => pr.mergeable === "CONFLICTING" || pr.mergeStateStatus === "DIRTY";
@@ -4855,10 +4928,10 @@ var applySize = (contract, verdict) => {
4855
4928
  // src/gate/prepare.ts
4856
4929
  var diskFiles = (runtime, root) => ({
4857
4930
  read: (relative9) => {
4858
- const text6 = runtime.readText(toAbsolutePath(join14(root, relative9)));
4931
+ const text6 = runtime.readText(toAbsolutePath(join15(root, relative9)));
4859
4932
  return text6.ok ? text6.value : void 0;
4860
4933
  },
4861
- exists: (relative9) => runtime.stat(toAbsolutePath(join14(root, relative9))) !== void 0
4934
+ exists: (relative9) => runtime.stat(toAbsolutePath(join15(root, relative9))) !== void 0
4862
4935
  });
4863
4936
  var baseFiles = (git, base, disk) => ({
4864
4937
  read: (relative9) => base === EMPTY_TREE ? disk.read(relative9) : fileAt(git, base, relative9),
@@ -5120,19 +5193,19 @@ var params = (value) => isRecord(value) ? Object.fromEntries(
5120
5193
  var notes = (value) => Array.isArray(value) ? value.flatMap(
5121
5194
  (item) => isRecord(item) && typeof item["key"] === "string" ? [{ key: item["key"], params: params(item["params"]) }] : []
5122
5195
  ) : [];
5123
- var text3 = (record2, key) => {
5124
- const value = record2[key];
5196
+ var text3 = (record3, key) => {
5197
+ const value = record3[key];
5125
5198
  return typeof value === "string" ? value : void 0;
5126
5199
  };
5127
- var optional = (record2) => {
5128
- const exitCode = record2["exitCode"];
5129
- const durationMs = record2["durationMs"];
5130
- const command = text3(record2, "command");
5131
- const stdoutSha256 = text3(record2, "stdoutSha256");
5132
- const releasedBy = text3(record2, "releasedBy");
5133
- const reason = oneOf2(UNVERIFIED_REASONS, record2["reason"]);
5134
- const failures = record2["failures"];
5135
- 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"];
5136
5209
  return {
5137
5210
  ...command === void 0 ? {} : { command },
5138
5211
  ...typeof exitCode === "number" || exitCode === null ? { exitCode } : {},
@@ -5147,14 +5220,14 @@ var optional = (record2) => {
5147
5220
  var parseEvidence = (line2) => {
5148
5221
  const parsed = parseJsonObject(line2);
5149
5222
  if (!parsed.ok) return void 0;
5150
- const record2 = parsed.value;
5151
- const status = oneOf2(EVIDENCE_STATUSES, record2["status"]);
5152
- const task = text3(record2, "task");
5153
- const check2 = text3(record2, "check");
5154
- const kind = text3(record2, "kind");
5155
- const treeHash2 = text3(record2, "treeHash");
5156
- const at = text3(record2, "at");
5157
- 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;
5158
5231
  if (task === void 0 || check2 === void 0 || kind === void 0 || treeHash2 === void 0 || at === void 0) {
5159
5232
  return void 0;
5160
5233
  }
@@ -5166,8 +5239,8 @@ var parseEvidence = (line2) => {
5166
5239
  status,
5167
5240
  treeHash: treeHash2,
5168
5241
  at,
5169
- notes: notes(record2["notes"]),
5170
- ...optional(record2)
5242
+ notes: notes(record3["notes"]),
5243
+ ...optional(record3)
5171
5244
  };
5172
5245
  };
5173
5246
  var readLedger = (runtime, path) => {
@@ -5706,7 +5779,15 @@ var en_default = {
5706
5779
  "gate.secrets.warning": "warning: {rule} at {file}:{line}",
5707
5780
  "doctor.secrets.remote_credential": "git remote {remotes} has a credential in its URL, stored in plain text in .git/config",
5708
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",
5709
- "secrets.session.remote_credential": "git remote {remotes} has a credential in its URL (plain text in .git/config). Ask the person to run `git remote set-url {remote} <url without user:token@>` and rotate the token; do not print the URL."
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."
5710
5791
  };
5711
5792
 
5712
5793
  // src/i18n/es.json
@@ -6230,7 +6311,15 @@ var es_default = {
6230
6311
  "gate.secrets.warning": "aviso: {rule} en {file}:{line}",
6231
6312
  "doctor.secrets.remote_credential": "el remoto git {remotes} tiene una credencial en la URL, guardada en texto plano en .git/config",
6232
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",
6233
- "secrets.session.remote_credential": "el remoto git {remotes} tiene una credencial en la URL (texto plano en .git/config). Pide a la persona que ejecute `git remote set-url {remote} <url sin usuario:token@>` y rote el token; no imprimas la URL."
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."
6234
6323
  };
6235
6324
 
6236
6325
  // src/i18n/pt-BR.json
@@ -6754,7 +6843,15 @@ var pt_BR_default = {
6754
6843
  "gate.secrets.warning": "aviso: {rule} em {file}:{line}",
6755
6844
  "doctor.secrets.remote_credential": "o remoto git {remotes} tem uma credencial na URL, guardada em texto puro no .git/config",
6756
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",
6757
- "secrets.session.remote_credential": "o remoto git {remotes} tem uma credencial na URL (texto puro no .git/config). Peça para a pessoa rodar `git remote set-url {remote} <url sem usuario:token@>` e rotacionar o token; não imprima a URL."
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."
6758
6855
  };
6759
6856
 
6760
6857
  // src/i18n/review.json
@@ -7279,7 +7376,15 @@ var review_default = {
7279
7376
  "gate.secrets.warning": "machine",
7280
7377
  "doctor.secrets.remote_credential": "machine",
7281
7378
  "doctor.secrets.fix_remote_credential": "machine",
7282
- "secrets.session.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"
7283
7388
  },
7284
7389
  es: {
7285
7390
  "command.init.summary": "machine",
@@ -7801,7 +7906,15 @@ var review_default = {
7801
7906
  "gate.secrets.warning": "machine",
7802
7907
  "doctor.secrets.remote_credential": "machine",
7803
7908
  "doctor.secrets.fix_remote_credential": "machine",
7804
- "secrets.session.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"
7805
7918
  }
7806
7919
  };
7807
7920
 
@@ -8080,6 +8193,256 @@ var recordReview = (runtime, paths, treeHash2, agent) => {
8080
8193
  return writeJsonFile(runtime, paths.cache(REVIEWS_FILE), { reviews: reviews.map((review2) => ({ ...review2 })) }).ok;
8081
8194
  };
8082
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
+
8083
8446
  // src/gate/test-summary.ts
8084
8447
  var ANSI2 = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
8085
8448
  var COUNTERS = [
@@ -8276,13 +8639,13 @@ var manualCheck = (check2, ctx) => {
8276
8639
  };
8277
8640
 
8278
8641
  // src/gate/checks/placeholders.ts
8279
- import { join as join15 } from "node:path";
8642
+ import { join as join17 } from "node:path";
8280
8643
  var MAX_LISTED2 = 10;
8281
8644
  var SKIP_FILES = /\.(md|mdx|txt|rst|lock|snap|svg|png|jpg|gif|json)$|(^|\/)(CHANGELOG|LICENSE)/i;
8282
8645
  var PLACEHOLDER = /\b(TODO|FIXME|XXX)\b|not implemented|NotImplementedError|unimplemented!|todo!\(|lorem ipsum/i;
8283
8646
  var findPlaceholders = (lines3) => lines3.filter((line2) => !isTestFile(line2.path) && !SKIP_FILES.test(line2.path) && PLACEHOLDER.test(line2.text));
8284
8647
  var untrackedAdded = (ctx) => ctx.changes.filter((change) => change.status === "A" && !SKIP_FILES.test(change.path)).flatMap((change) => {
8285
- const text6 = ctx.runtime.readText(toAbsolutePath(join15(ctx.paths.root, change.path)));
8648
+ const text6 = ctx.runtime.readText(toAbsolutePath(join17(ctx.paths.root, change.path)));
8286
8649
  return text6.ok ? text6.value.split("\n").map((line2, index) => ({ path: change.path, line: index + 1, text: line2 })) : [];
8287
8650
  });
8288
8651
  var placeholdersCheck = (_check, ctx) => {
@@ -9014,15 +9377,15 @@ var reviewCheck = (check2, ctx) => {
9014
9377
  };
9015
9378
 
9016
9379
  // src/secrets/baseline.ts
9017
- import { join as join16 } from "node:path";
9380
+ import { join as join18 } from "node:path";
9018
9381
  var BASELINE_FILE2 = ".finitemonkey/findings/baseline.json";
9019
9382
  var BASELINE_SCHEMA = "finitemonkey/secrets-baseline@1";
9020
9383
  var BASELINE_STATES = ["aceito", "falso-positivo"];
9021
9384
  var EXCEPTION_MAX_DAYS = 90;
9022
9385
  var EMPTY_BASELINE = { schema: BASELINE_SCHEMA, entries: [] };
9023
- var baselinePath = (root) => toAbsolutePath(join16(root, BASELINE_FILE2));
9024
- var text5 = (record2, key) => {
9025
- const value = record2[key];
9386
+ var baselinePath = (root) => toAbsolutePath(join18(root, BASELINE_FILE2));
9387
+ var text5 = (record3, key) => {
9388
+ const value = record3[key];
9026
9389
  return typeof value === "string" && value !== "" ? value : void 0;
9027
9390
  };
9028
9391
  var stateOf = (value) => BASELINE_STATES.find((state) => state === value);
@@ -9080,22 +9443,22 @@ var withoutFixed = (baseline, fixed) => {
9080
9443
  var acceptedFingerprints = (baseline, now) => new Set(baseline.entries.filter((entry2) => entry2.expiresAt >= now).map((entry2) => entry2.fingerprint));
9081
9444
 
9082
9445
  // src/secrets/incidents.ts
9083
- import { join as join17 } from "node:path";
9446
+ import { join as join19 } from "node:path";
9084
9447
  var INCIDENTS_DIR = ".finitemonkey/incidents";
9085
9448
  var INCIDENT_STATES = ["aberto", "exposicao-local", "rotacionado", "fechado"];
9086
9449
  var INCIDENT_SOURCES = ["tool-output", "prompt", "history", "scan"];
9087
9450
  var ID = /^inc-[0-9a-f]{12}$/;
9088
9451
  var incidentId = (rule, fingerprint) => `inc-${sha256Hex(`${rule}\0${fingerprint}`).slice(0, 12)}`;
9089
- var incidentsDir = (root) => toAbsolutePath(join17(root, INCIDENTS_DIR));
9090
- var incidentFile = (root, id) => toAbsolutePath(join17(incidentsDir(root), `${id}.json`));
9091
- var pick2 = (options, value) => options.find((option) => option === value);
9092
- var str2 = (record2, key) => {
9093
- 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];
9094
9457
  return typeof value === "string" ? value : void 0;
9095
9458
  };
9096
9459
  var parseEvent = (value) => {
9097
9460
  if (!isRecord(value)) return void 0;
9098
- const state = pick2(INCIDENT_STATES, value["state"]);
9461
+ const state = pick3(INCIDENT_STATES, value["state"]);
9099
9462
  const at = str2(value, "at");
9100
9463
  const by = str2(value, "by");
9101
9464
  return state === void 0 || at === void 0 || by === void 0 ? void 0 : { state, at, by };
@@ -9108,12 +9471,12 @@ var parseHistory = (value) => {
9108
9471
  var parseIncident = (raw) => {
9109
9472
  const parsed = parseJsonObject(raw);
9110
9473
  if (!parsed.ok) return parsed;
9111
- const record2 = parsed.value;
9112
- const [id, rule, where, fingerprint, createdAt, updatedAt] = ["id", "rule", "where", "fingerprint", "createdAt", "updatedAt"].map((key) => str2(record2, key));
9113
- const state = pick2(INCIDENT_STATES, record2["state"]);
9114
- const source = pick2(INCIDENT_SOURCES, record2["source"]);
9115
- const history = parseHistory(record2["history"]);
9116
- 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");
9117
9480
  if (id === void 0 || !ID.test(id) || rule === void 0 || where === void 0) return reject("incomplete");
9118
9481
  if (fingerprint === void 0 || createdAt === void 0 || updatedAt === void 0) return reject("incomplete");
9119
9482
  if (state === void 0 || source === void 0 || history === void 0) return reject("incomplete");
@@ -9142,7 +9505,7 @@ var listIncidents = (runtime, root) => {
9142
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));
9143
9506
  };
9144
9507
  var saveIncident = (runtime, root, incident) => {
9145
- const ignore = runtime.writeText(toAbsolutePath(join17(incidentsDir(root), ".gitignore")), "*\n");
9508
+ const ignore = runtime.writeText(toAbsolutePath(join19(incidentsDir(root), ".gitignore")), "*\n");
9146
9509
  if (!ignore.ok) return ignore;
9147
9510
  return runtime.writeText(incidentFile(root, incident.id), `${JSON.stringify(incident, null, 2)}
9148
9511
  `);
@@ -9189,7 +9552,7 @@ var moveIncident = (incident, to, now, by) => ({
9189
9552
  var isRotated = (incident) => incident.state === "rotacionado" || incident.state === "fechado";
9190
9553
 
9191
9554
  // src/secrets/scan.ts
9192
- import { join as join18, relative as relative4 } from "node:path";
9555
+ import { join as join20, relative as relative4 } from "node:path";
9193
9556
 
9194
9557
  // src/secrets/located.ts
9195
9558
  var findingId = (ruleId, file, fingerprint) => `fm-${sha256Hex(`${ruleId}\0${file}\0${fingerprint}`).slice(0, 16)}`;
@@ -9289,7 +9652,7 @@ var listed2 = (runtime, root, target) => {
9289
9652
  const base = toAbsolutePath(target, root);
9290
9653
  const walked = runtime.walkFiles(base);
9291
9654
  if (!walked.ok) return walked;
9292
- return accept(walked.value.map((name) => toPosixPath(relative4(root, join18(base, toPosixPath(name))))));
9655
+ return accept(walked.value.map((name) => toPosixPath(relative4(root, join20(base, toPosixPath(name))))));
9293
9656
  };
9294
9657
  var scanFile = (runtime, root, file) => {
9295
9658
  const text6 = runtime.readText(toAbsolutePath(file, root));
@@ -9359,7 +9722,7 @@ var secretsCheck = (_check, ctx) => {
9359
9722
  };
9360
9723
 
9361
9724
  // src/gate/checks/tests.ts
9362
- import { join as join19 } from "node:path";
9725
+ import { join as join21 } from "node:path";
9363
9726
 
9364
9727
  // src/guards/tests/tree.ts
9365
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 }] : [];
@@ -9391,7 +9754,7 @@ var treeViolations = (ctx) => {
9391
9754
  changes: relevant,
9392
9755
  before: (path) => fileAt(ctx.git, ctx.base, path),
9393
9756
  after: (path) => {
9394
- const text6 = ctx.runtime.readText(toAbsolutePath(join19(ctx.paths.root, path)));
9757
+ const text6 = ctx.runtime.readText(toAbsolutePath(join21(ctx.paths.root, path)));
9395
9758
  return text6.ok ? text6.value : void 0;
9396
9759
  },
9397
9760
  acceptedConfigs: ctx.trust.configs,
@@ -9412,7 +9775,7 @@ var testsCheck = (check2, ctx) => {
9412
9775
  };
9413
9776
 
9414
9777
  // src/git/tree.ts
9415
- import { join as join20 } from "node:path";
9778
+ import { join as join22 } from "node:path";
9416
9779
 
9417
9780
  // src/secrets/guards/paths.ts
9418
9781
  var SAFE_ENV = /^\.env\.(?:example|sample|template|dist|defaults|schema)$/i;
@@ -9451,7 +9814,7 @@ var dropSensitive = (runtime, repo, env, options) => {
9451
9814
  return removed.ok ? accept(void 0) : removed;
9452
9815
  };
9453
9816
  var withTempIndex = (runtime, repo, options, work) => {
9454
- const path = toAbsolutePath(join20(repo.gitDir, `finitemonkey-${options.stamp}.index`));
9817
+ const path = toAbsolutePath(join22(repo.gitDir, `finitemonkey-${options.stamp}.index`));
9455
9818
  const env = { GIT_INDEX_FILE: path, GIT_OPTIONAL_LOCKS: "0" };
9456
9819
  try {
9457
9820
  return work(env);
@@ -9684,26 +10047,26 @@ var entryOf2 = (kind, xy, path) => ({
9684
10047
  staged: kind !== "untracked" && !xy.startsWith("."),
9685
10048
  unstaged: kind === "untracked" || xy.charAt(1) !== "."
9686
10049
  });
9687
- var afterFields = (record2, count4) => record2.split(" ").slice(count4).join(" ");
10050
+ var afterFields = (record3, count4) => record3.split(" ").slice(count4).join(" ");
9688
10051
  var parsePorcelainV2 = (stdout) => {
9689
10052
  const records = stdout.split("\0");
9690
10053
  const out = [];
9691
10054
  for (let index = 0; index < records.length; index += 1) {
9692
- const record2 = records[index] ?? "";
9693
- const xy = record2.split(" ")[1] ?? "..";
9694
- if (record2.startsWith("1 ")) out.push(entryOf2("changed", xy, afterFields(record2, PORCELAIN_FIELDS.ordinary)));
9695
- else if (record2.startsWith("2 ")) {
9696
- 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)));
9697
10060
  index += 1;
9698
- } else if (record2.startsWith("u ")) {
9699
- out.push(entryOf2("unmerged", xy, afterFields(record2, PORCELAIN_FIELDS.unmerged)));
9700
- } 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)));
9701
10064
  }
9702
10065
  return out.filter((entry2) => entry2.path !== "" && !entry2.path.startsWith(STATE_PREFIX));
9703
10066
  };
9704
10067
 
9705
10068
  // src/git/merge/dropped.ts
9706
- import { join as join21 } from "node:path";
10069
+ import { join as join23 } from "node:path";
9707
10070
 
9708
10071
  // src/git/merge/lines.ts
9709
10072
  var normalizeLine = (line2) => line2.replace(/\r$/, "").trim().replace(/\s*,$/, "");
@@ -9783,7 +10146,7 @@ var parseDropped = (text6) => {
9783
10146
  return accept({ items, invalid });
9784
10147
  };
9785
10148
  var loadDropped = (runtime, root, file) => {
9786
- const path = toAbsolutePath(file ?? join21(root, DROPPED_FILE), root);
10149
+ const path = toAbsolutePath(file ?? join23(root, DROPPED_FILE), root);
9787
10150
  const text6 = runtime.readText(path);
9788
10151
  if (!text6.ok) return file === void 0 ? accept({ items: [], invalid: [] }) : reject(`${file}: ${text6.error}`);
9789
10152
  return parseDropped(text6.value);
@@ -9793,7 +10156,7 @@ var isDropped = (finding3, items) => items.some(
9793
10156
  );
9794
10157
 
9795
10158
  // src/git/merge/generated.ts
9796
- import { join as join22 } from "node:path";
10159
+ import { join as join24 } from "node:path";
9797
10160
  var LOCKFILES3 = /* @__PURE__ */ new Set([
9798
10161
  "package-lock.json",
9799
10162
  "npm-shrinkwrap.json",
@@ -9856,7 +10219,7 @@ var verifyGenerated = (runtime, repo, inputs, paths, generators) => {
9856
10219
  if (declared.length === 0) return accept([]);
9857
10220
  const commit2 = resultCommit(runtime, repo, inputs);
9858
10221
  if (!commit2.ok) return commit2;
9859
- const dir = join22(repo.gitDir, `finitemonkey-generate-${runtime.now().replace(/[:.]/g, "-")}`);
10222
+ const dir = join24(repo.gitDir, `finitemonkey-generate-${runtime.now().replace(/[:.]/g, "-")}`);
9860
10223
  if (!runtime.git(["worktree", "add", "--detach", dir, commit2.value], repo.root).ok)
9861
10224
  return reject("could not check out the merge result");
9862
10225
  try {
@@ -9866,7 +10229,7 @@ var verifyGenerated = (runtime, repo, inputs, paths, generators) => {
9866
10229
  }
9867
10230
  return accept(
9868
10231
  declared.flatMap(({ path }) => {
9869
- const fresh3 = runtime.readText(toAbsolutePath(join22(dir, path)));
10232
+ const fresh3 = runtime.readText(toAbsolutePath(join24(dir, path)));
9870
10233
  const merged = blobText(runtime, repo, commit2.value, path);
9871
10234
  if (fresh3.ok && fresh3.value === merged) return [];
9872
10235
  return [{ kind: "generated-differs", file: path, side: void 0, line: void 0, text: path, weak: false }];
@@ -10080,16 +10443,16 @@ var failed2 = (error, inputs) => ({
10080
10443
  generated: [],
10081
10444
  error
10082
10445
  });
10083
- 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) ?? "";
10084
10447
  var pairsOf = (ours, theirs) => {
10085
- 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();
10086
10449
  return keys.map((key) => ({
10087
10450
  key,
10088
- ours: ours.find((diff) => keyOf(diff) === key),
10089
- theirs: theirs.find((diff) => keyOf(diff) === key)
10451
+ ours: ours.find((diff) => keyOf2(diff) === key),
10452
+ theirs: theirs.find((diff) => keyOf2(diff) === key)
10090
10453
  }));
10091
10454
  };
10092
- var contextOf2 = (runtime, repo, inputs, pairs) => {
10455
+ var contextOf2 = (runtime, repo, inputs, pairs2) => {
10093
10456
  const resultText = memoBy((path) => blobText(runtime, repo, inputs.result, path));
10094
10457
  const context = {
10095
10458
  runtime,
@@ -10097,7 +10460,7 @@ var contextOf2 = (runtime, repo, inputs, pairs) => {
10097
10460
  inputs,
10098
10461
  resultText,
10099
10462
  resultFiles: memo2(
10100
- () => pairs.flatMap((pair) => {
10463
+ () => pairs2.flatMap((pair) => {
10101
10464
  const path = resultPathOf(context, pair);
10102
10465
  return path === void 0 ? [] : [path];
10103
10466
  })
@@ -10114,24 +10477,24 @@ var verifyMerge = (runtime, repo, options = {}) => {
10114
10477
  const theirs = sideDiff(runtime, repo, inputs.value.base, inputs.value.theirs);
10115
10478
  if (!ours.ok) return failed2(ours.error, inputs.value);
10116
10479
  if (!theirs.ok) return failed2(theirs.error, inputs.value);
10117
- const pairs = pairsOf(ours.value, theirs.value);
10480
+ const pairs2 = pairsOf(ours.value, theirs.value);
10118
10481
  const generators = configMap(loadProjectConfig(runtime, repo.root), "merge", "generators");
10119
10482
  const generated = generatedPaths(
10120
10483
  runtime,
10121
10484
  repo,
10122
- pairs.map((pair) => pair.key),
10485
+ pairs2.map((pair) => pair.key),
10123
10486
  generators
10124
10487
  );
10125
10488
  const context = contextOf2(
10126
10489
  runtime,
10127
10490
  repo,
10128
10491
  inputs.value,
10129
- pairs.filter((pair) => !generated.has(pair.key))
10492
+ pairs2.filter((pair) => !generated.has(pair.key))
10130
10493
  );
10131
10494
  const regenerated = verifyGenerated(runtime, repo, inputs.value, [...generated], generators);
10132
10495
  if (!regenerated.ok) return failed2(regenerated.error, inputs.value);
10133
10496
  const all = [
10134
- ...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)),
10135
10498
  ...regenerated.value
10136
10499
  ];
10137
10500
  const open = [...dropped.value.invalid, ...all.filter((item) => !isDropped(item, dropped.value.items))];
@@ -10143,7 +10506,7 @@ var verifyMerge = (runtime, repo, options = {}) => {
10143
10506
  theirs: inputs.value.theirs,
10144
10507
  result: inputs.value.result,
10145
10508
  bases: inputs.value.bases,
10146
- files: pairs.length,
10509
+ files: pairs2.length,
10147
10510
  findings: open,
10148
10511
  justified: all.length - (open.length - dropped.value.invalid.length),
10149
10512
  generated: [...generated].sort(),
@@ -10152,19 +10515,19 @@ var verifyMerge = (runtime, repo, options = {}) => {
10152
10515
  };
10153
10516
 
10154
10517
  // src/git/ownership.ts
10155
- import { join as join23 } from "node:path";
10518
+ import { join as join25 } from "node:path";
10156
10519
  var OWNERSHIP_DIR = ".finitemonkey/git";
10157
10520
  var ownerKey = (raw) => {
10158
10521
  const cleaned = (raw ?? "").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80);
10159
10522
  return cleaned === "" ? "unknown" : cleaned;
10160
10523
  };
10161
- 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`));
10162
10525
  var readDirty = (runtime, root) => {
10163
10526
  const status = runtime.git(["status", "--porcelain=v2", "-z", "--untracked-files=all"], root);
10164
10527
  return status.ok ? parsePorcelainV2(status.stdout) : void 0;
10165
10528
  };
10166
10529
  var hashDirty = (runtime, repo, entries) => {
10167
- 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");
10168
10531
  const hashed = present2.length === 0 ? void 0 : runtime.gitWith(["hash-object", "--stdin-paths"], repo.root, {
10169
10532
  input: `${present2.map((entry2) => entry2.path).join("\n")}
10170
10533
  `
@@ -10417,7 +10780,8 @@ var DEFAULT_CHECKS = {
10417
10780
  prose: proseCheck,
10418
10781
  worktree: worktreeCheck,
10419
10782
  merge: mergeCheck,
10420
- review: reviewCheck
10783
+ review: reviewCheck,
10784
+ callers: callersCheck
10421
10785
  };
10422
10786
 
10423
10787
  // src/gate/run.ts
@@ -10611,7 +10975,7 @@ var subagentStopGate = (input) => {
10611
10975
  };
10612
10976
 
10613
10977
  // src/gate/hooks/subagent.ts
10614
- import { join as join24 } from "node:path";
10978
+ import { join as join26 } from "node:path";
10615
10979
  var NO_CONTRACT_AGENTS = /(^|:)(reviewer|verifier|explore|plan|claude-code-guide|statusline-setup)$/i;
10616
10980
  var KNOWN_KINDS = /* @__PURE__ */ new Set([
10617
10981
  "command",
@@ -10625,7 +10989,8 @@ var KNOWN_KINDS = /* @__PURE__ */ new Set([
10625
10989
  "prose",
10626
10990
  "worktree",
10627
10991
  "merge",
10628
- "review"
10992
+ "review",
10993
+ "callers"
10629
10994
  ]);
10630
10995
  var requirementText = (t, check2) => {
10631
10996
  const key = `gate.require.${check2.kind}`;
@@ -10646,12 +11011,12 @@ var subagentStart = (input) => {
10646
11011
  const active = readState(runtime, paths).active;
10647
11012
  if (active === void 0) return { kind: "allow" };
10648
11013
  const disk = (relative9) => {
10649
- const text6 = runtime.readText(toAbsolutePath(join24(paths.root, relative9)));
11014
+ const text6 = runtime.readText(toAbsolutePath(join26(paths.root, relative9)));
10650
11015
  return text6.ok ? text6.value : void 0;
10651
11016
  };
10652
11017
  const loaded = loadTask(runtime, paths, { disk, base: () => void 0 }, active);
10653
11018
  if (!loaded.ok) return { kind: "allow" };
10654
- 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;
10655
11020
  return { kind: "allow", output: contextOutput("SubagentStart", contractFacts(t, loaded.value.contract, briefing)) };
10656
11021
  };
10657
11022
 
@@ -10668,7 +11033,7 @@ var PORTAO_HANDLERS = {
10668
11033
  };
10669
11034
 
10670
11035
  // src/prod/guard.ts
10671
- import { join as join26 } from "node:path";
11036
+ import { join as join28 } from "node:path";
10672
11037
 
10673
11038
  // src/git/command-text.ts
10674
11039
  var MAX_SHOWN = 160;
@@ -11010,7 +11375,7 @@ var remoteHostsOf = (command) => {
11010
11375
  };
11011
11376
 
11012
11377
  // src/prod/targets.ts
11013
- import { join as join25 } from "node:path";
11378
+ import { join as join27 } from "node:path";
11014
11379
  var DEPLOY_FILES = [
11015
11380
  "deploy.sh",
11016
11381
  "scripts/deploy.sh",
@@ -11034,12 +11399,12 @@ var hostsInText = (text6) => {
11034
11399
  return [...urls, ...keyed, ...commands].filter((host) => host !== "" && !host.includes("$"));
11035
11400
  };
11036
11401
  var workflowFiles = (runtime, root) => {
11037
- const listed4 = runtime.listDir(toAbsolutePath(join25(root, ".github", "workflows")));
11402
+ const listed4 = runtime.listDir(toAbsolutePath(join27(root, ".github", "workflows")));
11038
11403
  if (!listed4.ok) return [];
11039
11404
  return listed4.value.filter((name) => /deploy/i.test(name) && /\.ya?ml$/i.test(name)).map((name) => `.github/workflows/${name}`);
11040
11405
  };
11041
11406
  var detectedHosts = (runtime, root, files) => files.flatMap((file) => {
11042
- const text6 = runtime.readText(toAbsolutePath(join25(root, file)));
11407
+ const text6 = runtime.readText(toAbsolutePath(join27(root, file)));
11043
11408
  return text6.ok ? hostsInText(text6.value) : [];
11044
11409
  });
11045
11410
  var loadProdTargets = (runtime, root, config) => {
@@ -11051,7 +11416,7 @@ var loadProdTargets = (runtime, root, config) => {
11051
11416
  ...detectedHosts(runtime, root, deployFiles)
11052
11417
  ];
11053
11418
  const declaredDeploy = configAt(config, "prod", "deploy");
11054
- 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);
11055
11420
  return {
11056
11421
  hosts: new Set(hosts.map(hostKey).filter((host) => host !== "")),
11057
11422
  kubeContexts: configList(config, "prod", "kube_contexts"),
@@ -11228,8 +11593,8 @@ var recordEvidence = (runtime, root, owner, reads) => {
11228
11593
  ] : []
11229
11594
  );
11230
11595
  if (lines3.length === 0) return;
11231
- const path = toAbsolutePath(join26(root, EVIDENCE_FILE));
11232
- runtime.mkdir(toAbsolutePath(join26(root, ".finitemonkey")));
11596
+ const path = toAbsolutePath(join28(root, EVIDENCE_FILE));
11597
+ runtime.mkdir(toAbsolutePath(join28(root, ".finitemonkey")));
11233
11598
  const before = runtime.readText(path);
11234
11599
  const text6 = before.ok ? before.value : "";
11235
11600
  runtime.writeAtomic(path, `${text6}${text6 === "" || text6.endsWith("\n") ? "" : "\n"}${lines3.join("\n")}
@@ -11331,7 +11696,7 @@ var withAlias = (invocation, expansion) => ({
11331
11696
  });
11332
11697
 
11333
11698
  // src/git/path-key.ts
11334
- import { posix as posix2 } from "node:path";
11699
+ import { posix as posix3 } from "node:path";
11335
11700
  var WSL_MOUNT3 = /^\/mnt\/([a-zA-Z])(\/|$)/;
11336
11701
  var DRIVE = /^([a-zA-Z]):(\/|$)/;
11337
11702
  var pathKey = (path) => {
@@ -11340,7 +11705,7 @@ var pathKey = (path) => {
11340
11705
  (_match, letter, slash) => `${letter}:${slash || "/"}`
11341
11706
  );
11342
11707
  const drive = DRIVE.exec(posixPath);
11343
- const normalized = posix2.normalize(drive === null ? posixPath : posixPath.slice(2) || "/");
11708
+ const normalized = posix3.normalize(drive === null ? posixPath : posixPath.slice(2) || "/");
11344
11709
  const trimmed = normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
11345
11710
  return drive === null ? trimmed : `${(drive[1] ?? "").toLowerCase()}:${trimmed}`.toLowerCase();
11346
11711
  };
@@ -11356,15 +11721,15 @@ var isRootLike = (path, home) => {
11356
11721
  if (key === "/" || /^[a-z]:\/?$/.test(key)) return true;
11357
11722
  return home !== void 0 && home !== "" && key === pathKey(home);
11358
11723
  };
11359
- var resolveFrom = (base, ...parts) => parts.reduce((current, part2) => {
11360
- if (part2 === void 0 || part2 === "") return current;
11724
+ var resolveFrom = (base, ...parts) => parts.reduce((current2, part2) => {
11725
+ if (part2 === void 0 || part2 === "") return current2;
11361
11726
  const next2 = toPosixPath(part2);
11362
11727
  if (next2.startsWith("/") || DRIVE.test(next2)) return next2;
11363
- return posix2.join(toPosixPath(current), next2);
11728
+ return posix3.join(toPosixPath(current2), next2);
11364
11729
  }, toPosixPath(base));
11365
11730
 
11366
11731
  // src/git/pathspec.ts
11367
- import { posix as posix3 } from "node:path";
11732
+ import { posix as posix4 } from "node:path";
11368
11733
  var MAGIC = /^:(?:\(([^)]*)\)|([!^/]*))(.*)$/s;
11369
11734
  var GLOB = /[*?[]/;
11370
11735
  var specOf = (raw, prefix) => {
@@ -11374,7 +11739,7 @@ var specOf = (raw, prefix) => {
11374
11739
  const rest = magic === null ? posixRaw : magic[3] ?? "";
11375
11740
  const exclude = /(?:^|,)exclude(?:,|$)|[!^]/.test(words);
11376
11741
  const top = /(?:^|,)top(?:,|$)|\//.test(words);
11377
- const joined = posix3.normalize(top ? rest : posix3.join(prefix === "" ? "." : prefix, rest));
11742
+ const joined = posix4.normalize(top ? rest : posix4.join(prefix === "" ? "." : prefix, rest));
11378
11743
  return { pattern: joined === "./" ? "." : joined.replace(/\/$/, ""), exclude };
11379
11744
  };
11380
11745
  var globRegex3 = (pattern) => {
@@ -11407,7 +11772,7 @@ var discard = (op, scope2, pathspecs = []) => ({
11407
11772
  discard: { scope: scope2, pathspecs }
11408
11773
  });
11409
11774
  var SEVERITY = { pass: 0, snapshot: 1, discard: 2, block: 3 };
11410
- 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);
11411
11776
 
11412
11777
  // src/git/options.ts
11413
11778
  var canonical = (name, spec2) => {
@@ -11768,24 +12133,24 @@ var branchName = (ref) => {
11768
12133
  if (ref.startsWith("refs/tags/")) return void 0;
11769
12134
  return ref.replace(/^refs\/heads\//, "");
11770
12135
  };
11771
- var refspecTarget = (refspec, current) => {
12136
+ var refspecTarget = (refspec, current2) => {
11772
12137
  const plain = refspec.replace(/^\+/, "");
11773
12138
  const colon = plain.indexOf(":");
11774
12139
  const destination = colon < 0 ? plain : plain.slice(colon + 1);
11775
- if (destination === "HEAD" || colon < 0 && plain === "HEAD") return current;
12140
+ if (destination === "HEAD" || colon < 0 && plain === "HEAD") return current2;
11776
12141
  if (destination === "" || destination.startsWith("refs/tags/")) return void 0;
11777
12142
  return branchName(destination);
11778
12143
  };
11779
12144
  var pushDestinations = (parsed, facts) => {
11780
12145
  if (hasAny(parsed, "--all", "--branches", "--mirror")) return "all";
11781
12146
  const refspecs = parsed.positionals.slice(1).filter((refspec) => refspec !== ":");
11782
- const current = facts.currentBranch();
12147
+ const current2 = facts.currentBranch();
11783
12148
  if (refspecs.length === 0) {
11784
- if (hasAny(parsed, "--tags") || current === void 0) return [];
11785
- return [facts.upstream(current)?.branch ?? current];
12149
+ if (hasAny(parsed, "--tags") || current2 === void 0) return [];
12150
+ return [facts.upstream(current2)?.branch ?? current2];
11786
12151
  }
11787
12152
  return refspecs.flatMap((refspec) => {
11788
- const target = refspecTarget(refspec, current);
12153
+ const target = refspecTarget(refspec, current2);
11789
12154
  return target === void 0 ? [] : [target];
11790
12155
  });
11791
12156
  };
@@ -11793,10 +12158,10 @@ var leaseVerdict = (parsed, facts) => {
11793
12158
  const leases = valuesOf(parsed, "--force-with-lease");
11794
12159
  const explicit = leases.length > 0 && leases.every((lease) => /^[^:]+:.+$/.test(lease));
11795
12160
  if (!explicit || !hasAny(parsed, "--force-if-includes")) return block2("force-push", "--force-with-lease");
11796
- const current = facts.currentBranch();
12161
+ const current2 = facts.currentBranch();
11797
12162
  const leased = leases.map((lease) => branchName(lease.slice(0, lease.indexOf(":"))));
11798
12163
  const destinations = pushDestinations(parsed, facts);
11799
- 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);
11800
12165
  return own ? PASS : block2("lease-not-own", leased.join(", "));
11801
12166
  };
11802
12167
  var pushVerdict = (invocation, facts) => {
@@ -11819,8 +12184,8 @@ var LEAVES = /* @__PURE__ */ new Set(["--abort", "--quit", "--skip", "--dry-run"
11819
12184
  var commitVerdict = (invocation, facts) => {
11820
12185
  if (invocation.args.some((arg) => LEAVES.has(arg))) return PASS;
11821
12186
  if (invocation.subcommand === "rebase") return PASS;
11822
- const current = facts.currentBranch();
11823
- 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;
11824
12189
  };
11825
12190
  var pushVerdict2 = (invocation, facts) => {
11826
12191
  const parsed = parseOptions(invocation.args, PUSH_OPTIONS);
@@ -12041,33 +12406,33 @@ var listWorktrees = (runtime, cwd) => {
12041
12406
  };
12042
12407
 
12043
12408
  // src/git/journal.ts
12044
- import { join as join27 } from "node:path";
12409
+ import { join as join29 } from "node:path";
12045
12410
  var JOURNAL_FILE = ".finitemonkey/git-journal.jsonl";
12046
12411
  var JOURNAL_SCHEMA = "finitemonkey/git-journal@1";
12047
- var journalPath = (root) => toAbsolutePath(join27(root, JOURNAL_FILE));
12412
+ var journalPath = (root) => toAbsolutePath(join29(root, JOURNAL_FILE));
12048
12413
  var headsOf = (value) => isRecord(value) ? Object.fromEntries(
12049
12414
  Object.entries(value).filter((entry2) => typeof entry2[1] === "string")
12050
12415
  ) : {};
12051
12416
  var parseJournalLine = (line2) => {
12052
12417
  const parsed = parseJsonObject(line2);
12053
12418
  if (!parsed.ok) return void 0;
12054
- const record2 = parsed.value;
12055
- const ref = optionalString(record2, "ref");
12056
- const commit2 = optionalString(record2, "commit");
12057
- 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");
12058
12423
  if (ref === void 0 || commit2 === void 0 || at === void 0) return void 0;
12059
12424
  return {
12060
12425
  schema: JOURNAL_SCHEMA,
12061
12426
  at,
12062
- op: optionalString(record2, "op") ?? "",
12427
+ op: optionalString(record3, "op") ?? "",
12063
12428
  ref,
12064
12429
  commit: commit2,
12065
- head: optionalString(record2, "head"),
12066
- branch: optionalString(record2, "branch"),
12067
- index: optionalString(record2, "index"),
12068
- owner: optionalString(record2, "owner"),
12069
- command: optionalString(record2, "command"),
12070
- 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"])
12071
12436
  };
12072
12437
  };
12073
12438
  var readJournal = (runtime, root) => {
@@ -12080,7 +12445,7 @@ var readJournal = (runtime, root) => {
12080
12445
  };
12081
12446
  var appendJournal = (runtime, root, entry2) => {
12082
12447
  const path = journalPath(root);
12083
- const made = runtime.mkdir(toAbsolutePath(join27(root, ".finitemonkey")));
12448
+ const made = runtime.mkdir(toAbsolutePath(join29(root, ".finitemonkey")));
12084
12449
  if (!made.ok) return made;
12085
12450
  const previous = runtime.readText(path);
12086
12451
  const before = previous.ok ? previous.value : "";
@@ -12322,6 +12687,83 @@ var GIT_HANDLERS = {
12322
12687
  PostToolUse: [gitPostToolUse]
12323
12688
  };
12324
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
+
12325
12767
  // src/prose/marker-guard.ts
12326
12768
  import { relative as relative6 } from "node:path";
12327
12769
 
@@ -12631,7 +13073,7 @@ var liveFindings = (input, texts) => {
12631
13073
  const accepted2 = acceptedFingerprints(baseline.ok ? baseline.value : EMPTY_BASELINE, input.runtime.now());
12632
13074
  return texts.flatMap((text6) => scanText(text6, { rules: CREDENTIAL_RULES })).filter((finding3) => !accepted2.has(finding3.fingerprint));
12633
13075
  };
12634
- var record = (input, findings, draft, where) => {
13076
+ var record2 = (input, findings, draft, where) => {
12635
13077
  const ids2 = findings.map((finding3) => {
12636
13078
  const saved = recordIncident(
12637
13079
  input.runtime,
@@ -12647,7 +13089,7 @@ var secretsUserPromptSubmit = (input) => {
12647
13089
  const prompt = optionalString(input.payload.raw, "prompt") ?? "";
12648
13090
  const findings = liveFindings(input, [prompt]);
12649
13091
  if (findings.length === 0) return { kind: "allow" };
12650
- const incident = record(input, findings, { source: "prompt", state: "exposicao-local" }, "prompt");
13092
+ const incident = record2(input, findings, { source: "prompt", state: "exposicao-local" }, "prompt");
12651
13093
  const items = findings.map((finding3) => ({ ruleId: finding3.ruleId, where: lineWhere(input.t, finding3) }));
12652
13094
  return {
12653
13095
  kind: "block",
@@ -12658,7 +13100,7 @@ var secretsPostToolUse = (input) => {
12658
13100
  const findings = liveFindings(input, stringLeaves(input.payload.raw["tool_response"]));
12659
13101
  if (findings.length === 0) return { kind: "allow" };
12660
13102
  const tool = toolName2(input.payload) || "tool";
12661
- 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}`);
12662
13104
  const items = findings.map((finding3) => ({ ruleId: finding3.ruleId, where: lineWhere(input.t, finding3) }));
12663
13105
  const context = input.t("secrets.post.context", { tool, found: describeFindings(input.t, items), incident });
12664
13106
  return {
@@ -12940,12 +13382,13 @@ var SECRETS_HANDLERS = {
12940
13382
  PostToolUse: [secretsPostToolUse],
12941
13383
  SessionStart: [secretsSessionStart]
12942
13384
  };
12943
- var DEFAULT_HANDLERS = mergeHandlers(
12944
- SECRETS_HANDLERS,
12945
- PORTAO_HANDLERS,
12946
- PROSE_HANDLERS,
12947
- GIT_HANDLERS
12948
- );
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));
12949
13392
 
12950
13393
  // src/hooks/payload.ts
12951
13394
  var EMPTY_PAYLOAD = {
@@ -13356,12 +13799,12 @@ var hookSchemaProblems = (hooks, claude) => {
13356
13799
  };
13357
13800
 
13358
13801
  // src/doctor/hook-sources.ts
13359
- import { join as join28 } from "node:path";
13802
+ import { join as join30 } from "node:path";
13360
13803
  var entriesOf = (source, hooks, own) => Object.entries(asRecord(hooks)).flatMap(
13361
13804
  ([event, groups]) => asList(groups).flatMap((group) => {
13362
- const record2 = asRecord(group);
13363
- const matcher = optionalString(record2, "matcher");
13364
- 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 }));
13365
13808
  })
13366
13809
  );
13367
13810
  var PROJECT_SCOPES = { project: "project", local: "local" };
@@ -13369,10 +13812,10 @@ var projectHookFiles = (env) => readSettings(env).flatMap(({ scope: scope2, path
13369
13812
  const source = PROJECT_SCOPES[scope2];
13370
13813
  return source === void 0 || !("hooks" in settings) ? [] : [{ source, path, hooks: settings["hooks"] }];
13371
13814
  });
13372
- 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");
13373
13816
  var installRecord = (value, root) => {
13374
13817
  const records = Array.isArray(value) ? value.filter(isRecord) : isRecord(value) ? [value] : [];
13375
- return records.find((record2) => record2["projectPath"] === root) ?? records[0];
13818
+ return records.find((record3) => record3["projectPath"] === root) ?? records[0];
13376
13819
  };
13377
13820
  var readJson = (env, path) => {
13378
13821
  const text6 = env.runtime.readText(path);
@@ -13380,18 +13823,18 @@ var readJson = (env, path) => {
13380
13823
  return parsed?.ok === true ? parsed.value : void 0;
13381
13824
  };
13382
13825
  var installedPlugin = (env) => {
13383
- const index = readJson(env, toAbsolutePath(join28(pluginsDir(env), "installed_plugins.json")));
13384
- const record2 = installRecord(asRecord(index?.["plugins"])[PLUGIN_ID], env.root);
13385
- const installPath = record2 === void 0 ? void 0 : optionalString(record2, "installPath");
13386
- 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;
13387
13830
  const root = toAbsolutePath(installPath);
13388
- const manifest = readJson(env, toAbsolutePath(join28(root, ".claude-plugin", "plugin.json")));
13389
- 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"));
13390
13833
  const text6 = env.runtime.readText(hooksPath);
13391
13834
  const parsed = text6.ok ? parseJsonObject(text6.value) : void 0;
13392
13835
  return {
13393
13836
  root,
13394
- 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"),
13395
13838
  hooksFile: parsed?.ok === true ? { source: "plugin", path: hooksPath, hooks: parsed.value["hooks"] } : void 0,
13396
13839
  hooksText: text6.ok ? text6.value : void 0
13397
13840
  };
@@ -13431,7 +13874,7 @@ var schemaFindings = (env, plugin, claude) => {
13431
13874
  const files = [...projectHookFiles(env), ...plugin?.hooksFile === void 0 ? [] : [plugin.hooksFile]];
13432
13875
  const invalidJson = [
13433
13876
  ...unreadableProjectFiles(env),
13434
- ...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"))] : []
13435
13878
  ];
13436
13879
  return [
13437
13880
  ...invalidJson.flatMap((file) => schemaFinding(file, ["not valid JSON"], label2)),
@@ -13452,7 +13895,7 @@ var pluginFindings = (env, projectOwn) => {
13452
13895
  ];
13453
13896
  };
13454
13897
  var coverageFindings = (env, entries) => {
13455
- 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;
13456
13899
  if (entries.length === 0 && !installed) {
13457
13900
  return [finding("hooks.registered", "warn", "doctor.hooks.none", {}, "doctor.hooks.fix_none")];
13458
13901
  }
@@ -13504,17 +13947,17 @@ var checkHooks = (env) => {
13504
13947
  };
13505
13948
 
13506
13949
  // src/doctor/instructions.ts
13507
- import { join as join30 } from "node:path";
13950
+ import { join as join32 } from "node:path";
13508
13951
  var INSTRUCTION_LINE_CEILING = 200;
13509
13952
  var TOP_INSTRUCTIONS = ["AGENTS.md", "CLAUDE.md", ".claude/CLAUDE.md", "CLAUDE.local.md"];
13510
13953
  var RULES_DIR = ".claude/rules";
13511
13954
  var SHOWN = 3;
13512
13955
  var readProjectFile = (env, relative9) => {
13513
- const text6 = env.runtime.readText(toAbsolutePath(join30(env.root, ...relative9.split("/"))));
13956
+ const text6 = env.runtime.readText(toAbsolutePath(join32(env.root, ...relative9.split("/"))));
13514
13957
  return text6.ok ? text6.value : void 0;
13515
13958
  };
13516
13959
  var instructionFiles = (env) => {
13517
- 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("/"))));
13518
13961
  const rules = listed4.ok ? listed4.value.filter((name) => name.endsWith(".md")).map((name) => `${RULES_DIR}/${name}`) : [];
13519
13962
  return [...TOP_INSTRUCTIONS, ...rules].flatMap((path) => {
13520
13963
  const text6 = readProjectFile(env, path);
@@ -13619,12 +14062,12 @@ var checkInstructions = (env) => {
13619
14062
  };
13620
14063
 
13621
14064
  // src/doctor/line-endings.ts
13622
- import { join as join31 } from "node:path";
14065
+ import { join as join33 } from "node:path";
13623
14066
  var LF_RULE = /^\*\s.*\beol=lf\b/m;
13624
14067
  var checkLineEndings = ({ root, runtime }) => {
13625
14068
  const autocrlf = runtime.git(["config", "--get", "core.autocrlf"], root);
13626
14069
  if (!autocrlf.ok || autocrlf.stdout.trim().toLowerCase() !== "true") return [];
13627
- const attributes = runtime.readText(toAbsolutePath(join31(root, ".gitattributes")));
14070
+ const attributes = runtime.readText(toAbsolutePath(join33(root, ".gitattributes")));
13628
14071
  if (attributes.ok && LF_RULE.test(attributes.value)) return [];
13629
14072
  return [
13630
14073
  finding("git.autocrlf", "warn", "doctor.git.autocrlf", { file: ".gitattributes" }, "doctor.git.fix_autocrlf")
@@ -13632,7 +14075,7 @@ var checkLineEndings = ({ root, runtime }) => {
13632
14075
  };
13633
14076
 
13634
14077
  // src/doctor/memory.ts
13635
- import { join as join32 } from "node:path";
14078
+ import { join as join34 } from "node:path";
13636
14079
  var ceilingOf = (config) => {
13637
14080
  const value = configAt(parseConfigYaml(config ?? ""), "memory", "maxLines");
13638
14081
  const parsed = typeof value === "string" ? Number.parseInt(value, 10) : Number.NaN;
@@ -13640,9 +14083,9 @@ var ceilingOf = (config) => {
13640
14083
  };
13641
14084
  var lineCount2 = (text6) => text6.replace(/\r?\n$/, "").split(/\r?\n/).length;
13642
14085
  var checkAgentsFile = ({ root, runtime }) => {
13643
- const text6 = runtime.readText(toAbsolutePath(join32(root, AGENTS_FILE)));
14086
+ const text6 = runtime.readText(toAbsolutePath(join34(root, AGENTS_FILE)));
13644
14087
  if (!text6.ok) return [];
13645
- const config = runtime.readText(toAbsolutePath(join32(root, CONFIG_FILE)));
14088
+ const config = runtime.readText(toAbsolutePath(join34(root, CONFIG_FILE)));
13646
14089
  const max = ceilingOf(config.ok ? config.value : void 0);
13647
14090
  const lines3 = lineCount2(text6.value);
13648
14091
  const params2 = { file: AGENTS_FILE, lines: lines3, max };
@@ -13650,7 +14093,7 @@ var checkAgentsFile = ({ root, runtime }) => {
13650
14093
  };
13651
14094
 
13652
14095
  // src/doctor/project-files.ts
13653
- import { join as join33 } from "node:path";
14096
+ import { join as join35 } from "node:path";
13654
14097
  var variants = (base, extensions) => extensions.map((extension) => `${base}${extension}`);
13655
14098
  var SCRIPT = [".js", ".cjs", ".mjs", ".ts", ".cts", ".mts"];
13656
14099
  var DATA = ["", ".json", ".yml", ".yaml"];
@@ -13681,7 +14124,7 @@ var CONFIG_GROUPS = [
13681
14124
  files: [...variants("commitlint.config", SCRIPT), ...variants(".commitlintrc", [...DATA, ".js"])]
13682
14125
  }
13683
14126
  ];
13684
- 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");
13685
14128
  var crowded = (env, groups, build) => groups.flatMap(({ name, files }) => {
13686
14129
  const found = present(env, files);
13687
14130
  return found.length > 1 ? [build(name, found.join(", "))] : [];
@@ -14160,11 +14603,11 @@ var unreachable = (runtime, repo, shas) => {
14160
14603
  var fsckDangling = (runtime, repo) => {
14161
14604
  const result = runtime.git(["fsck", "--no-reflogs", "--no-progress", "--dangling"], repo.root);
14162
14605
  const lines3 = linesOf(result.stdout);
14163
- const pick3 = (kind) => lines3.flatMap((line2) => {
14606
+ const pick4 = (kind) => lines3.flatMap((line2) => {
14164
14607
  const [label2, type, sha] = line2.split(" ");
14165
14608
  return label2 === "dangling" && type === kind && sha !== void 0 ? [sha] : [];
14166
14609
  });
14167
- return { commits: pick3("commit"), blobs: pick3("blob").length };
14610
+ return { commits: pick4("commit"), blobs: pick4("blob").length };
14168
14611
  };
14169
14612
  var describe = (runtime, repo, shas) => {
14170
14613
  if (shas.length === 0) return /* @__PURE__ */ new Map();
@@ -14264,7 +14707,7 @@ var rescueCommand = {
14264
14707
  };
14265
14708
 
14266
14709
  // src/git/undo.ts
14267
- 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";
14268
14711
  var INDEX_SUBJECT = "finitemonkey index:";
14269
14712
  var gitLine = (runtime, repo, args) => {
14270
14713
  const result = runtime.git(args, repo.root);
@@ -14307,7 +14750,7 @@ var findSnapshot = (views, wanted) => {
14307
14750
  };
14308
14751
  var stampOf2 = (iso) => iso.replace(/[:.]/g, "-");
14309
14752
  var worktreePath = (runtime, repo) => {
14310
- 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())}`);
14311
14754
  return toAbsolutePath(runtime.stat(toAbsolutePath(base)) === void 0 ? base : `${base}-2`);
14312
14755
  };
14313
14756
  var intoWorktree = (runtime, repo, view2) => {
@@ -14320,8 +14763,8 @@ var step2 = (runtime, repo, args) => {
14320
14763
  return result.ok ? accept(void 0) : reject(failure2(args.join(" "), result));
14321
14764
  };
14322
14765
  var inPlace = (runtime, repo, view2) => {
14323
- const current = gitLine(runtime, repo, ["rev-parse", "--verify", "--quiet", "HEAD"]);
14324
- 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) {
14325
14768
  const moved = step2(runtime, repo, ["reset", "--soft", view2.head]);
14326
14769
  if (!moved.ok) return moved;
14327
14770
  }
@@ -14542,7 +14985,7 @@ var hookCommand = {
14542
14985
  };
14543
14986
 
14544
14987
  // src/commands/i18n-check.ts
14545
- import { join as join35 } from "node:path";
14988
+ import { join as join37 } from "node:path";
14546
14989
 
14547
14990
  // src/i18n/message.ts
14548
14991
  var ARG = /^\s*([a-zA-Z][a-zA-Z0-9_]*)\s*(?:,\s*(plural|select)\s*,)?/;
@@ -14744,7 +15187,7 @@ var unreadable = (dir, file, detail) => ({
14744
15187
  exitCode: ExitCode.NotConfigured
14745
15188
  });
14746
15189
  var readCatalog = (ctx, dir, lang) => {
14747
- const file = toAbsolutePath(join35(dir, `${lang}.json`));
15190
+ const file = toAbsolutePath(join37(dir, `${lang}.json`));
14748
15191
  const text6 = ctx.runtime.readText(file);
14749
15192
  if (!text6.ok) return reject(unreadable(dir, file, text6.error));
14750
15193
  const parsed = parseJsonObject(text6.value);
@@ -14767,7 +15210,7 @@ var describeProblem = (ctx, problem4) => {
14767
15210
  return ` ${problem4.lang} ${problem4.key} ${kind}`;
14768
15211
  };
14769
15212
  var loadReview = (ctx, dir) => {
14770
- const text6 = ctx.runtime.readText(toAbsolutePath(join35(dir, "review.json")));
15213
+ const text6 = ctx.runtime.readText(toAbsolutePath(join37(dir, "review.json")));
14771
15214
  const parsed = text6.ok ? parseJsonObject(text6.value) : void 0;
14772
15215
  if (parsed?.ok !== true) return void 0;
14773
15216
  const statuses = (lang) => {
@@ -14808,10 +15251,10 @@ var i18nCheckCommand = {
14808
15251
  };
14809
15252
 
14810
15253
  // src/history/look-back.ts
14811
- import { join as join36 } from "node:path";
15254
+ import { join as join38 } from "node:path";
14812
15255
 
14813
15256
  // src/history/patterns.ts
14814
- import { posix as posix4 } from "node:path";
15257
+ import { posix as posix5 } from "node:path";
14815
15258
  var PATTERNS = ["done_after_failing_tests", "env_read", "no_verify", "force_push", "test_skip"];
14816
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)/;
14817
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;
@@ -14833,7 +15276,7 @@ var stringField = (tool, name) => {
14833
15276
  return typeof value === "string" ? value : "";
14834
15277
  };
14835
15278
  var isEnvPath = (path) => {
14836
- const name = posix4.basename(path.replace(/\\/g, "/"));
15279
+ const name = posix5.basename(path.replace(/\\/g, "/"));
14837
15280
  return /^\.env(\.[A-Za-z0-9_-]+)?$/.test(name) && !ENV_TEMPLATE.test(name);
14838
15281
  };
14839
15282
  var envRead = (tool) => {
@@ -14921,7 +15364,7 @@ var addResults = (draft, blocks) => {
14921
15364
  };
14922
15365
  var parseTranscript = (jsonl) => {
14923
15366
  const turns = [];
14924
- let current;
15367
+ let current2;
14925
15368
  for (const line2 of jsonl.split(/\r?\n/)) {
14926
15369
  if (line2.trim() === "") continue;
14927
15370
  const parsed = parseJsonObject(line2);
@@ -14929,13 +15372,13 @@ var parseTranscript = (jsonl) => {
14929
15372
  const entry2 = parsed.value;
14930
15373
  const blocks = blocksOf(entry2["message"]);
14931
15374
  if (entry2["type"] === "user") {
14932
- if (current !== void 0 && addResults(current, blocks)) continue;
15375
+ if (current2 !== void 0 && addResults(current2, blocks)) continue;
14933
15376
  const prompt = blocks.map(textOf3).join("\n").trim();
14934
15377
  if (prompt === "") continue;
14935
- current = startTurn(prompt);
14936
- turns.push(current);
14937
- } else if (entry2["type"] === "assistant" && current !== void 0) {
14938
- addAssistant(current, blocks);
15378
+ current2 = startTurn(prompt);
15379
+ turns.push(current2);
15380
+ } else if (entry2["type"] === "assistant" && current2 !== void 0) {
15381
+ addAssistant(current2, blocks);
14939
15382
  }
14940
15383
  }
14941
15384
  return turns;
@@ -14944,7 +15387,7 @@ var parseTranscript = (jsonl) => {
14944
15387
  // src/history/look-back.ts
14945
15388
  var SAMPLES = 3;
14946
15389
  var projectLogKey = (root) => root.replace(/[^A-Za-z0-9-]/g, "-");
14947
- 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));
14948
15391
  var lookBack = (runtime, root, env) => {
14949
15392
  const dir = projectLogDir(runtime, root, env);
14950
15393
  const listed4 = runtime.listDir(toAbsolutePath(dir));
@@ -14965,7 +15408,7 @@ var lookBack = (runtime, root, env) => {
14965
15408
  };
14966
15409
  let turns = 0;
14967
15410
  for (const name of files) {
14968
- const text6 = runtime.readText(toAbsolutePath(join36(dir, name)));
15411
+ const text6 = runtime.readText(toAbsolutePath(join38(dir, name)));
14969
15412
  if (!text6.ok) continue;
14970
15413
  for (const turn of parseTranscript(text6.value)) {
14971
15414
  turns += 1;
@@ -15181,6 +15624,32 @@ var initCommand = {
15181
15624
  }
15182
15625
  };
15183
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
+
15184
15653
  // src/commands/prose-check.ts
15185
15654
  import { relative as relative8 } from "node:path";
15186
15655
 
@@ -15543,7 +16012,7 @@ var view = (finding3, status) => ({
15543
16012
  status,
15544
16013
  ...finding3.commit === void 0 ? {} : { commit: finding3.commit }
15545
16014
  });
15546
- var modeOf2 = (values) => {
16015
+ var modeOf3 = (values) => {
15547
16016
  const chosen = [values["staged"] === true, values["history"] === true, stringOption(values, "path") !== void 0];
15548
16017
  if (chosen.filter(Boolean).length > 1) return void 0;
15549
16018
  if (chosen[0] === true) return "staged";
@@ -15634,7 +16103,7 @@ var secretsScanCommand = {
15634
16103
  status: "ready",
15635
16104
  options: { staged: { type: "boolean" }, history: { type: "boolean" }, path: { type: "string" } },
15636
16105
  run: async (ctx, args) => {
15637
- const mode = modeOf2(args.values);
16106
+ const mode = modeOf3(args.values);
15638
16107
  const fix = "finitemonkey help secrets scan";
15639
16108
  if (mode === void 0) return failureFrom(ctx.t, usageProblem("invalid_arguments", "secrets.error.one_mode", fix));
15640
16109
  const root = projectRoot(ctx);
@@ -15656,7 +16125,7 @@ var secretsScanCommand = {
15656
16125
  };
15657
16126
 
15658
16127
  // src/task/create.ts
15659
- import { join as join37 } from "node:path";
16128
+ import { join as join39 } from "node:path";
15660
16129
 
15661
16130
  // src/core/yaml/write.ts
15662
16131
  var PLAIN_SAFE = /^[A-Za-z_./][A-Za-z0-9_./ ()-]*$/;
@@ -15715,7 +16184,8 @@ var contractYaml = (contract) => ({
15715
16184
  tests_protected: contract.scope.testsProtected
15716
16185
  },
15717
16186
  acceptance: contract.acceptance.map(checkYaml),
15718
- decisions_needed: [...contract.decisionsNeeded]
16187
+ decisions_needed: [...contract.decisionsNeeded],
16188
+ ...contract.callersIgnore === void 0 ? {} : { callers_ignore: [...contract.callersIgnore] }
15719
16189
  });
15720
16190
  var contractJson = (contract) => ({
15721
16191
  id: contract.id,
@@ -15740,10 +16210,10 @@ var explicitChecks = (draft) => [
15740
16210
  var draftChecks = (runtime, paths, draft) => {
15741
16211
  const disk = {
15742
16212
  read: (relative9) => {
15743
- const text6 = runtime.readText(toAbsolutePath(join37(paths.root, relative9)));
16213
+ const text6 = runtime.readText(toAbsolutePath(join39(paths.root, relative9)));
15744
16214
  return text6.ok ? text6.value : void 0;
15745
16215
  },
15746
- exists: (relative9) => runtime.stat(toAbsolutePath(join37(paths.root, relative9))) !== void 0
16216
+ exists: (relative9) => runtime.stat(toAbsolutePath(join39(paths.root, relative9))) !== void 0
15747
16217
  };
15748
16218
  const detected = detectChecks(disk);
15749
16219
  const base = draft.runs.length > 0 || draft.tests !== void 0 ? explicitChecks(draft) : detected;
@@ -15756,6 +16226,7 @@ var draftChecks = (runtime, paths, draft) => {
15756
16226
  { id: "prose", kind: "prose" },
15757
16227
  { id: "worktree", kind: "worktree" },
15758
16228
  { id: "merge", kind: "merge" },
16229
+ { id: "callers", kind: "callers" },
15759
16230
  ...draft.paths.length > 0 || draft.budget !== void 0 ? [{ id: "diff", kind: "diff" }] : [],
15760
16231
  ...draft.git.length > 0 ? [{ id: "git", kind: "git", require: draft.git }] : [],
15761
16232
  { id: "pr", kind: "pr", requireOpen: draft.git.includes("pr_open") },
@@ -15945,15 +16416,15 @@ var taskDoneCommand = {
15945
16416
  };
15946
16417
 
15947
16418
  // src/install/uninstall.ts
15948
- var structural = (entry2, current) => {
16419
+ var structural = (entry2, current2) => {
15949
16420
  if (entry2.kind === "gitignore" || entry2.kind === "markdown") {
15950
- const text6 = entry2.kind === "gitignore" ? withoutBlock(current) : withoutMarkdownBlock(current);
16421
+ const text6 = entry2.kind === "gitignore" ? withoutBlock(current2) : withoutMarkdownBlock(current2);
15951
16422
  return text6 === "" && !entry2.existed ? void 0 : text6;
15952
16423
  }
15953
- const parsed = parseSettings(current);
15954
- if (!parsed.ok || entry2.added === void 0) return current;
16424
+ const parsed = parseSettings(current2);
16425
+ if (!parsed.ok || entry2.added === void 0) return current2;
15955
16426
  const settings = unmergeSettings(parsed.value, entry2.added, entry2.handlers ?? []);
15956
- 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));
15957
16428
  };
15958
16429
  var unchanged = (project, entry2) => {
15959
16430
  if (!entry2.existed) return { path: entry2.path, action: "delete", after: void 0 };
@@ -15961,17 +16432,17 @@ var unchanged = (project, entry2) => {
15961
16432
  return original === void 0 ? void 0 : { path: entry2.path, action: "restore", after: original };
15962
16433
  };
15963
16434
  var removalFor = (project, entry2, options) => {
15964
- const current = project.read(entry2.path);
15965
- 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 };
15966
16437
  if (entry2.kind === "generated" && options.keepGenerated) {
15967
- return { path: entry2.path, action: "keep-generated", after: current };
16438
+ return { path: entry2.path, action: "keep-generated", after: current2 };
15968
16439
  }
15969
- if (hashText(current) === entry2.hash) {
16440
+ if (hashText(current2) === entry2.hash) {
15970
16441
  const exact2 = unchanged(project, entry2);
15971
16442
  if (exact2 !== void 0) return exact2;
15972
16443
  }
15973
- if (entry2.kind === "generated") return { path: entry2.path, action: "keep-edited", after: current };
15974
- 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);
15975
16446
  return { path: entry2.path, action: after === void 0 ? "delete" : "unmerge", after };
15976
16447
  };
15977
16448
  var planRemoval = (project, lock, options) => ({
@@ -16136,6 +16607,7 @@ var REGISTRY = [
16136
16607
  taskDoneCommand,
16137
16608
  secretsScanCommand,
16138
16609
  secretsAcceptCommand,
16610
+ policyFalsePositiveCommand,
16139
16611
  secretsIncidentCommand,
16140
16612
  proseCheckCommand,
16141
16613
  snapshotCommand,
@@ -16147,7 +16619,7 @@ var REGISTRY = [
16147
16619
  ];
16148
16620
 
16149
16621
  // src/i18n/lang-settings.ts
16150
- import { join as join38 } from "node:path";
16622
+ import { join as join40 } from "node:path";
16151
16623
  var read3 = (runtime, path) => {
16152
16624
  const text6 = runtime.readText(toAbsolutePath(path));
16153
16625
  return text6.ok ? text6.value : void 0;
@@ -16162,14 +16634,14 @@ var languageIn = (text6) => {
16162
16634
  const value = parsed.ok && isRecord(parsed.value) ? parsed.value["language"] : void 0;
16163
16635
  return typeof value === "string" && value.trim() !== "" ? value : void 0;
16164
16636
  };
16165
- 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");
16166
16638
  var langSettings = (runtime, cwd, env, platform) => {
16167
16639
  const root = findGitRoot(cwd, runtime) ?? cwd;
16168
- const config = parseConfigYaml(read3(runtime, join38(root, CONFIG_FILE)) ?? "");
16640
+ const config = parseConfigYaml(read3(runtime, join40(root, CONFIG_FILE)) ?? "");
16169
16641
  const claudeFiles = [
16170
- join38(root, ".claude", "settings.local.json"),
16171
- join38(root, ".claude", "settings.json"),
16172
- 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")
16173
16645
  ];
16174
16646
  return {
16175
16647
  config: configText(config, "lang"),