finitemonkey 0.1.0-next.15 → 0.1.0-next.17

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
 
@@ -69,7 +69,7 @@ var resolveWindowsCommand = (command, env, exists) => {
69
69
  }
70
70
  return { command, shell: false };
71
71
  };
72
- var windowsCommandLine = (resolved, args) => [quoteForCmd(resolved.command), ...args.map(quoteForCmd)].join(" ");
72
+ var windowsCommandLine = (resolved, args2) => [quoteForCmd(resolved.command), ...args2.map(quoteForCmd)].join(" ");
73
73
 
74
74
  // src/core/node-exec.ts
75
75
  var SHELL_NOT_FOUND = /* @__PURE__ */ new Set([127, 9009]);
@@ -117,9 +117,9 @@ var nodeAppendText = (path, content) => {
117
117
  return reject(error instanceof Error ? error.message : String(error));
118
118
  }
119
119
  };
120
- var nodeSpawnSelf = (selfPath) => (args, cwd) => {
120
+ var nodeSpawnSelf = (selfPath) => (args2, cwd) => {
121
121
  try {
122
- const child2 = spawn(process.execPath, [selfPath, ...args], {
122
+ const child2 = spawn(process.execPath, [selfPath, ...args2], {
123
123
  cwd,
124
124
  detached: true,
125
125
  stdio: "ignore",
@@ -290,8 +290,8 @@ import { createInterface } from "node:readline";
290
290
  var MAX_BUFFER2 = 256 * 1024 * 1024;
291
291
  var GIT_ENV = { ...process.env, GIT_PAGER: "cat", GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" };
292
292
  var notFound = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
293
- var nodeGit = (args, cwd) => {
294
- const result = spawnSync2("git", [...args], {
293
+ var nodeGit = (args2, cwd) => {
294
+ const result = spawnSync2("git", [...args2], {
295
295
  cwd,
296
296
  encoding: "utf8",
297
297
  maxBuffer: MAX_BUFFER2,
@@ -305,8 +305,8 @@ var nodeGit = (args, cwd) => {
305
305
  stdout: typeof stdout === "string" ? stdout : ""
306
306
  };
307
307
  };
308
- var nodeGitLines = (args, cwd, onLine) => new Promise((resolve2) => {
309
- const child2 = spawn2("git", [...args], {
308
+ var nodeGitLines = (args2, cwd, onLine) => new Promise((resolve2) => {
309
+ const child2 = spawn2("git", [...args2], {
310
310
  cwd,
311
311
  windowsHide: true,
312
312
  env: GIT_ENV,
@@ -323,8 +323,8 @@ var nodeGitLines = (args, cwd, onLine) => new Promise((resolve2) => {
323
323
  });
324
324
  });
325
325
  var textOf = (value) => typeof value === "string" ? value : "";
326
- var nodeGitWith = (args, cwd, options) => {
327
- const result = spawnSync2("git", [...args], {
326
+ var nodeGitWith = (args2, cwd, options) => {
327
+ const result = spawnSync2("git", [...args2], {
328
328
  cwd,
329
329
  encoding: "utf8",
330
330
  maxBuffer: MAX_BUFFER2,
@@ -350,15 +350,15 @@ var errnoCode2 = (error) => {
350
350
  if (!(error instanceof Error) || !("code" in error)) return void 0;
351
351
  return typeof error.code === "string" ? error.code : void 0;
352
352
  };
353
- var spawnProbe = (command, args, timeout) => {
353
+ var spawnProbe = (command, args2, timeout) => {
354
354
  const options = { encoding: "utf8", timeout, windowsHide: true };
355
- return process.platform === "win32" ? spawnSync3([command, ...args].join(" "), { ...options, shell: true }) : spawnSync3(command, [...args], options);
355
+ return process.platform === "win32" ? spawnSync3([command, ...args2].join(" "), { ...options, shell: true }) : spawnSync3(command, [...args2], options);
356
356
  };
357
- var nodeProbe = (command, args, timeoutMs) => {
358
- if (![command, ...args].every((token) => SAFE_TOKEN.test(token))) {
357
+ var nodeProbe = (command, args2, timeoutMs) => {
358
+ if (![command, ...args2].every((token) => SAFE_TOKEN.test(token))) {
359
359
  return { found: false, ok: false, stdout: "", timedOut: false };
360
360
  }
361
- const result = spawnProbe(command, args, timeoutMs);
361
+ const result = spawnProbe(command, args2, timeoutMs);
362
362
  const code = errnoCode2(result.error);
363
363
  const stdout = result.stdout;
364
364
  return {
@@ -409,14 +409,155 @@ var nodeRuntime = () => ({
409
409
  exec: nodeExec,
410
410
  appendText: nodeAppendText,
411
411
  spawnSelf: nodeSpawnSelf(SELF_PATH),
412
+ execSelf: (args2, cwd, timeoutMs) => nodeExec({ command: process.execPath, args: [SELF_PATH, ...args2], cwd, timeoutMs }),
412
413
  gitWith: nodeGitWith,
413
414
  realpath: nodeRealpath,
414
415
  homeDir: homedir,
415
416
  systemLocale: () => Intl.DateTimeFormat().resolvedOptions().locale
416
417
  });
417
418
 
418
- // src/commands/doctor.ts
419
- import { dirname as dirname7 } from "node:path";
419
+ // src/app/node-process.ts
420
+ import { spawn as spawn3, spawnSync as spawnSync4 } from "node:child_process";
421
+ import { connect } from "node:net";
422
+ var MAX_OUTPUT = 64 * 1024;
423
+ var POLL_MS = 150;
424
+ var STOP_GRACE_MS = 3e3;
425
+ var sleep = (ms) => new Promise((resolve2) => {
426
+ setTimeout(resolve2, ms);
427
+ });
428
+ var portOpen = (host, port) => new Promise((resolve2) => {
429
+ const socket = connect({ host, port });
430
+ const done = (open) => {
431
+ socket.destroy();
432
+ resolve2(open);
433
+ };
434
+ socket.setTimeout(1e3, () => {
435
+ done(false);
436
+ });
437
+ socket.once("connect", () => {
438
+ done(true);
439
+ });
440
+ socket.once("error", () => {
441
+ done(false);
442
+ });
443
+ });
444
+ var startApp = (command, cwd, port) => {
445
+ const child2 = spawn3(command, {
446
+ cwd,
447
+ shell: true,
448
+ detached: process.platform !== "win32",
449
+ windowsHide: true,
450
+ stdio: ["ignore", "pipe", "pipe"],
451
+ env: { ...process.env, PORT: String(port) }
452
+ });
453
+ let output = "";
454
+ let exit;
455
+ const collect4 = (chunk) => {
456
+ output = `${output}${chunk.toString("utf8")}`.slice(-MAX_OUTPUT);
457
+ };
458
+ child2.stdout.on("data", collect4);
459
+ child2.stderr.on("data", collect4);
460
+ child2.once("error", (error) => {
461
+ output = `${output}${error.message}`;
462
+ exit = { code: null };
463
+ });
464
+ child2.once("exit", (code) => {
465
+ exit = { code };
466
+ });
467
+ return { child: child2, output: () => output, exited: () => exit };
468
+ };
469
+ var waitForPort = async (app, host, port, deadline) => {
470
+ while (Date.now() < deadline) {
471
+ if (await portOpen(host, port)) return "open";
472
+ if (app.exited() !== void 0) return "exited";
473
+ await sleep(POLL_MS);
474
+ }
475
+ return "timeout";
476
+ };
477
+ var signalGroup = (pid, signal) => {
478
+ try {
479
+ process.kill(-pid, signal);
480
+ } catch {
481
+ return;
482
+ }
483
+ };
484
+ var alive = (pid) => {
485
+ try {
486
+ process.kill(-pid, 0);
487
+ return true;
488
+ } catch {
489
+ return false;
490
+ }
491
+ };
492
+ var stopApp = async (app) => {
493
+ const pid = app.child.pid;
494
+ if (pid === void 0) return true;
495
+ if (process.platform === "win32") {
496
+ spawnSync4("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
497
+ return true;
498
+ }
499
+ signalGroup(pid, "SIGTERM");
500
+ const deadline = Date.now() + STOP_GRACE_MS;
501
+ while (alive(pid) && Date.now() < deadline) await sleep(POLL_MS);
502
+ if (alive(pid)) signalGroup(pid, "SIGKILL");
503
+ return !alive(pid);
504
+ };
505
+
506
+ // src/app/node-probe.ts
507
+ var BODY_EXCERPT = 200;
508
+ var MAX_BODY = 256 * 1024;
509
+ var url = (request) => {
510
+ const host = request.host.includes(":") ? `[${request.host}]` : request.host;
511
+ return `http://${host}:${String(request.port)}${request.route.startsWith("/") ? "" : "/"}${request.route}`;
512
+ };
513
+ var ask = async (request, deadline) => {
514
+ try {
515
+ const response = await fetch(url(request), {
516
+ redirect: "manual",
517
+ signal: AbortSignal.timeout(Math.max(1e3, deadline - Date.now()))
518
+ });
519
+ const body2 = (await response.text()).slice(0, MAX_BODY);
520
+ const excerpt = body2.slice(0, BODY_EXCERPT);
521
+ if (response.status !== request.status)
522
+ return { kind: "wrong_status", status: response.status, bodyExcerpt: excerpt };
523
+ if (request.body !== void 0 && !body2.includes(request.body)) {
524
+ return { kind: "body_mismatch", status: response.status, bodyExcerpt: excerpt };
525
+ }
526
+ return { kind: "ok", status: response.status };
527
+ } catch (error) {
528
+ return { kind: "request_failed", bodyExcerpt: error instanceof Error ? error.message : String(error) };
529
+ }
530
+ };
531
+ var finish = async (app, answer, started) => {
532
+ const stopped = app === void 0 ? true : await stopApp(app);
533
+ return {
534
+ ...answer,
535
+ reused: app === void 0,
536
+ output: app?.output() ?? "",
537
+ stopped,
538
+ durationMs: Date.now() - started
539
+ };
540
+ };
541
+ var probeApp = async (request) => {
542
+ const started = Date.now();
543
+ const deadline = started + request.timeoutMs;
544
+ if (await portOpen(request.host, request.port)) return finish(void 0, await ask(request, deadline), started);
545
+ if (request.start === void 0) return finish(void 0, { kind: "no_start" }, started);
546
+ const app = startApp(request.start, request.cwd, request.port);
547
+ const port = await waitForPort(app, request.host, request.port, deadline);
548
+ if (port === "exited") return finish(app, { kind: "crashed", exitCode: app.exited()?.code ?? null }, started);
549
+ if (port === "timeout") return finish(app, { kind: "no_port" }, started);
550
+ return finish(app, await ask(request, deadline), started);
551
+ };
552
+
553
+ // src/app/types.ts
554
+ var DEFAULT_HOST = "127.0.0.1";
555
+ var DEFAULT_ROUTE = "/";
556
+ var DEFAULT_STATUS = 200;
557
+ var DEFAULT_APP_TIMEOUT_S = 60;
558
+
559
+ // src/core/args.ts
560
+ import { parseArgs } from "node:util";
420
561
 
421
562
  // src/exit-codes.ts
422
563
  var ExitCode = {
@@ -444,6 +585,89 @@ var exitCodeDocs = {
444
585
  5: "exit.internal"
445
586
  };
446
587
 
588
+ // src/core/problem.ts
589
+ var usageProblem = (code, messageKey, fix, params2 = {}) => ({
590
+ code,
591
+ messageKey,
592
+ params: params2,
593
+ fix,
594
+ exitCode: ExitCode.Usage
595
+ });
596
+ var needsPersonProblem = (messageKey, fix, params2 = {}) => ({
597
+ code: "needs_confirmation",
598
+ messageKey,
599
+ params: params2,
600
+ fix,
601
+ exitCode: ExitCode.NeedsPerson
602
+ });
603
+ var FmError = class extends Error {
604
+ problem;
605
+ constructor(problem4) {
606
+ super(problem4.code);
607
+ this.name = "FmError";
608
+ this.problem = problem4;
609
+ }
610
+ };
611
+
612
+ // src/core/args.ts
613
+ var GLOBAL_OPTIONS = {
614
+ json: { type: "boolean" },
615
+ lang: { type: "string" },
616
+ yes: { type: "boolean", short: "y" },
617
+ "dry-run": { type: "boolean" },
618
+ version: { type: "boolean", short: "v" },
619
+ help: { type: "boolean", short: "h" }
620
+ };
621
+ var flag = (values, name) => values[name] === true;
622
+ var looseArgs = (argv) => {
623
+ const { values, positionals } = parseArgs({
624
+ args: [...argv],
625
+ options: GLOBAL_OPTIONS,
626
+ strict: false,
627
+ allowPositionals: true
628
+ });
629
+ const lang = values.lang;
630
+ return {
631
+ positionals,
632
+ json: flag(values, "json"),
633
+ yes: flag(values, "yes"),
634
+ dryRun: flag(values, "dry-run"),
635
+ version: flag(values, "version"),
636
+ help: flag(values, "help"),
637
+ lang: typeof lang === "string" ? lang : void 0
638
+ };
639
+ };
640
+ var resolveCommand = (registry, positionals) => registry.filter((spec2) => spec2.path.every((part2, index) => positionals[index] === part2)).reduce(
641
+ (best, spec2) => best === void 0 || spec2.path.length > best.consumed ? { spec: spec2, consumed: spec2.path.length } : best,
642
+ void 0
643
+ );
644
+ var strictArgs = (argv, spec2) => {
645
+ const fix = `finitemonkey help ${spec2.path.join(" ")}`;
646
+ let parsed;
647
+ try {
648
+ parsed = parseArgs({
649
+ args: [...argv],
650
+ options: { ...GLOBAL_OPTIONS, ...spec2.options },
651
+ strict: true,
652
+ allowPositionals: true
653
+ });
654
+ } catch (error) {
655
+ const detail = error instanceof Error ? error.message : String(error);
656
+ return reject(usageProblem("invalid_arguments", "error.invalid_arguments", fix, { detail }));
657
+ }
658
+ const rest = parsed.positionals.slice(spec2.path.length);
659
+ const max = spec2.maxPositionals ?? 0;
660
+ const extra = rest[max];
661
+ if (extra !== void 0) {
662
+ return reject(usageProblem("unexpected_argument", "error.unexpected_argument", fix, { argument: extra }));
663
+ }
664
+ return accept({ values: parsed.values, positionals: rest });
665
+ };
666
+ var stringOption = (values, name) => {
667
+ const value = values[name];
668
+ return typeof value === "string" ? value : void 0;
669
+ };
670
+
447
671
  // src/core/result.ts
448
672
  var SCHEMA_VERSION = 1;
449
673
  var success = (data, human2 = []) => ({
@@ -468,6 +692,87 @@ var toEnvelope = (command, result) => ({
468
692
  ...result.next === void 0 ? {} : { next: result.next }
469
693
  });
470
694
 
695
+ // src/commands/app-check.ts
696
+ var APP_CHECK_SCHEMA = "finitemonkey/app-check@1";
697
+ var positive = (raw, fallback) => {
698
+ if (raw === void 0) return fallback;
699
+ const value = Number(raw);
700
+ return Number.isInteger(value) && value > 0 ? value : void 0;
701
+ };
702
+ var requestFrom = (ctx, values) => {
703
+ const port = positive(stringOption(values, "port"), 0);
704
+ const status = positive(stringOption(values, "status"), DEFAULT_STATUS);
705
+ const timeout = positive(stringOption(values, "timeout"), DEFAULT_APP_TIMEOUT_S);
706
+ if (port === void 0 || port === 0 || port > 65535 || status === void 0 || timeout === void 0)
707
+ return void 0;
708
+ return {
709
+ host: stringOption(values, "host") ?? DEFAULT_HOST,
710
+ port,
711
+ route: stringOption(values, "route") ?? DEFAULT_ROUTE,
712
+ status,
713
+ body: stringOption(values, "body"),
714
+ start: stringOption(values, "start"),
715
+ cwd: ctx.cwd,
716
+ timeoutMs: timeout * 1e3
717
+ };
718
+ };
719
+ var humanLine = (ctx, data) => ctx.t(`app.check.${data.kind}`, {
720
+ url: data.url,
721
+ status: data.status ?? 0,
722
+ code: data.exitCode ?? -1
723
+ });
724
+ var toResult = (ctx, request, data) => {
725
+ const human2 = [humanLine(ctx, data)];
726
+ if (data.kind === "ok") return success(data, human2);
727
+ return {
728
+ ok: false,
729
+ exitCode: ExitCode.CheckFailed,
730
+ data,
731
+ human: human2,
732
+ error: {
733
+ code: "app_check_failed",
734
+ message: human2.join(" "),
735
+ fix: `finitemonkey app check --port ${String(request.port)}`
736
+ }
737
+ };
738
+ };
739
+ var appCheckCommand = {
740
+ path: ["app", "check"],
741
+ summaryKey: "command.app_check.summary",
742
+ usage: "finitemonkey app check --port <n> [--start <command>] [--route <path>] [--status <code>] [--body <text>] [--timeout <seconds>] [--host <host>]",
743
+ status: "ready",
744
+ options: {
745
+ port: { type: "string" },
746
+ start: { type: "string" },
747
+ route: { type: "string" },
748
+ status: { type: "string" },
749
+ body: { type: "string" },
750
+ timeout: { type: "string" },
751
+ host: { type: "string" }
752
+ },
753
+ run: async (ctx, args2) => {
754
+ const request = requestFrom(ctx, args2.values);
755
+ if (request === void 0) {
756
+ return {
757
+ ok: false,
758
+ exitCode: ExitCode.Usage,
759
+ human: [ctx.t("app.check.usage")],
760
+ error: {
761
+ code: "app_check_failed",
762
+ message: ctx.t("app.check.usage"),
763
+ fix: "finitemonkey app check --port 3000"
764
+ }
765
+ };
766
+ }
767
+ const result = await probeApp(request);
768
+ const url2 = `http://${request.host}:${String(request.port)}${request.route}`;
769
+ return toResult(ctx, request, { schema: APP_CHECK_SCHEMA, url: url2, ...result });
770
+ }
771
+ };
772
+
773
+ // src/commands/doctor.ts
774
+ import { dirname as dirname7 } from "node:path";
775
+
471
776
  // src/install/apply.ts
472
777
  import { dirname as dirname3 } from "node:path";
473
778
 
@@ -512,12 +817,12 @@ var parseAdded = (value) => {
512
817
  const deny2 = strings(value["deny"]);
513
818
  const { schema, attribution } = value;
514
819
  if (deny2 === void 0 || typeof schema !== "boolean" || typeof attribution !== "boolean") return void 0;
515
- const ask = strings(value["ask"]) ?? [];
820
+ const ask2 = strings(value["ask"]) ?? [];
516
821
  const blockReads = value["blockReads"] === true;
517
822
  const marketplace = value["marketplace"] === true;
518
823
  const plugin = value["plugin"] === true;
519
824
  const language = value["language"];
520
- const added = { schema, attribution, deny: deny2, ask, blockReads, marketplace, plugin };
825
+ const added = { schema, attribution, deny: deny2, ask: ask2, blockReads, marketplace, plugin };
521
826
  return typeof language === "string" ? { ...added, language } : added;
522
827
  };
523
828
  var parseEntry = (value) => {
@@ -544,11 +849,11 @@ var parseEntries = (value) => {
544
849
  const entries = value.map(parseEntry);
545
850
  return entries.every((entry2) => entry2 !== void 0) ? entries : void 0;
546
851
  };
547
- var fromRecord = (record2) => {
548
- const { schema, version, preset, installedAt, updatedAt } = record2;
852
+ var fromRecord = (record3) => {
853
+ const { schema, version, preset, installedAt, updatedAt } = record3;
549
854
  const known2 = PRESETS.find((item) => item === preset);
550
- const entries = parseEntries(record2["entries"]);
551
- const createdDirs = strings(record2["createdDirs"]);
855
+ const entries = parseEntries(record3["entries"]);
856
+ const createdDirs = strings(record3["createdDirs"]);
552
857
  if (schema !== LOCK_SCHEMA) return reject(`schema is not ${LOCK_SCHEMA}`);
553
858
  if (typeof version !== "string" || typeof installedAt !== "string" || typeof updatedAt !== "string") {
554
859
  return reject("version, installedAt and updatedAt must be text");
@@ -565,6 +870,134 @@ var parseLock = (text6) => {
565
870
  var renderLock = (lock) => `${JSON.stringify(lock, null, 2)}
566
871
  `;
567
872
 
873
+ // src/policy/policy.ts
874
+ import { join as join3 } from "node:path";
875
+
876
+ // src/git/project-config.ts
877
+ import { join as join2 } from "node:path";
878
+ var CONFIG_FILE2 = ".finitemonkey/config.yml";
879
+ var unquote = (value) => value.trim().replace(/^(["'])(.*)\1$/s, "$2");
880
+ var stripComment = (line2) => {
881
+ let quote;
882
+ for (let index = 0; index < line2.length; index += 1) {
883
+ const char = line2.charAt(index);
884
+ if (quote === void 0 && (char === '"' || char === "'")) quote = char;
885
+ else if (char === quote) quote = void 0;
886
+ else if (quote === void 0 && char === "#" && (index === 0 || /\s/.test(line2.charAt(index - 1)))) {
887
+ return line2.slice(0, index);
888
+ }
889
+ }
890
+ return line2;
891
+ };
892
+ var flowList = (value) => {
893
+ const trimmed = value.trim();
894
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return void 0;
895
+ return trimmed.slice(1, -1).split(",").map(unquote).filter((item) => item !== "");
896
+ };
897
+ var keyValue = (body2) => {
898
+ const match = /^("[^"]*"|'[^']*'|[^:]+?)\s*:(?:\s+(.*))?$/.exec(body2);
899
+ if (match === null) return void 0;
900
+ return { key: unquote(match[1] ?? ""), value: match[2] ?? "" };
901
+ };
902
+ var addItem = (frame, item) => {
903
+ if (frame.key === void 0) return;
904
+ const parent = frame.tree[frame.key];
905
+ if (Array.isArray(parent)) parent.push(item);
906
+ else frame.tree[frame.key] = [item];
907
+ };
908
+ var placeLine = (stack, indent, body2) => {
909
+ while (stack.length > 1 && (stack[stack.length - 1]?.indent ?? 0) >= indent && !body2.startsWith("- ")) stack.pop();
910
+ const top = stack[stack.length - 1];
911
+ if (top === void 0) return;
912
+ if (body2.startsWith("- ")) {
913
+ addItem(top, unquote(body2.slice(2)));
914
+ return;
915
+ }
916
+ const pair = keyValue(body2);
917
+ if (pair === void 0) return;
918
+ const parentTree = top.key === void 0 ? top.tree : ensureTree(top.tree, top.key);
919
+ if (pair.value === "") {
920
+ stack.push({ indent, tree: parentTree, key: pair.key });
921
+ return;
922
+ }
923
+ parentTree[pair.key] = flowList(pair.value) ?? unquote(pair.value);
924
+ };
925
+ var ensureTree = (tree, key) => {
926
+ const existing = tree[key];
927
+ if (existing !== void 0 && typeof existing !== "string" && !Array.isArray(existing)) return existing;
928
+ const fresh3 = {};
929
+ tree[key] = fresh3;
930
+ return fresh3;
931
+ };
932
+ var parseConfigYaml = (text6) => {
933
+ const root = {};
934
+ const stack = [{ indent: -1, tree: root, key: void 0 }];
935
+ for (const raw of text6.split(/\r?\n/)) {
936
+ const line2 = stripComment(raw).trimEnd();
937
+ if (line2.trim() === "" || line2.trim() === "---") continue;
938
+ const indent = line2.length - line2.trimStart().length;
939
+ placeLine(stack, indent, line2.trim());
940
+ }
941
+ return root;
942
+ };
943
+ var isTree = (node) => typeof node === "object" && !Array.isArray(node);
944
+ var configAt = (tree, ...path) => path.reduce((node, key) => isTree(node) ? node[key] : void 0, tree);
945
+ var configList = (tree, ...path) => {
946
+ const node = configAt(tree, ...path);
947
+ if (typeof node === "string") return node === "" ? [] : [node];
948
+ return Array.isArray(node) ? node.filter((item) => typeof item === "string") : [];
949
+ };
950
+ var configMap = (tree, ...path) => {
951
+ const node = configAt(tree, ...path);
952
+ if (!isTree(node)) return {};
953
+ return Object.fromEntries(
954
+ Object.entries(node).filter((entry2) => typeof entry2[1] === "string")
955
+ );
956
+ };
957
+ var loadProjectConfig = (runtime, root) => {
958
+ const text6 = runtime.readText(toAbsolutePath(join2(root, CONFIG_FILE2)));
959
+ return text6.ok ? parseConfigYaml(text6.value) : {};
960
+ };
961
+
962
+ // src/policy/policy.ts
963
+ var POLICY_FILE = ".finitemonkey/policy.yml";
964
+ var GUARD_MODES = ["off", "shadow", "active"];
965
+ var LEVELS = ["light", "standard", "strict"];
966
+ var SEVERITIES = ["low", "medium", "high"];
967
+ var DEFAULT_PROMOTE_AFTER = 20;
968
+ var OPTIONAL_GUARDS = ["prose"];
969
+ var DEFAULT_POLICY = {
970
+ exists: false,
971
+ level: "standard",
972
+ severity: "medium",
973
+ promoteAfter: DEFAULT_PROMOTE_AFTER,
974
+ guards: {}
975
+ };
976
+ var pick = (options, value, fallback) => options.find((option) => option === value) ?? fallback;
977
+ var parsePolicy = (text6) => {
978
+ const tree = parseConfigYaml(text6);
979
+ const raw = configAt(tree, "promoteAfter");
980
+ const promoteAfter = typeof raw === "string" ? Number.parseInt(raw, 10) : Number.NaN;
981
+ const guards = Object.fromEntries(
982
+ Object.entries(configMap(tree, "guards")).flatMap(([id, mode]) => {
983
+ const known2 = GUARD_MODES.find((option) => option === mode);
984
+ return known2 === void 0 ? [] : [[id, known2]];
985
+ })
986
+ );
987
+ return {
988
+ exists: true,
989
+ level: pick(LEVELS, configAt(tree, "level"), DEFAULT_POLICY.level),
990
+ severity: pick(SEVERITIES, configAt(tree, "severity"), DEFAULT_POLICY.severity),
991
+ promoteAfter: Number.isInteger(promoteAfter) && promoteAfter > 0 ? promoteAfter : DEFAULT_PROMOTE_AFTER,
992
+ guards
993
+ };
994
+ };
995
+ var policyPath = (root) => toAbsolutePath(join3(root, POLICY_FILE));
996
+ var loadPolicy = (runtime, root) => {
997
+ const text6 = runtime.readText(policyPath(root));
998
+ return text6.ok ? parsePolicy(text6.value) : DEFAULT_POLICY;
999
+ };
1000
+
568
1001
  // src/install/managed-block.ts
569
1002
  var lineBreak = (text6) => text6.includes("\r\n") ? "\r\n" : "\n";
570
1003
  var blockRange = (lines3, markers) => {
@@ -594,6 +1027,11 @@ var withoutManagedBlock = (text6, markers) => {
594
1027
  // src/install/templates/en.ts
595
1028
  var en = {
596
1029
  configHeader: "Finite Monkey project settings. Commit this file. `finitemonkey update` keeps your edits.",
1030
+ policyHeader: [
1031
+ "Guard policy for this project. Modes: off, shadow (record what it would block), active.",
1032
+ "The guards for secrets, production, lost git work and false done stay active whatever this file says.",
1033
+ "A guard missing from this list starts in shadow and is promoted after promoteAfter firings without a false positive."
1034
+ ],
597
1035
  agentsBlock: ({ gate, doctor }) => [
598
1036
  "## Finite Monkey",
599
1037
  "",
@@ -616,6 +1054,11 @@ var en = {
616
1054
  // src/install/templates/es.ts
617
1055
  var es = {
618
1056
  configHeader: "Configuración de Finite Monkey en este proyecto. Versiona este archivo. `finitemonkey update` conserva tus cambios.",
1057
+ policyHeader: [
1058
+ "Política de los guards de este proyecto. Modos: off, shadow (registra lo que bloquearía), active.",
1059
+ "Los guards de secretos, producción, trabajo de git perdido y falso terminado siguen activos diga lo que diga este archivo.",
1060
+ "Un guard que no está en esta lista empieza en shadow y se promueve tras promoteAfter disparos sin falso positivo."
1061
+ ],
619
1062
  agentsBlock: ({ gate, doctor }) => [
620
1063
  "## Finite Monkey",
621
1064
  "",
@@ -638,6 +1081,11 @@ var es = {
638
1081
  // src/install/templates/pt-BR.ts
639
1082
  var ptBR = {
640
1083
  configHeader: "Configuração do Finite Monkey neste projeto. Versione este arquivo. O `finitemonkey update` mantém suas edições.",
1084
+ policyHeader: [
1085
+ "Política dos guards deste projeto. Modos: off, shadow (registra o que bloquearia), active.",
1086
+ "Os guards de segredos, produção, trabalho de git perdido e falso pronto ficam ativos, diga o que disser este arquivo.",
1087
+ "Um guard que não está nesta lista começa em shadow e é promovido depois de promoteAfter disparos sem falso positivo."
1088
+ ],
641
1089
  agentsBlock: ({ gate, doctor }) => [
642
1090
  "## Finite Monkey",
643
1091
  "",
@@ -694,8 +1142,8 @@ var execForm = (runner, event) => {
694
1142
  };
695
1143
  var hookHandler = (runner, spec2, failClosedSupported) => {
696
1144
  const failure4 = spec2.guard && failClosedSupported ? { onFailure: "block" } : {};
697
- const { command, args } = execForm(runner, spec2.event);
698
- return { type: "command", command, args, timeout: spec2.timeout, ...failure4 };
1145
+ const { command, args: args2 } = execForm(runner, spec2.event);
1146
+ return { type: "command", command, args: args2, timeout: spec2.timeout, ...failure4 };
699
1147
  };
700
1148
  var OWN_SHELL = /(^|[\s/"'])finitemonkey(@[^\s"]+|\.mjs|\.cmd|\.exe)?"?\s+hook\s/;
701
1149
  var OWN_SCRIPT = /finitemonkey(\.mjs|\.cmd|\.exe)?$/;
@@ -704,19 +1152,19 @@ var isOwnHandler = (handler) => {
704
1152
  if (!isRecord(handler)) return false;
705
1153
  const command = handler["command"];
706
1154
  if (typeof command !== "string") return false;
707
- const args = stringsOf(handler["args"]);
708
- if (args.length === 0) return OWN_SHELL.test(`${command} `);
709
- const hookAt = args.indexOf("hook");
1155
+ const args2 = stringsOf(handler["args"]);
1156
+ if (args2.length === 0) return OWN_SHELL.test(`${command} `);
1157
+ const hookAt = args2.indexOf("hook");
710
1158
  if (hookAt < 0) return false;
711
- const target = hookAt === 0 ? command : args[hookAt - 1] ?? "";
1159
+ const target = hookAt === 0 ? command : args2[hookAt - 1] ?? "";
712
1160
  return OWN_SCRIPT.test(target);
713
1161
  };
714
1162
 
715
1163
  // src/install/skill-template.ts
716
- var cliCommand = (runner, args) => {
717
- if (runner.kind === "local") return `node ${LOCAL_BIN} ${args}`;
718
- if (runner.kind === "vendored") return `node ${VENDOR_BIN} ${args}`;
719
- return isScript(runner.path) ? `node "${runner.path}" ${args}` : `"${runner.path}" ${args}`;
1164
+ var cliCommand = (runner, args2) => {
1165
+ if (runner.kind === "local") return `node ${LOCAL_BIN} ${args2}`;
1166
+ if (runner.kind === "vendored") return `node ${VENDOR_BIN} ${args2}`;
1167
+ return isScript(runner.path) ? `node "${runner.path}" ${args2}` : `"${runner.path}" ${args2}`;
720
1168
  };
721
1169
  var verifySkillTemplate = (runner, lang) => {
722
1170
  const skill = TEMPLATES[lang].verifySkill;
@@ -856,7 +1304,7 @@ var DENY_FLOOR = {
856
1304
  var DENY_RULES = [...DENY_FLOOR.secrets, ...DENY_FLOOR.git, ...DENY_FLOOR.proof];
857
1305
 
858
1306
  // src/install/detect.ts
859
- import { join as join2 } from "node:path";
1307
+ import { join as join4 } from "node:path";
860
1308
 
861
1309
  // src/install/json-text.ts
862
1310
  var DEFAULT_STYLE = { indent: " ", finalNewline: true };
@@ -868,11 +1316,11 @@ var renderJson = (value, style) => `${JSON.stringify(value, null, style.indent)}
868
1316
  var parseSettings = (text6) => text6 === void 0 || text6.trim() === "" ? { ok: true, value: {} } : parseJsonObject(text6);
869
1317
  var asList = (value) => Array.isArray(value) ? value : [];
870
1318
  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 };
1319
+ var setKey = (record3, key, value) => {
1320
+ if (!(key in record3)) return value === void 0 ? record3 : { ...record3, [key]: value };
873
1321
  return Object.fromEntries(
874
- Object.entries(record2).flatMap(([name, current]) => {
875
- if (name !== key) return [[name, current]];
1322
+ Object.entries(record3).flatMap(([name, current2]) => {
1323
+ if (name !== key) return [[name, current2]];
876
1324
  return value === void 0 ? [] : [[name, value]];
877
1325
  })
878
1326
  );
@@ -949,7 +1397,7 @@ var scopeAuthorities = (text6, source) => {
949
1397
  var userSettingsText = (project, env) => {
950
1398
  const home = env["CLAUDE_CONFIG_DIR"] ?? env["HOME"] ?? env["USERPROFILE"];
951
1399
  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");
1400
+ const file = env["CLAUDE_CONFIG_DIR"] === void 0 ? join4(home, ".claude", "settings.json") : join4(home, "settings.json");
953
1401
  const text6 = project.runtime.readText(toAbsolutePath(file));
954
1402
  return text6.ok ? text6.value : void 0;
955
1403
  };
@@ -1041,6 +1489,18 @@ var applyMigrations = (items, previous, version, migrations) => {
1041
1489
  // src/install/migrations/index.ts
1042
1490
  var MIGRATIONS = [];
1043
1491
 
1492
+ // src/install/policy-template.ts
1493
+ var policyTemplate = (lang) => [
1494
+ `# ${GENERATED_MARKER}`,
1495
+ ...TEMPLATES[lang].policyHeader.map((line2) => `# ${line2}`),
1496
+ "level: standard",
1497
+ "severity: medium",
1498
+ `promoteAfter: ${String(DEFAULT_PROMOTE_AFTER)}`,
1499
+ "guards:",
1500
+ ...OPTIONAL_GUARDS.map((id) => ` ${id}: active`),
1501
+ ""
1502
+ ].join("\n");
1503
+
1044
1504
  // src/install/settings-merge.ts
1045
1505
  var SETTINGS_SCHEMA_URL = "https://json.schemastore.org/claude-code-settings.json";
1046
1506
  var ATTRIBUTION_OFF = { commit: "", pr: "", sessionUrl: false };
@@ -1095,15 +1555,15 @@ var withPermissions = (settings, lists) => {
1095
1555
  var missing = (wanted, existing) => wanted.filter((rule) => !existing.includes(rule));
1096
1556
  var withoutAdded = (existing, added) => existing.filter((rule) => typeof rule !== "string" || !added.includes(rule));
1097
1557
  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;
1558
+ var mergeSettings = (current2, wanted, recorded) => {
1559
+ const schema = Object.keys(current2).length === 0;
1560
+ const base = schema ? { $schema: SETTINGS_SCHEMA_URL } : current2;
1101
1561
  const hooks = mergeHooks(asRecord(base["hooks"]), ownHookMatcher(recorded), wanted.hooks);
1102
1562
  const permissions = asRecord(base["permissions"]);
1103
1563
  const existingDeny = asList(permissions["deny"]);
1104
1564
  const existingAsk = asList(permissions["ask"]);
1105
1565
  const deny2 = missing(wanted.deny, existingDeny);
1106
- const ask = missing(wanted.ask, existingAsk);
1566
+ const ask2 = missing(wanted.ask, existingAsk);
1107
1567
  const blockReads = !(BLOCK_READS in permissions);
1108
1568
  const attribution = wanted.attributionOff && !("attribution" in base);
1109
1569
  const marketplace = wanted.marketplace && !hasEntry(base, "extraKnownMarketplaces", MARKETPLACE_KEY);
@@ -1111,21 +1571,21 @@ var mergeSettings = (current, wanted, recorded) => {
1111
1571
  const language = "language" in base ? void 0 : wanted.language;
1112
1572
  let settings = withPermissions(withHooks(base, hooks), {
1113
1573
  deny: [...existingDeny, ...deny2],
1114
- ask: [...existingAsk, ...ask],
1574
+ ask: [...existingAsk, ...ask2],
1115
1575
  blockReads: blockReads ? true : permissions[BLOCK_READS]
1116
1576
  });
1117
1577
  if (attribution) settings = { ...settings, attribution: ATTRIBUTION_OFF };
1118
1578
  if (marketplace) settings = withEntry(settings, "extraKnownMarketplaces", MARKETPLACE_KEY, MARKETPLACE_ENTRY);
1119
1579
  if (plugin) settings = withEntry(settings, "enabledPlugins", PLUGIN_ID, true);
1120
1580
  if (language !== void 0) settings = { ...settings, language };
1121
- const added = { schema, deny: deny2, ask, blockReads, attribution, marketplace, plugin };
1581
+ const added = { schema, deny: deny2, ask: ask2, blockReads, attribution, marketplace, plugin };
1122
1582
  return { settings, added: language === void 0 ? added : { ...added, language } };
1123
1583
  };
1124
1584
  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), {
1585
+ var unmergeSettings = (current2, added, recorded) => {
1586
+ const hooks = mergeHooks(asRecord(current2["hooks"]), ownHookMatcher(recorded), {});
1587
+ const permissions = asRecord(current2["permissions"]);
1588
+ let settings = withPermissions(withHooks(current2, hooks), {
1129
1589
  deny: withoutAdded(asList(permissions["deny"]), added.deny),
1130
1590
  ask: withoutAdded(asList(permissions["ask"]), added.ask),
1131
1591
  blockReads: added.blockReads && permissions[BLOCK_READS] === true ? void 0 : permissions[BLOCK_READS]
@@ -1202,8 +1662,8 @@ var wantedHooks = (detection) => Object.fromEntries(
1202
1662
  );
1203
1663
  var settingsItem = (project, detection, input, previous) => {
1204
1664
  const before = project.read(SETTINGS_FILE);
1205
- const current = parseSettings(before);
1206
- if (!current.ok) return reject({ kind: "settings", detail: current.error });
1665
+ const current2 = parseSettings(before);
1666
+ if (!current2.ok) return reject({ kind: "settings", detail: current2.error });
1207
1667
  const hooks = wantedHooks(detection);
1208
1668
  const wanted = {
1209
1669
  hooks,
@@ -1213,7 +1673,7 @@ var settingsItem = (project, detection, input, previous) => {
1213
1673
  marketplace: input.marketplace,
1214
1674
  language: input.claudeLanguage
1215
1675
  };
1216
- const merged = mergeSettings(current.value, wanted, previous?.handlers ?? []);
1676
+ const merged = mergeSettings(current2.value, wanted, previous?.handlers ?? []);
1217
1677
  const after = renderJson(merged.settings, before === void 0 ? DEFAULT_STYLE : styleOf(before));
1218
1678
  return accept({
1219
1679
  path: SETTINGS_FILE,
@@ -1254,7 +1714,8 @@ var generatedItem = (project, file, previous) => {
1254
1714
  var generatedFiles = (input, runner, vendor) => {
1255
1715
  const own = [
1256
1716
  { path: CONFIG_FILE, template: configTemplate(input.preset, input.lang), isOurs: hasMarker },
1257
- { path: VERIFY_SKILL, template: verifySkillTemplate(runner, input.lang), isOurs: isOwnSkill }
1717
+ { path: VERIFY_SKILL, template: verifySkillTemplate(runner, input.lang), isOurs: isOwnSkill },
1718
+ { path: POLICY_FILE, template: policyTemplate(input.lang), isOurs: hasMarker }
1258
1719
  ];
1259
1720
  if (!vendor) return accept(own);
1260
1721
  if (input.bundle === void 0) return reject({ kind: "bundle", detail: "the running finitemonkey is not a bundle" });
@@ -1428,9 +1889,9 @@ var detectLang = (flag2, env, settings = {}) => {
1428
1889
  };
1429
1890
 
1430
1891
  // src/install/project.ts
1431
- import { dirname as dirname4, join as join3 } from "node:path";
1892
+ import { dirname as dirname4, join as join5 } from "node:path";
1432
1893
  var projectAt = (root, runtime) => {
1433
- const abs = (relative9) => toAbsolutePath(join3(root, ...relative9.split("/")));
1894
+ const abs = (relative9) => toAbsolutePath(join5(root, ...relative9.split("/")));
1434
1895
  return {
1435
1896
  root,
1436
1897
  runtime,
@@ -1444,12 +1905,12 @@ var projectAt = (root, runtime) => {
1444
1905
  };
1445
1906
  };
1446
1907
  var findGitRoot = (start, runtime) => {
1447
- let current = start;
1908
+ let current2 = start;
1448
1909
  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;
1910
+ if (runtime.stat(toAbsolutePath(join5(current2, ".git"))) !== void 0) return toAbsolutePath(current2);
1911
+ const parent = dirname4(current2);
1912
+ if (parent === current2) return void 0;
1913
+ current2 = parent;
1453
1914
  }
1454
1915
  };
1455
1916
 
@@ -1547,7 +2008,7 @@ var applyFixes = (ctx, write3) => {
1547
2008
  };
1548
2009
 
1549
2010
  // src/doctor/report-file.ts
1550
- import { join as join5 } from "node:path";
2011
+ import { join as join7 } from "node:path";
1551
2012
 
1552
2013
  // src/evidence/log.ts
1553
2014
  var logEvent = (runtime, paths, event, data) => runtime.appendText(paths.log, `${JSON.stringify({ at: runtime.now(), event, ...data })}
@@ -2014,11 +2475,11 @@ var maskDeep = (value) => {
2014
2475
  };
2015
2476
 
2016
2477
  // src/task/paths.ts
2017
- import { join as join4 } from "node:path";
2478
+ import { join as join6 } from "node:path";
2018
2479
  var FM_DIR = ".finitemonkey";
2019
2480
  var fmPaths = (root) => {
2020
- const dir = toAbsolutePath(join4(root, FM_DIR));
2021
- const at = (...parts) => toAbsolutePath(join4(dir, ...parts));
2481
+ const dir = toAbsolutePath(join6(root, FM_DIR));
2482
+ const at = (...parts) => toAbsolutePath(join6(dir, ...parts));
2022
2483
  return {
2023
2484
  root,
2024
2485
  dir,
@@ -2036,8 +2497,8 @@ var fmPaths = (root) => {
2036
2497
  var HOOK_FAILURE_EVENT = "hook.failed";
2037
2498
  var FAILURES = ["failed_open", "failed_closed", "stop_released"];
2038
2499
  var MAX_DETAIL = 500;
2039
- var logHookFailure = (ctx, event, outcome) => {
2040
- if (!FAILURES.includes(outcome.verdict)) return;
2500
+ var logHookFailure = (ctx, event, outcome2) => {
2501
+ if (!FAILURES.includes(outcome2.verdict)) return;
2041
2502
  const fromEnv = ctx.env["CLAUDE_PROJECT_DIR"];
2042
2503
  const root = fromEnv === void 0 ? findGitRoot(ctx.cwd, ctx.runtime) : toAbsolutePath(fromEnv);
2043
2504
  if (root === void 0) return;
@@ -2045,8 +2506,8 @@ var logHookFailure = (ctx, event, outcome) => {
2045
2506
  if (ctx.runtime.stat(paths.dir)?.kind !== "dir") return;
2046
2507
  logEvent(ctx.runtime, paths, HOOK_FAILURE_EVENT, {
2047
2508
  hook: event,
2048
- verdict: outcome.verdict,
2049
- detail: maskText(outcome.stderr.trim()).slice(0, MAX_DETAIL)
2509
+ verdict: outcome2.verdict,
2510
+ detail: maskText(outcome2.stderr.trim()).slice(0, MAX_DETAIL)
2050
2511
  });
2051
2512
  };
2052
2513
 
@@ -2254,16 +2715,16 @@ var reportMarkdown = (env, ctx, envelope) => {
2254
2715
  ];
2255
2716
  return maskText(withoutHome(lines3.join("\n"), env.runtime.homeDir()));
2256
2717
  };
2257
- var reportPath = (env) => toAbsolutePath(join5(env.root, ...REPORTS_DIR.split("/"), `doctor-${stamp(env.runtime.now())}.md`));
2718
+ var reportPath = (env) => toAbsolutePath(join7(env.root, ...REPORTS_DIR.split("/"), `doctor-${stamp(env.runtime.now())}.md`));
2258
2719
 
2259
2720
  // src/install/plugin-yield.ts
2260
- import { join as join6 } from "node:path";
2721
+ import { join as join8 } from "node:path";
2261
2722
  var PLUGIN_VIA = "plugin";
2262
2723
  var YIELD_STDOUT = `${JSON.stringify({ continue: true })}
2263
2724
  `;
2264
2725
  var projectOwnsHooks = (ctx) => {
2265
2726
  const root = ctx.env["CLAUDE_PROJECT_DIR"] ?? ctx.cwd;
2266
- const text6 = ctx.runtime.readText(toAbsolutePath(join6(root, ...SETTINGS_FILE.split("/"))));
2727
+ const text6 = ctx.runtime.readText(toAbsolutePath(join8(root, ...SETTINGS_FILE.split("/"))));
2267
2728
  if (!text6.ok) return false;
2268
2729
  const parsed = parseJsonObject(text6.value);
2269
2730
  return parsed.ok && ownHandlersIn(parsed.value) > 0;
@@ -2277,7 +2738,7 @@ var STABLE_PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
2277
2738
  var NEXT_PLUGIN_ID = `${PLUGIN_NAME}@${NEXT_MARKETPLACE_NAME}`;
2278
2739
 
2279
2740
  // src/doctor/settings.ts
2280
- import { join as join7 } from "node:path";
2741
+ import { join as join9 } from "node:path";
2281
2742
  var MANAGED = {
2282
2743
  darwin: "/Library/Application Support/ClaudeCode/managed-settings.json",
2283
2744
  linux: "/etc/claude-code/managed-settings.json",
@@ -2286,7 +2747,7 @@ var MANAGED = {
2286
2747
  var userSettingsPath = ({ env, runtime }) => {
2287
2748
  const configDir = env["CLAUDE_CONFIG_DIR"];
2288
2749
  return toAbsolutePath(
2289
- configDir === void 0 ? join7(runtime.homeDir(), ".claude", "settings.json") : join7(configDir, "settings.json")
2750
+ configDir === void 0 ? join9(runtime.homeDir(), ".claude", "settings.json") : join9(configDir, "settings.json")
2290
2751
  );
2291
2752
  };
2292
2753
  var settingsPaths = (env) => {
@@ -2294,8 +2755,8 @@ var settingsPaths = (env) => {
2294
2755
  return [
2295
2756
  ...managed === void 0 ? [] : [{ scope: "managed", path: toAbsolutePath(managed) }],
2296
2757
  { 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")) }
2758
+ { scope: "project", path: toAbsolutePath(join9(env.root, ".claude", "settings.json")) },
2759
+ { scope: "local", path: toAbsolutePath(join9(env.root, ".claude", "settings.local.json")) }
2299
2760
  ];
2300
2761
  };
2301
2762
  var readSettings = (env) => settingsPaths(env).flatMap(({ scope: scope2, path }) => {
@@ -2439,7 +2900,7 @@ var checkClaudeSettings = (env) => {
2439
2900
  };
2440
2901
 
2441
2902
  // src/doctor/environment.ts
2442
- import { join as join8 } from "node:path";
2903
+ import { join as join10 } from "node:path";
2443
2904
  var AUTH_VARIABLES = [
2444
2905
  "ANTHROPIC_API_KEY",
2445
2906
  "ANTHROPIC_AUTH_TOKEN",
@@ -2459,7 +2920,7 @@ var checkSandbox = (env) => {
2459
2920
  }
2460
2921
  if (os === "macos") return [finding("sandbox", "ok", "doctor.sandbox.ok")];
2461
2922
  if (os === "other") return [];
2462
- const missing2 = SANDBOX_TOOLS.filter(({ command, args }) => !env.runtime.probe(command, args, PROBE_MS2).found).map(
2923
+ const missing2 = SANDBOX_TOOLS.filter(({ command, args: args2 }) => !env.runtime.probe(command, args2, PROBE_MS2).found).map(
2463
2924
  ({ command }) => command
2464
2925
  );
2465
2926
  if (missing2.length === 0) return [finding("sandbox", "ok", "doctor.sandbox.ok")];
@@ -2477,7 +2938,7 @@ var pathDirs = ({ env, platform }) => {
2477
2938
  var copiesOnPath = (env) => {
2478
2939
  const names = EXECUTABLES[env.platform] ?? ["finitemonkey"];
2479
2940
  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))
2941
+ (dir) => names.map((name) => toAbsolutePath(join10(dir, name))).filter((path) => env.runtime.stat(path)?.kind === "file").map((path) => env.runtime.realpath(path))
2481
2942
  );
2482
2943
  const byDir = new Map(found.map((path) => [path.replace(/\.(cmd|exe|ps1)$/i, ""), path]));
2483
2944
  return [...byDir.values()];
@@ -2511,7 +2972,7 @@ var checkAuthVariables = ({ env, platform }) => {
2511
2972
  };
2512
2973
 
2513
2974
  // src/doctor/hooks.ts
2514
- import { join as join29 } from "node:path";
2975
+ import { join as join31 } from "node:path";
2515
2976
 
2516
2977
  // src/hooks/events.ts
2517
2978
  var HOOK_EVENTS = [
@@ -2558,9 +3019,9 @@ var deadlineFor = (event, raw) => {
2558
3019
  };
2559
3020
 
2560
3021
  // src/hooks/fields.ts
2561
- var textField = (record2, ...names) => {
3022
+ var textField = (record3, ...names) => {
2562
3023
  for (const name of names) {
2563
- const value = record2[name];
3024
+ const value = record3[name];
2564
3025
  if (typeof value === "string") return value;
2565
3026
  }
2566
3027
  return void 0;
@@ -2655,8 +3116,8 @@ var closeTask = (state, id) => assemble({
2655
3116
 
2656
3117
  // src/guards/release/grants.ts
2657
3118
  var GRANT_MINUTES = 30;
2658
- var text2 = (record2, key) => {
2659
- const value = record2[key];
3119
+ var text2 = (record3, key) => {
3120
+ const value = record3[key];
2660
3121
  return typeof value === "string" ? value : void 0;
2661
3122
  };
2662
3123
  var toGrant = (value) => {
@@ -3180,10 +3641,10 @@ var withAssignments = (stage) => {
3180
3641
  }
3181
3642
  return { ...stage, argv: stage.argv.slice(index), env };
3182
3643
  };
3183
- var skipOptions = (args, wrapper) => {
3644
+ var skipOptions = (args2, wrapper) => {
3184
3645
  let index = 0;
3185
- while (index < args.length) {
3186
- const arg = args[index] ?? "";
3646
+ while (index < args2.length) {
3647
+ const arg = args2[index] ?? "";
3187
3648
  if (arg === "--") return index + 1 + wrapper.positionals;
3188
3649
  if (!arg.startsWith("-") || arg === "-") break;
3189
3650
  if (wrapper.queries.has(arg)) return void 0;
@@ -3213,33 +3674,33 @@ var envCommand = (stage) => {
3213
3674
  return printsEnvironment ? { kind: "command", stage, extra: [] } : shift(stage, index, "env");
3214
3675
  };
3215
3676
  var shellScript = (stage, name, stdin) => {
3216
- const args = stage.argv.slice(1);
3677
+ const args2 = stage.argv.slice(1);
3217
3678
  let index = 0;
3218
3679
  let command = false;
3219
- while (index < args.length && /^[-+]/.test(args[index] ?? "")) {
3220
- const arg = args[index] ?? "";
3680
+ while (index < args2.length && /^[-+]/.test(args2[index] ?? "")) {
3681
+ const arg = args2[index] ?? "";
3221
3682
  if (/^-[A-Za-z]*c[A-Za-z]*$/.test(arg)) command = true;
3222
3683
  index += SHELL_WITH_VALUE.has(arg) ? 2 : 1;
3223
3684
  }
3224
- const script = args[index];
3685
+ const script = args2[index];
3225
3686
  if (command && script !== void 0) return { kind: "script", script, dialect: "bash", stage: shift(stage, 0, name) };
3226
3687
  if (script === void 0 && stdin !== void 0) return { kind: "script", script: stdin, dialect: "bash", stage };
3227
3688
  return void 0;
3228
3689
  };
3229
3690
  var decodeEncoded = (value) => Buffer.from(value, "base64").toString("utf16le");
3230
3691
  var powershellScript = (stage, name) => {
3231
- const args = stage.argv.slice(1);
3232
- const at = args.findIndex((arg) => /^[-/](?:c|co|com|comm|comma|comman|command)$/i.test(arg));
3233
- const encoded = args.findIndex((arg) => /^[-/](?:e|ec|enc|encodedcommand)$/i.test(arg));
3692
+ const args2 = stage.argv.slice(1);
3693
+ const at = args2.findIndex((arg) => /^[-/](?:c|co|com|comm|comma|comman|command)$/i.test(arg));
3694
+ const encoded = args2.findIndex((arg) => /^[-/](?:e|ec|enc|encodedcommand)$/i.test(arg));
3234
3695
  if (at >= 0) {
3235
3696
  return {
3236
3697
  kind: "script",
3237
- script: args.slice(at + 1).join(" "),
3698
+ script: args2.slice(at + 1).join(" "),
3238
3699
  dialect: "powershell",
3239
3700
  stage: shift(stage, 0, name)
3240
3701
  };
3241
3702
  }
3242
- const value = encoded >= 0 ? args[encoded + 1] : void 0;
3703
+ const value = encoded >= 0 ? args2[encoded + 1] : void 0;
3243
3704
  if (value !== void 0) {
3244
3705
  return { kind: "script", script: decodeEncoded(value), dialect: "powershell", stage: shift(stage, 0, name) };
3245
3706
  }
@@ -3298,10 +3759,10 @@ var PS_ENV = /^\$env:([A-Za-z_][A-Za-z0-9_]*)=?(.*)$/is;
3298
3759
  var CHANGES_DIR = /* @__PURE__ */ new Set(["cd", "pushd", "set-location", "push-location"]);
3299
3760
  var lex = (text6, language) => language === "bash" ? lexBash(text6) : lexPowerShell(text6, language === "cmd");
3300
3761
  var isWindowsAbsolute = (path) => /^[A-Za-z]:\//.test(path);
3301
- var joinDir = (current, target) => {
3762
+ var joinDir = (current2, target) => {
3302
3763
  const next2 = toPosixPath(target);
3303
3764
  if (next2.startsWith("/") || isWindowsAbsolute(next2) || next2.startsWith("~")) return next2;
3304
- return current === void 0 ? posix.normalize(next2) : posix.join(current, next2);
3765
+ return current2 === void 0 ? posix.normalize(next2) : posix.join(current2, next2);
3305
3766
  };
3306
3767
  var assignments = (words, language) => {
3307
3768
  if (language !== "bash") {
@@ -3414,24 +3875,24 @@ var COMMIT_VALUE_SHORTS = /* @__PURE__ */ new Set(["m", "F", "C", "c", "t"]);
3414
3875
  var VERIFY_SUBCOMMANDS = /* @__PURE__ */ new Set(["commit", "push", "merge", "am", "rebase", "cherry-pick", "revert", "pull"]);
3415
3876
  var isHooksPath = (value) => /^core\.hookspath(=|$)/i.test(value);
3416
3877
  var envFindings = (command) => {
3417
- const pairs = Object.entries(command.env);
3878
+ const pairs2 = Object.entries(command.env);
3418
3879
  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}` }));
3880
+ return [...pairs2, ...exported].filter(([name = ""]) => HOOK_ENV.test(name)).map(([name = "", value = ""]) => ({ kind: "hook_env", detail: `${name}=${value}` }));
3420
3881
  };
3421
- var splitGit = (args) => {
3882
+ var splitGit = (args2) => {
3422
3883
  let index = 0;
3423
3884
  const globals = [];
3424
- while (index < args.length && (args[index] ?? "").startsWith("-")) {
3425
- const arg = args[index] ?? "";
3885
+ while (index < args2.length && (args2[index] ?? "").startsWith("-")) {
3886
+ const arg = args2[index] ?? "";
3426
3887
  globals.push(arg);
3427
- if (GIT_VALUE_OPTIONS.has(arg)) globals.push(args[index + 1] ?? "");
3888
+ if (GIT_VALUE_OPTIONS.has(arg)) globals.push(args2[index + 1] ?? "");
3428
3889
  index += GIT_VALUE_OPTIONS.has(arg) ? 2 : 1;
3429
3890
  }
3430
- return { globals, rest: args.slice(index) };
3891
+ return { globals, rest: args2.slice(index) };
3431
3892
  };
3432
- var commitHasShortN = (args) => {
3433
- for (let index = 0; index < args.length; index += 1) {
3434
- const arg = args[index] ?? "";
3893
+ var commitHasShortN = (args2) => {
3894
+ for (let index = 0; index < args2.length; index += 1) {
3895
+ const arg = args2[index] ?? "";
3435
3896
  if (arg === "--") return false;
3436
3897
  if (!/^-[A-Za-z]/.test(arg)) continue;
3437
3898
  for (const char of arg.slice(1)) {
@@ -3444,8 +3905,8 @@ var commitHasShortN = (args) => {
3444
3905
  }
3445
3906
  return false;
3446
3907
  };
3447
- var gitFindings = (args) => {
3448
- const { globals, rest } = splitGit(args);
3908
+ var gitFindings = (args2) => {
3909
+ const { globals, rest } = splitGit(args2);
3449
3910
  const found = [];
3450
3911
  globals.forEach((arg, index) => {
3451
3912
  if (arg === "-c" && isHooksPath(globals[index + 1] ?? "") || /^--config-env=core\.hookspath/i.test(arg)) {
@@ -3522,16 +3983,16 @@ var GIT_WRITERS = /* @__PURE__ */ new Set(["checkout", "restore", "rm", "mv", "c
3522
3983
  var FM_NAMES2 = /* @__PURE__ */ new Set(["finitemonkey", "fm"]);
3523
3984
  var inline = (argv) => argv.some((arg) => arg === "-e" || arg === "-c" || arg === "--eval");
3524
3985
  var writtenPaths = (command) => {
3525
- const [name = "", ...args] = command.argv;
3526
- const operands2 = args.filter((arg) => !arg.startsWith("-"));
3527
- const ddTargets = args.filter((arg) => arg.startsWith("of=")).map((arg) => arg.slice(3));
3986
+ const [name = "", ...args2] = command.argv;
3987
+ const operands2 = args2.filter((arg) => !arg.startsWith("-"));
3988
+ const ddTargets = args2.filter((arg) => arg.startsWith("of=")).map((arg) => arg.slice(3));
3528
3989
  if (FM_NAMES2.has(name)) return command.redirects;
3529
3990
  if (COPIERS.has(name)) return [...command.redirects, ...operands2.slice(-1)];
3530
3991
  if (WRITERS.has(name)) return [...command.redirects, ...operands2, ...ddTargets];
3531
- if (IN_PLACE.has(name) && args.some((arg) => /^-[a-zA-Z]*i/.test(arg) || arg.startsWith("--in-place"))) {
3992
+ if (IN_PLACE.has(name) && args2.some((arg) => /^-[a-zA-Z]*i/.test(arg) || arg.startsWith("--in-place"))) {
3532
3993
  return [...command.redirects, ...operands2];
3533
3994
  }
3534
- if (INTERPRETERS.has(name) && inline(args)) return [...command.redirects, ...args];
3995
+ if (INTERPRETERS.has(name) && inline(args2)) return [...command.redirects, ...args2];
3535
3996
  if (name === "git" && GIT_WRITERS.has(operands2[0] ?? "")) return [...command.redirects, ...operands2.slice(1)];
3536
3997
  return command.redirects;
3537
3998
  };
@@ -3550,14 +4011,14 @@ var bashSelfTargets = (root, commandLine) => parseShell(commandLine).flatMap(
3550
4011
  // src/guards/self/hook.ts
3551
4012
  var targets = (input) => {
3552
4013
  const tool = toolName(input.payload.raw);
3553
- const args = toolInput(input.payload.raw);
4014
+ const args2 = toolInput(input.payload.raw);
3554
4015
  const root = guardRoot(input);
3555
4016
  if (FILE_TOOLS.has(tool)) {
3556
- const path = textField(args, "file_path", "notebook_path") ?? "";
4017
+ const path = textField(args2, "file_path", "notebook_path") ?? "";
3557
4018
  const kind = path === "" ? void 0 : selfTarget(root, path);
3558
4019
  return kind === void 0 ? [] : [{ kind, path }];
3559
4020
  }
3560
- return SHELL_TOOLS.has(tool) ? bashSelfTargets(root, textField(args, "command") ?? "") : [];
4021
+ return SHELL_TOOLS.has(tool) ? bashSelfTargets(root, textField(args2, "command") ?? "") : [];
3561
4022
  };
3562
4023
  var selfGuard = (input) => {
3563
4024
  const found = targets(input);
@@ -3574,7 +4035,7 @@ var selfGuard = (input) => {
3574
4035
  };
3575
4036
 
3576
4037
  // src/guards/tests/hook.ts
3577
- import { join as join9, relative as relative3 } from "node:path";
4038
+ import { join as join11, relative as relative3 } from "node:path";
3578
4039
 
3579
4040
  // src/guards/tests/patterns.ts
3580
4041
  var JS_TEST = /(^|\/)(__tests__|__test__)\/|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)tests?\/.*\.[cm]?[jt]sx?$/;
@@ -3741,19 +4202,19 @@ var read = (input, path) => {
3741
4202
  const text6 = input.runtime.readText(path);
3742
4203
  return text6.ok ? text6.value : void 0;
3743
4204
  };
3744
- var proposed = (tool, args, before) => {
3745
- if (tool === "Write") return textField(args, "content") ?? "";
3746
- return before === void 0 ? void 0 : applyEdits(before, editsOf(args));
4205
+ var proposed = (tool, args2, before) => {
4206
+ if (tool === "Write") return textField(args2, "content") ?? "";
4207
+ return before === void 0 ? void 0 : applyEdits(before, editsOf(args2));
3747
4208
  };
3748
4209
  var fileViolations = (input, root) => {
3749
4210
  const tool = toolName(input.payload.raw);
3750
- const args = toolInput(input.payload.raw);
3751
- const file = textField(args, "file_path") ?? "";
4211
+ const args2 = toolInput(input.payload.raw);
4212
+ const file = textField(args2, "file_path") ?? "";
3752
4213
  if (tool === "NotebookEdit" || file === "") return [];
3753
4214
  const local = localPath(root, file);
3754
4215
  if (!isTestFile(local) && !isProtectedConfig(local)) return [];
3755
4216
  const before = read(input, toAbsolutePath(file, root));
3756
- const after = proposed(tool, args, before);
4217
+ const after = proposed(tool, args2, before);
3757
4218
  if (after === void 0) return [];
3758
4219
  const accepted2 = readTrust(input.runtime, fmPaths(root)).configs[local];
3759
4220
  if (before === void 0) return compareNewTestFile(local, after);
@@ -3764,7 +4225,7 @@ var shellViolations = (input, root) => {
3764
4225
  return parseShell(command).flatMap(
3765
4226
  (simple) => writtenPaths(simple).flatMap((path) => {
3766
4227
  const local = localPath(root, path);
3767
- const exists = input.runtime.stat(toAbsolutePath(join9(root, local))) !== void 0;
4228
+ const exists = input.runtime.stat(toAbsolutePath(join11(root, local))) !== void 0;
3768
4229
  if (!exists) return [];
3769
4230
  if (isProtectedConfig(local)) return [{ kind: "config_changed", path: local }];
3770
4231
  return isTestFile(local) ? [{ kind: "test_removed", path: local }] : [];
@@ -3784,20 +4245,20 @@ var testsGuard = (input) => {
3784
4245
  };
3785
4246
 
3786
4247
  // src/gate/hooks/config.ts
3787
- import { join as join11 } from "node:path";
4248
+ import { join as join13 } from "node:path";
3788
4249
 
3789
4250
  // src/gate/hooks/common.ts
3790
- import { dirname as dirname5, join as join10 } from "node:path";
4251
+ import { dirname as dirname5, join as join12 } from "node:path";
3791
4252
  var DEFAULT_GATE_BUDGET_MS = 3e5;
3792
4253
  var sessionCwd = (input) => input.payload.cwd === void 0 ? input.cwd : toAbsolutePath(input.payload.cwd, input.cwd);
3793
4254
  var MAX_DEPTH2 = 64;
3794
4255
  var findRepoRoot = (runtime, start) => {
3795
- let current = start;
4256
+ let current2 = start;
3796
4257
  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;
4258
+ if (runtime.stat(toAbsolutePath(join12(current2, ".git"))) !== void 0) return current2;
4259
+ const parent = toAbsolutePath(dirname5(current2));
4260
+ if (parent === current2) return void 0;
4261
+ current2 = parent;
3801
4262
  }
3802
4263
  return void 0;
3803
4264
  };
@@ -3817,7 +4278,7 @@ var contextOutput = (event, additionalContext, systemMessage) => ({
3817
4278
  var human = (text6) => `finitemonkey: ${text6}`;
3818
4279
 
3819
4280
  // src/gate/hooks/config.ts
3820
- var CONFIG_FILE2 = "config-hooks.json";
4281
+ var CONFIG_FILE3 = "config-hooks.json";
3821
4282
  var GUARDED_SOURCES = /* @__PURE__ */ new Set(["user_settings", "project_settings", "local_settings"]);
3822
4283
  var countIn = (value) => {
3823
4284
  if (typeof value === "string") return /\bfinitemonkey\b|\bfm hook\b/.test(value) ? 1 : 0;
@@ -3831,20 +4292,20 @@ var censusOf = (text6) => {
3831
4292
  return { hooks: countIn(parsed.value["hooks"]), disabled: parsed.value["disableAllHooks"] === true };
3832
4293
  };
3833
4294
  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"))]
4295
+ toAbsolutePath(join13(paths.root, ".claude", "settings.json")),
4296
+ toAbsolutePath(join13(paths.root, ".claude", "settings.local.json")),
4297
+ ...env["HOME"] === void 0 ? [] : [toAbsolutePath(join13(env["HOME"], ".claude", "settings.json"))]
3837
4298
  ];
3838
4299
  var read2 = (runtime, path) => {
3839
4300
  const text6 = runtime.readText(path);
3840
4301
  return text6.ok ? text6.value : void 0;
3841
4302
  };
3842
4303
  var known = (runtime, paths) => {
3843
- const raw = readJsonFile(runtime, paths.cache(CONFIG_FILE2))?.["hooks"];
4304
+ const raw = readJsonFile(runtime, paths.cache(CONFIG_FILE3))?.["hooks"];
3844
4305
  return isRecord(raw) ? Object.fromEntries(Object.entries(raw).filter((entry2) => typeof entry2[1] === "number")) : {};
3845
4306
  };
3846
4307
  var save = (runtime, paths, counts) => {
3847
- writeJsonFile(runtime, paths.cache(CONFIG_FILE2), { hooks: { ...counts } });
4308
+ writeJsonFile(runtime, paths.cache(CONFIG_FILE3), { hooks: { ...counts } });
3848
4309
  };
3849
4310
  var recordConfigHooks = (runtime, paths, env) => {
3850
4311
  const counts = Object.fromEntries(
@@ -3964,107 +4425,22 @@ var implicitContract = (files) => {
3964
4425
  acceptance: [
3965
4426
  ...commands,
3966
4427
  ...hasTests ? [] : [{ id: "tests", kind: "tests" }],
3967
- { id: "placeholders", kind: "placeholders" },
3968
- { id: "secrets", kind: "secrets" },
3969
- { id: "prose", kind: "prose" },
3970
- { id: "worktree", kind: "worktree" },
3971
- { id: "merge", kind: "merge" },
3972
- { id: "pr", kind: "pr", requireOpen: false }
3973
- ],
3974
- decisionsNeeded: [],
3975
- implicit: true
3976
- };
3977
- };
3978
-
3979
- // 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) : {};
4428
+ { id: "placeholders", kind: "placeholders" },
4429
+ { id: "secrets", kind: "secrets" },
4430
+ { id: "prose", kind: "prose" },
4431
+ { id: "worktree", kind: "worktree" },
4432
+ { id: "merge", kind: "merge" },
4433
+ { id: "callers", kind: "callers" },
4434
+ { id: "pr", kind: "pr", requireOpen: false }
4435
+ ],
4436
+ decisionsNeeded: [],
4437
+ implicit: true
4438
+ };
4066
4439
  };
4067
4440
 
4441
+ // src/gate/prepare.ts
4442
+ import { join as join15 } from "node:path";
4443
+
4068
4444
  // src/core/yaml/scalar.ts
4069
4445
  var ESCAPES = {
4070
4446
  '"': '"',
@@ -4343,8 +4719,8 @@ var TASK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
4343
4719
  var TASK_SCHEMA = "finitemonkey/task@1";
4344
4720
 
4345
4721
  // src/task/validate.ts
4346
- var str = (record2, key, at, issues, required) => {
4347
- const value = record2[key];
4722
+ var str = (record3, key, at, issues, required) => {
4723
+ const value = record3[key];
4348
4724
  if (value === void 0 || value === null) {
4349
4725
  if (required) issues.push(`${at}${key}: required`);
4350
4726
  return void 0;
@@ -4355,8 +4731,8 @@ var str = (record2, key, at, issues, required) => {
4355
4731
  }
4356
4732
  return value;
4357
4733
  };
4358
- var positive = (record2, key, at, issues) => {
4359
- const value = record2[key];
4734
+ var positive2 = (record3, key, at, issues) => {
4735
+ const value = record3[key];
4360
4736
  if (value === void 0 || value === null) return void 0;
4361
4737
  if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
4362
4738
  issues.push(`${at}${key}: must be a positive integer`);
@@ -4375,19 +4751,35 @@ var gitRequire = (value, at, issues) => {
4375
4751
  if (known2.some((item) => item === void 0)) issues.push(`${at}: use ${GIT_REQUIREMENTS.join(", ")}`);
4376
4752
  return known2.filter((item) => item !== void 0);
4377
4753
  };
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"];
4754
+ var appFields = (record3, kind, at, issues) => {
4755
+ const port = positive2(record3, "port", at, issues);
4756
+ if (kind === "runtime" && port === void 0) issues.push(`${at}port: required`);
4757
+ if (port !== void 0 && port > 65535) issues.push(`${at}port: must be at most 65535`);
4758
+ const status = positive2(record3, "status", at, issues);
4759
+ const route = str(record3, "route", at, issues, false);
4760
+ const body2 = str(record3, "body", at, issues, false);
4761
+ return {
4762
+ ...port === void 0 ? {} : { port },
4763
+ ...route === void 0 ? {} : { route },
4764
+ ...status === void 0 ? {} : { status },
4765
+ ...body2 === void 0 ? {} : { body: body2 }
4766
+ };
4767
+ };
4768
+ var checkFields = (record3, kind, at, issues) => {
4769
+ const run4 = str(record3, "run", at, issues, kind === "command");
4770
+ const timeoutS = positive2(record3, "timeout_s", at, issues);
4771
+ const description = str(record3, "description", at, issues, kind === "manual");
4772
+ const require2 = kind === "git" ? gitRequire(record3["require"] ?? ["committed"], `${at}require`, issues) : void 0;
4773
+ const app = appFields(record3, kind, at, issues);
4774
+ const open = record3["require_open"];
4384
4775
  if (open !== void 0 && typeof open !== "boolean") issues.push(`${at}require_open: must be true or false`);
4385
4776
  return {
4386
4777
  ...run4 === void 0 ? {} : { run: run4 },
4387
4778
  ...timeoutS === void 0 ? {} : { timeoutS },
4388
4779
  ...description === void 0 ? {} : { description },
4389
4780
  ...require2 === void 0 ? {} : { require: require2 },
4390
- ...typeof open === "boolean" ? { requireOpen: open } : {}
4781
+ ...typeof open === "boolean" ? { requireOpen: open } : {},
4782
+ ...app
4391
4783
  };
4392
4784
  };
4393
4785
  var acceptanceItem = (value, index, issues) => {
@@ -4425,8 +4817,8 @@ var scope = (value, issues) => {
4425
4817
  ...files === void 0 ? {} : { files },
4426
4818
  ...lines3 === void 0 ? {} : { lines: lines3 }
4427
4819
  }))({
4428
- files: positive(budget, "files", "scope.diff_budget.", issues),
4429
- lines: positive(budget, "lines", "scope.diff_budget.", issues)
4820
+ files: positive2(budget, "files", "scope.diff_budget.", issues),
4821
+ lines: positive2(budget, "lines", "scope.diff_budget.", issues)
4430
4822
  }) : void 0;
4431
4823
  if (budget !== void 0 && budget !== null && !isRecord(budget)) issues.push("scope.diff_budget: must be a mapping");
4432
4824
  const protectedValue = value["tests_protected"];
@@ -4451,6 +4843,7 @@ var validateContract = (value, implicit = false) => {
4451
4843
  const contractScope = scope(value["scope"], issues);
4452
4844
  const checks = acceptance(value["acceptance"], issues);
4453
4845
  const decisionsNeeded = strings2(value["decisions_needed"], "decisions_needed", issues);
4846
+ const callersIgnore = strings2(value["callers_ignore"], "callers_ignore", issues);
4454
4847
  if (issues.length > 0 || id === void 0 || goal === void 0 || size === void 0) return reject(issues);
4455
4848
  return accept({
4456
4849
  id,
@@ -4460,6 +4853,7 @@ var validateContract = (value, implicit = false) => {
4460
4853
  scope: contractScope,
4461
4854
  acceptance: checks,
4462
4855
  decisionsNeeded,
4856
+ ...callersIgnore.length === 0 ? {} : { callersIgnore },
4463
4857
  implicit
4464
4858
  });
4465
4859
  };
@@ -4476,7 +4870,7 @@ var parseContract = (id, text6) => {
4476
4870
  if (valid.value.id !== id) return reject({ kind: "invalid", id, issues: [`id: must match the file name (${id})`] });
4477
4871
  return accept(valid.value);
4478
4872
  };
4479
- var pick = (lock, sources, id) => {
4873
+ var pick2 = (lock, sources, id) => {
4480
4874
  const approved = lock.approved[id];
4481
4875
  if (approved !== void 0) return { text: approved, source: "lock" };
4482
4876
  const base = sources.base(taskRelative(id));
@@ -4487,7 +4881,7 @@ var pick = (lock, sources, id) => {
4487
4881
  var loadTask = (files, paths, sources, id) => {
4488
4882
  if (!TASK_ID.test(id)) return reject({ kind: "not_found", id });
4489
4883
  const lock = readTrust(files, paths);
4490
- const chosen = pick(lock, sources, id);
4884
+ const chosen = pick2(lock, sources, id);
4491
4885
  if (chosen === void 0) return reject({ kind: "not_found", id });
4492
4886
  const parsed = parseContract(id, chosen.text);
4493
4887
  if (!parsed.ok) return parsed;
@@ -4542,19 +4936,19 @@ var readBaseline = (runtime, paths) => {
4542
4936
  var writeBaseline = (runtime, paths, treeHash2, checks) => writeJsonFile(runtime, paths.cache(BASELINE_FILE), { treeHash: treeHash2, checks: { ...checks } }).ok;
4543
4937
 
4544
4938
  // src/gate/diff-shape.ts
4545
- import { join as join13 } from "node:path";
4939
+ import { join as join14 } from "node:path";
4546
4940
 
4547
4941
  // src/gate/git.ts
4548
4942
  var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
4549
4943
  var GIT_TIMEOUT_MS = 15e3;
4550
4944
  var EXCLUDE_FM = `:(exclude)${FM_DIR}`;
4551
4945
  var gitAt = (runtime, cwd) => {
4552
- const run4 = (args) => runtime.exec({ command: "git", args: ["-c", "core.quotepath=off", ...args], cwd, timeoutMs: GIT_TIMEOUT_MS });
4946
+ const run4 = (args2) => runtime.exec({ command: "git", args: ["-c", "core.quotepath=off", ...args2], cwd, timeoutMs: GIT_TIMEOUT_MS });
4553
4947
  return {
4554
4948
  cwd,
4555
4949
  run: run4,
4556
- out: (args) => {
4557
- const result = run4(args);
4950
+ out: (args2) => {
4951
+ const result = run4(args2);
4558
4952
  return result.found && result.exitCode === 0 ? result.stdout : void 0;
4559
4953
  }
4560
4954
  };
@@ -4649,7 +5043,7 @@ var aheadOfUpstream = (git) => {
4649
5043
 
4650
5044
  // src/gate/diff-shape.ts
4651
5045
  var untrackedLines = (runtime, root, changes) => changes.filter((change) => change.status === "A").reduce((sum, change) => {
4652
- const text6 = runtime.readText(toAbsolutePath(join13(root, change.path)));
5046
+ const text6 = runtime.readText(toAbsolutePath(join14(root, change.path)));
4653
5047
  return sum + (text6.ok ? text6.value.split("\n").length : 0);
4654
5048
  }, 0);
4655
5049
  var diffShape = (runtime, git, base, changes) => ({
@@ -4676,18 +5070,18 @@ var ciChecks = (value) => Array.isArray(value) ? value.flatMap((item) => {
4676
5070
  var parsePrView = (text6) => {
4677
5071
  const parsed = parseJsonObject(text6);
4678
5072
  if (!parsed.ok) return void 0;
4679
- const record2 = parsed.value;
4680
- const number = record2["number"];
5073
+ const record3 = parsed.value;
5074
+ const number = record3["number"];
4681
5075
  if (typeof number !== "number") return void 0;
4682
5076
  return {
4683
5077
  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"])
5078
+ url: optionalString(record3, "url") ?? "",
5079
+ state: optionalString(record3, "state") ?? "",
5080
+ mergeable: optionalString(record3, "mergeable") ?? "UNKNOWN",
5081
+ mergeStateStatus: optionalString(record3, "mergeStateStatus") ?? "UNKNOWN",
5082
+ baseRefName: optionalString(record3, "baseRefName") ?? "",
5083
+ headRefOid: optionalString(record3, "headRefOid") ?? "",
5084
+ checks: ciChecks(record3["statusCheckRollup"])
4691
5085
  };
4692
5086
  };
4693
5087
  var isConflicting = (pr) => pr.mergeable === "CONFLICTING" || pr.mergeStateStatus === "DIRTY";
@@ -4701,7 +5095,7 @@ var parseMergeTreeConflicts = (stdout) => {
4701
5095
  var PR_FIELDS = "number,url,state,mergeable,mergeStateStatus,baseRefName,headRefOid,statusCheckRollup";
4702
5096
  var PR_CACHE_TTL_MS = 6e4;
4703
5097
  var GH_TIMEOUT_MS = 8e3;
4704
- var gh = (runtime, cwd, args, timeoutMs = GH_TIMEOUT_MS) => runtime.exec({ command: "gh", args, cwd, timeoutMs });
5098
+ var gh = (runtime, cwd, args2, timeoutMs = GH_TIMEOUT_MS) => runtime.exec({ command: "gh", args: args2, cwd, timeoutMs });
4705
5099
  var ghAuthenticated = (runtime, cwd, timeoutMs = 4e3) => {
4706
5100
  const result = gh(runtime, cwd, ["auth", "status"], timeoutMs);
4707
5101
  if (result.timedOut) return "timeout";
@@ -4855,10 +5249,10 @@ var applySize = (contract, verdict) => {
4855
5249
  // src/gate/prepare.ts
4856
5250
  var diskFiles = (runtime, root) => ({
4857
5251
  read: (relative9) => {
4858
- const text6 = runtime.readText(toAbsolutePath(join14(root, relative9)));
5252
+ const text6 = runtime.readText(toAbsolutePath(join15(root, relative9)));
4859
5253
  return text6.ok ? text6.value : void 0;
4860
5254
  },
4861
- exists: (relative9) => runtime.stat(toAbsolutePath(join14(root, relative9))) !== void 0
5255
+ exists: (relative9) => runtime.stat(toAbsolutePath(join15(root, relative9))) !== void 0
4862
5256
  });
4863
5257
  var baseFiles = (git, base, disk) => ({
4864
5258
  read: (relative9) => base === EMPTY_TREE ? disk.read(relative9) : fileAt(git, base, relative9),
@@ -5120,19 +5514,19 @@ var params = (value) => isRecord(value) ? Object.fromEntries(
5120
5514
  var notes = (value) => Array.isArray(value) ? value.flatMap(
5121
5515
  (item) => isRecord(item) && typeof item["key"] === "string" ? [{ key: item["key"], params: params(item["params"]) }] : []
5122
5516
  ) : [];
5123
- var text3 = (record2, key) => {
5124
- const value = record2[key];
5517
+ var text3 = (record3, key) => {
5518
+ const value = record3[key];
5125
5519
  return typeof value === "string" ? value : void 0;
5126
5520
  };
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"];
5521
+ var optional = (record3) => {
5522
+ const exitCode = record3["exitCode"];
5523
+ const durationMs = record3["durationMs"];
5524
+ const command = text3(record3, "command");
5525
+ const stdoutSha256 = text3(record3, "stdoutSha256");
5526
+ const releasedBy = text3(record3, "releasedBy");
5527
+ const reason = oneOf2(UNVERIFIED_REASONS, record3["reason"]);
5528
+ const failures = record3["failures"];
5529
+ const testCount = record3["testCount"];
5136
5530
  return {
5137
5531
  ...command === void 0 ? {} : { command },
5138
5532
  ...typeof exitCode === "number" || exitCode === null ? { exitCode } : {},
@@ -5147,14 +5541,14 @@ var optional = (record2) => {
5147
5541
  var parseEvidence = (line2) => {
5148
5542
  const parsed = parseJsonObject(line2);
5149
5543
  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;
5544
+ const record3 = parsed.value;
5545
+ const status = oneOf2(EVIDENCE_STATUSES, record3["status"]);
5546
+ const task = text3(record3, "task");
5547
+ const check2 = text3(record3, "check");
5548
+ const kind = text3(record3, "kind");
5549
+ const treeHash2 = text3(record3, "treeHash");
5550
+ const at = text3(record3, "at");
5551
+ if (record3["schema"] !== EVIDENCE_SCHEMA || status === void 0) return void 0;
5158
5552
  if (task === void 0 || check2 === void 0 || kind === void 0 || treeHash2 === void 0 || at === void 0) {
5159
5553
  return void 0;
5160
5554
  }
@@ -5166,8 +5560,8 @@ var parseEvidence = (line2) => {
5166
5560
  status,
5167
5561
  treeHash: treeHash2,
5168
5562
  at,
5169
- notes: notes(record2["notes"]),
5170
- ...optional(record2)
5563
+ notes: notes(record3["notes"]),
5564
+ ...optional(record3)
5171
5565
  };
5172
5566
  };
5173
5567
  var readLedger = (runtime, path) => {
@@ -5706,7 +6100,36 @@ var en_default = {
5706
6100
  "gate.secrets.warning": "warning: {rule} at {file}:{line}",
5707
6101
  "doctor.secrets.remote_credential": "git remote {remotes} has a credential in its URL, stored in plain text in .git/config",
5708
6102
  "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."
6103
+ "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.",
6104
+ "gate.require.callers": "no removed or renamed symbol is still called",
6105
+ "gate.callers.none": "{count, plural, one {# removed definition} other {# removed definitions}}, no caller left",
6106
+ "gate.callers.open": "{name} is gone but still called in {count, plural, one {# place} other {# places}}",
6107
+ "gate.callers.at": "{name} called at {file}:{line}",
6108
+ "gate.callers.warning": "warning only: {percent}% of callers findings were released with callers_ignore ({released} of {total}), above the 5% ceiling",
6109
+ "command.policy_false_positive.summary": "Record a false positive of a guard in shadow so it is not promoted",
6110
+ "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)",
6111
+ "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.",
6112
+ "command.app_check.summary": "Start the app or reuse the running one, call one route and check the answer",
6113
+ "app.check.ok": "{url} answered {status}",
6114
+ "app.check.crashed": "the app exited with code {code} before opening the port",
6115
+ "app.check.no_port": "the port of {url} did not open before the timeout",
6116
+ "app.check.no_start": "nothing listens on {url} and no start command was given",
6117
+ "app.check.wrong_status": "{url} answered {status}",
6118
+ "app.check.body_mismatch": "{url} answered {status} without the expected text",
6119
+ "app.check.request_failed": "the request to {url} failed",
6120
+ "app.check.usage": "give --port as a number from 1 to 65535; --status and --timeout must be positive numbers",
6121
+ "gate.require.runtime": "the app starts and `{command}` answers as declared",
6122
+ "gate.runtime.ok": "started, {route} answered {status}, stopped",
6123
+ "gate.runtime.reused": "reused the running app, {route} answered {status}",
6124
+ "gate.runtime.crashed": "the app exited with code {code} before opening its port",
6125
+ "gate.runtime.no_port": "port {port} did not open before the timeout",
6126
+ "gate.runtime.no_start": "nothing listens on port {port} and the check has no `run` to start the app",
6127
+ "gate.runtime.no_port_declared": "the runtime check needs a `port`",
6128
+ "gate.runtime.wrong_status": "{route} answered {status}, expected {expected}",
6129
+ "gate.runtime.body_mismatch": '{route} did not contain "{body}"',
6130
+ "gate.runtime.request_failed": "the request to {route} failed: {detail}",
6131
+ "gate.runtime.orphan": "the app on port {port} could not be stopped",
6132
+ "gate.runtime.probe_failed": "the app check did not report a result: {line}"
5710
6133
  };
5711
6134
 
5712
6135
  // src/i18n/es.json
@@ -6230,7 +6653,36 @@ var es_default = {
6230
6653
  "gate.secrets.warning": "aviso: {rule} en {file}:{line}",
6231
6654
  "doctor.secrets.remote_credential": "el remoto git {remotes} tiene una credencial en la URL, guardada en texto plano en .git/config",
6232
6655
  "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."
6656
+ "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.",
6657
+ "gate.require.callers": "ningún símbolo eliminado o renombrado sigue siendo llamado",
6658
+ "gate.callers.none": "{count, plural, one {# definición eliminada} other {# definiciones eliminadas}}, ningún llamador quedó",
6659
+ "gate.callers.open": "{name} ya no existe, pero se llama en {count, plural, one {# lugar} other {# lugares}}",
6660
+ "gate.callers.at": "{name} llamado en {file}:{line}",
6661
+ "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%",
6662
+ "command.policy_false_positive.summary": "Registra un falso positivo de un guard en shadow para que no se promueva",
6663
+ "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)",
6664
+ "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.",
6665
+ "command.app_check.summary": "Arranca la app o reutiliza la que ya corre, llama a una ruta y comprueba la respuesta",
6666
+ "app.check.ok": "{url} respondió {status}",
6667
+ "app.check.crashed": "la app salió con código {code} antes de abrir el puerto",
6668
+ "app.check.no_port": "el puerto de {url} no se abrió antes del tiempo límite",
6669
+ "app.check.no_start": "nada escucha en {url} y no se indicó comando de arranque",
6670
+ "app.check.wrong_status": "{url} respondió {status}",
6671
+ "app.check.body_mismatch": "{url} respondió {status} sin el texto esperado",
6672
+ "app.check.request_failed": "la petición a {url} falló",
6673
+ "app.check.usage": "indica --port como número de 1 a 65535; --status y --timeout deben ser números positivos",
6674
+ "gate.require.runtime": "la app arranca y `{command}` responde como se declaró",
6675
+ "gate.runtime.ok": "arrancó, {route} respondió {status}, detenida",
6676
+ "gate.runtime.reused": "reutilizó la app en marcha, {route} respondió {status}",
6677
+ "gate.runtime.crashed": "la app salió con código {code} antes de abrir su puerto",
6678
+ "gate.runtime.no_port": "el puerto {port} no se abrió antes del tiempo límite",
6679
+ "gate.runtime.no_start": "nada escucha en el puerto {port} y la comprobación no tiene `run` para arrancar la app",
6680
+ "gate.runtime.no_port_declared": "la comprobación runtime necesita `port`",
6681
+ "gate.runtime.wrong_status": "{route} respondió {status}, se esperaba {expected}",
6682
+ "gate.runtime.body_mismatch": '{route} no contenía "{body}"',
6683
+ "gate.runtime.request_failed": "la petición a {route} falló: {detail}",
6684
+ "gate.runtime.orphan": "no se pudo detener la app del puerto {port}",
6685
+ "gate.runtime.probe_failed": "la comprobación de la app no devolvió resultado: {line}"
6234
6686
  };
6235
6687
 
6236
6688
  // src/i18n/pt-BR.json
@@ -6754,7 +7206,36 @@ var pt_BR_default = {
6754
7206
  "gate.secrets.warning": "aviso: {rule} em {file}:{line}",
6755
7207
  "doctor.secrets.remote_credential": "o remoto git {remotes} tem uma credencial na URL, guardada em texto puro no .git/config",
6756
7208
  "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."
7209
+ "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.",
7210
+ "gate.require.callers": "nenhum símbolo removido ou renomeado continua sendo chamado",
7211
+ "gate.callers.none": "{count, plural, one {# definição removida} other {# definições removidas}}, nenhum chamador sobrou",
7212
+ "gate.callers.open": "{name} saiu, mas é chamado em {count, plural, one {# lugar} other {# lugares}}",
7213
+ "gate.callers.at": "{name} chamado em {file}:{line}",
7214
+ "gate.callers.warning": "só aviso: {percent}% dos achados de callers foram liberados com callers_ignore ({released} de {total}), acima do teto de 5%",
7215
+ "command.policy_false_positive.summary": "Registra um falso positivo de um guard em shadow para ele não ser promovido",
7216
+ "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)",
7217
+ "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.",
7218
+ "command.app_check.summary": "Sobe o app ou reaproveita o que está rodando, chama uma rota e confere a resposta",
7219
+ "app.check.ok": "{url} respondeu {status}",
7220
+ "app.check.crashed": "o app saiu com código {code} antes de abrir a porta",
7221
+ "app.check.no_port": "a porta de {url} não abriu antes do tempo limite",
7222
+ "app.check.no_start": "nada escuta em {url} e nenhum comando de início foi dado",
7223
+ "app.check.wrong_status": "{url} respondeu {status}",
7224
+ "app.check.body_mismatch": "{url} respondeu {status} sem o texto esperado",
7225
+ "app.check.request_failed": "a requisição para {url} falhou",
7226
+ "app.check.usage": "passe --port como número de 1 a 65535; --status e --timeout precisam ser números positivos",
7227
+ "gate.require.runtime": "o app sobe e `{command}` responde como declarado",
7228
+ "gate.runtime.ok": "subiu, {route} respondeu {status}, parado",
7229
+ "gate.runtime.reused": "reaproveitou o app rodando, {route} respondeu {status}",
7230
+ "gate.runtime.crashed": "o app saiu com código {code} antes de abrir a porta",
7231
+ "gate.runtime.no_port": "a porta {port} não abriu antes do tempo limite",
7232
+ "gate.runtime.no_start": "nada escuta na porta {port} e a checagem não tem `run` para subir o app",
7233
+ "gate.runtime.no_port_declared": "a checagem runtime precisa de `port`",
7234
+ "gate.runtime.wrong_status": "{route} respondeu {status}, esperado {expected}",
7235
+ "gate.runtime.body_mismatch": '{route} não continha "{body}"',
7236
+ "gate.runtime.request_failed": "a requisição para {route} falhou: {detail}",
7237
+ "gate.runtime.orphan": "o app na porta {port} não pôde ser parado",
7238
+ "gate.runtime.probe_failed": "a checagem do app não devolveu resultado: {line}"
6758
7239
  };
6759
7240
 
6760
7241
  // src/i18n/review.json
@@ -7279,7 +7760,36 @@ var review_default = {
7279
7760
  "gate.secrets.warning": "machine",
7280
7761
  "doctor.secrets.remote_credential": "machine",
7281
7762
  "doctor.secrets.fix_remote_credential": "machine",
7282
- "secrets.session.remote_credential": "machine"
7763
+ "secrets.session.remote_credential": "machine",
7764
+ "gate.require.callers": "machine",
7765
+ "gate.callers.none": "machine",
7766
+ "gate.callers.open": "machine",
7767
+ "gate.callers.at": "machine",
7768
+ "gate.callers.warning": "machine",
7769
+ "command.policy_false_positive.summary": "machine",
7770
+ "policy.error.person_only": "machine",
7771
+ "policy.false_positive.recorded": "machine",
7772
+ "command.app_check.summary": "machine",
7773
+ "app.check.ok": "machine",
7774
+ "app.check.crashed": "machine",
7775
+ "app.check.no_port": "machine",
7776
+ "app.check.no_start": "machine",
7777
+ "app.check.wrong_status": "machine",
7778
+ "app.check.body_mismatch": "machine",
7779
+ "app.check.request_failed": "machine",
7780
+ "app.check.usage": "machine",
7781
+ "gate.require.runtime": "machine",
7782
+ "gate.runtime.ok": "machine",
7783
+ "gate.runtime.reused": "machine",
7784
+ "gate.runtime.crashed": "machine",
7785
+ "gate.runtime.no_port": "machine",
7786
+ "gate.runtime.no_start": "machine",
7787
+ "gate.runtime.no_port_declared": "machine",
7788
+ "gate.runtime.wrong_status": "machine",
7789
+ "gate.runtime.body_mismatch": "machine",
7790
+ "gate.runtime.request_failed": "machine",
7791
+ "gate.runtime.orphan": "machine",
7792
+ "gate.runtime.probe_failed": "machine"
7283
7793
  },
7284
7794
  es: {
7285
7795
  "command.init.summary": "machine",
@@ -7801,7 +8311,36 @@ var review_default = {
7801
8311
  "gate.secrets.warning": "machine",
7802
8312
  "doctor.secrets.remote_credential": "machine",
7803
8313
  "doctor.secrets.fix_remote_credential": "machine",
7804
- "secrets.session.remote_credential": "machine"
8314
+ "secrets.session.remote_credential": "machine",
8315
+ "gate.require.callers": "machine",
8316
+ "gate.callers.none": "machine",
8317
+ "gate.callers.open": "machine",
8318
+ "gate.callers.at": "machine",
8319
+ "gate.callers.warning": "machine",
8320
+ "command.policy_false_positive.summary": "machine",
8321
+ "policy.error.person_only": "machine",
8322
+ "policy.false_positive.recorded": "machine",
8323
+ "command.app_check.summary": "machine",
8324
+ "app.check.ok": "machine",
8325
+ "app.check.crashed": "machine",
8326
+ "app.check.no_port": "machine",
8327
+ "app.check.no_start": "machine",
8328
+ "app.check.wrong_status": "machine",
8329
+ "app.check.body_mismatch": "machine",
8330
+ "app.check.request_failed": "machine",
8331
+ "app.check.usage": "machine",
8332
+ "gate.require.runtime": "machine",
8333
+ "gate.runtime.ok": "machine",
8334
+ "gate.runtime.reused": "machine",
8335
+ "gate.runtime.crashed": "machine",
8336
+ "gate.runtime.no_port": "machine",
8337
+ "gate.runtime.no_start": "machine",
8338
+ "gate.runtime.no_port_declared": "machine",
8339
+ "gate.runtime.wrong_status": "machine",
8340
+ "gate.runtime.body_mismatch": "machine",
8341
+ "gate.runtime.request_failed": "machine",
8342
+ "gate.runtime.orphan": "machine",
8343
+ "gate.runtime.probe_failed": "machine"
7805
8344
  }
7806
8345
  };
7807
8346
 
@@ -8080,6 +8619,256 @@ var recordReview = (runtime, paths, treeHash2, agent) => {
8080
8619
  return writeJsonFile(runtime, paths.cache(REVIEWS_FILE), { reviews: reviews.map((review2) => ({ ...review2 })) }).ok;
8081
8620
  };
8082
8621
 
8622
+ // src/gate/callers/rate.ts
8623
+ var FLAGGED_EVENT = "callers.flagged";
8624
+ var RELEASED_EVENT = "callers.released";
8625
+ var MIN_SAMPLE = 20;
8626
+ var MAX_FALSE_POSITIVE = 0.05;
8627
+ var pairs = (record3) => {
8628
+ const { task, names } = record3;
8629
+ if (typeof task !== "string" || !Array.isArray(names)) return [];
8630
+ return names.filter((name) => typeof name === "string").map((name) => `${task}\0${name}`);
8631
+ };
8632
+ var callersRate = (runtime, paths) => {
8633
+ const text6 = runtime.readText(paths.log);
8634
+ const flagged = /* @__PURE__ */ new Set();
8635
+ const released2 = /* @__PURE__ */ new Set();
8636
+ for (const line2 of text6.ok ? text6.value.split("\n") : []) {
8637
+ const parsed = line2.trim() === "" ? void 0 : parseJsonObject(line2);
8638
+ if (parsed === void 0 || !parsed.ok || !isRecord(parsed.value)) continue;
8639
+ const event = parsed.value["event"];
8640
+ const target = event === FLAGGED_EVENT ? flagged : event === RELEASED_EVENT ? released2 : void 0;
8641
+ if (target !== void 0) for (const pair of pairs(parsed.value)) target.add(pair);
8642
+ }
8643
+ const total = (/* @__PURE__ */ new Set([...flagged, ...released2])).size;
8644
+ const rate = total === 0 ? 0 : released2.size / total;
8645
+ return {
8646
+ flagged: total,
8647
+ released: released2.size,
8648
+ rate,
8649
+ downgraded: total >= MIN_SAMPLE && rate > MAX_FALSE_POSITIVE
8650
+ };
8651
+ };
8652
+
8653
+ // src/gate/callers/scan.ts
8654
+ import { join as join16 } from "node:path";
8655
+
8656
+ // src/gate/callers/imports.ts
8657
+ import { posix as posix2 } from "node:path";
8658
+ var JS_SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(?\s*|\brequire\s*\(\s*)["']([^"']+)["']/g;
8659
+ var JS_EXTENSION = /\.(?:[cm]?[jt]sx?)$/;
8660
+ var stripModule = (path) => path.replace(JS_EXTENSION, "").replace(/\/index$/, "");
8661
+ var jsSpecifiers = (text6) => [...text6.matchAll(JS_SPECIFIER)].map((match) => match[1] ?? "");
8662
+ var jsImports = (callerPath, callerText, origin) => {
8663
+ const target = stripModule(origin);
8664
+ return jsSpecifiers(callerText).some((specifier) => {
8665
+ if (specifier.startsWith(".")) {
8666
+ return stripModule(posix2.normalize(posix2.join(posix2.dirname(callerPath), specifier))) === target;
8667
+ }
8668
+ const bare = stripModule(specifier.replace(/^[@~#]\//, ""));
8669
+ return target === bare || target.endsWith(`/${bare}`);
8670
+ });
8671
+ };
8672
+ var pyImports = (callerText, origin) => {
8673
+ const stem = posix2.basename(origin).replace(/\.py$/, "");
8674
+ const module = stem === "__init__" ? posix2.basename(posix2.dirname(origin)) : stem;
8675
+ const word2 = new RegExp(`\\b${module}\\b`);
8676
+ return callerText.split(/\r?\n/).some((line2) => /^\s*(?:from|import)\s/.test(line2) && word2.test(line2));
8677
+ };
8678
+ var goImports = (callerPath, callerText, origin) => {
8679
+ const dir = posix2.dirname(origin);
8680
+ if (posix2.dirname(callerPath) === dir) return true;
8681
+ const last = posix2.basename(dir);
8682
+ return new RegExp(`"[^"]*\\b${last}"`).test(callerText);
8683
+ };
8684
+ var importsOrigin = (language, callerPath, callerText, origins) => origins.some((origin) => {
8685
+ if (callerPath === origin) return true;
8686
+ switch (language.imports) {
8687
+ case "none":
8688
+ return true;
8689
+ case "package":
8690
+ return goImports(callerPath, callerText, origin);
8691
+ case "module":
8692
+ return language.id === "py" ? pyImports(callerText, origin) : jsImports(callerPath, callerText, origin);
8693
+ }
8694
+ });
8695
+
8696
+ // src/gate/callers/languages.ts
8697
+ import { extname } from "node:path";
8698
+ var JS_NAME = "[A-Za-z_$][\\w$]*";
8699
+ var LANGUAGES2 = [
8700
+ {
8701
+ id: "js",
8702
+ extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"],
8703
+ definitions: [
8704
+ new RegExp(`^\\s*(?:export\\s+)?(?:default\\s+)?(?:async\\s+)?function\\s*\\*?\\s*(${JS_NAME})`),
8705
+ new RegExp(`^\\s*(?:export\\s+)?(?:default\\s+)?(?:abstract\\s+)?class\\s+(${JS_NAME})`),
8706
+ new RegExp(`^\\s*export\\s+(?:declare\\s+)?(?:const|let|var|enum|interface|type)\\s+(${JS_NAME})`),
8707
+ new RegExp(
8708
+ `^\\s+(?:public\\s+)?(?:static\\s+)?(?:async\\s+)?(${JS_NAME})\\s*\\([^)]*\\)\\s*(?::[^{=]+)?\\{\\s*$`
8709
+ )
8710
+ ],
8711
+ imports: "module",
8712
+ comment: /^\s*(?:\/\/|\/\*|\*)/
8713
+ },
8714
+ {
8715
+ id: "py",
8716
+ extensions: [".py"],
8717
+ definitions: [/^\s*(?:async\s+)?def\s+([A-Za-z]\w*)/, /^\s*class\s+([A-Za-z]\w*)/],
8718
+ imports: "module",
8719
+ comment: /^\s*#/
8720
+ },
8721
+ {
8722
+ id: "go",
8723
+ extensions: [".go"],
8724
+ definitions: [/^func\s+(?:\([^)]*\)\s*)?([A-Za-z]\w*)/, /^type\s+([A-Za-z]\w*)\s+(?:struct|interface)/],
8725
+ imports: "package",
8726
+ comment: /^\s*\/\//
8727
+ },
8728
+ {
8729
+ id: "rb",
8730
+ extensions: [".rb"],
8731
+ definitions: [/^\s*def\s+(?:self\.)?([A-Za-z]\w*[?!]?)/, /^\s*(?:class|module)\s+([A-Z]\w*)/],
8732
+ imports: "none",
8733
+ comment: /^\s*#/
8734
+ },
8735
+ {
8736
+ id: "rs",
8737
+ extensions: [".rs"],
8738
+ definitions: [
8739
+ /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([A-Za-z]\w*)/,
8740
+ /^\s*(?:pub\s+)?(?:struct|enum|trait)\s+([A-Z]\w*)/
8741
+ ],
8742
+ imports: "none",
8743
+ comment: /^\s*\/\//
8744
+ }
8745
+ ];
8746
+ var KEYWORDS = /* @__PURE__ */ new Set([
8747
+ "if",
8748
+ "for",
8749
+ "while",
8750
+ "switch",
8751
+ "catch",
8752
+ "function",
8753
+ "return",
8754
+ "constructor",
8755
+ "with",
8756
+ "do",
8757
+ "else"
8758
+ ]);
8759
+ var languageOf = (path) => {
8760
+ const extension = extname(path).toLowerCase();
8761
+ return LANGUAGES2.find((language) => language.extensions.includes(extension));
8762
+ };
8763
+ var definitionsIn = (language, text6) => {
8764
+ const names = /* @__PURE__ */ new Set();
8765
+ for (const line2 of text6.split(/\r?\n/)) {
8766
+ for (const pattern of language.definitions) {
8767
+ const name = pattern.exec(line2)?.[1];
8768
+ if (name !== void 0 && !KEYWORDS.has(name)) names.add(name);
8769
+ }
8770
+ }
8771
+ return names;
8772
+ };
8773
+ var definesName = (language, line2, name) => language.definitions.some((pattern) => pattern.exec(line2)?.[1] === name);
8774
+
8775
+ // src/gate/callers/scan.ts
8776
+ var MAX_SYMBOLS = 200;
8777
+ var current = (runtime, root, path) => {
8778
+ const text6 = runtime.readText(toAbsolutePath(join16(root, path)));
8779
+ return text6.ok ? text6.value : "";
8780
+ };
8781
+ var keyOf = (language, name) => `${language.id}\0${name}`;
8782
+ var redefinedNames = (runtime, git, changes) => {
8783
+ const keys = /* @__PURE__ */ new Set();
8784
+ for (const change of changes) {
8785
+ const language = languageOf(change.path);
8786
+ if (language === void 0 || change.status === "D") continue;
8787
+ for (const name of definitionsIn(language, current(runtime, git.cwd, change.path))) keys.add(keyOf(language, name));
8788
+ }
8789
+ return keys;
8790
+ };
8791
+ var removedSymbols = (runtime, git, base, changes) => {
8792
+ const redefined = redefinedNames(runtime, git, changes);
8793
+ const found = /* @__PURE__ */ new Map();
8794
+ for (const change of changes) {
8795
+ const language = languageOf(change.oldPath ?? change.path);
8796
+ if (language === void 0 || change.status === "A") continue;
8797
+ const before = definitionsIn(language, fileAt(git, base, change.oldPath ?? change.path) ?? "");
8798
+ const origins = [change.path, ...change.oldPath === void 0 ? [] : [change.oldPath]];
8799
+ for (const name of before) {
8800
+ const key = keyOf(language, name);
8801
+ if (redefined.has(key)) continue;
8802
+ const prior = found.get(key);
8803
+ found.set(key, { name, language, origins: [...prior?.origins ?? [], ...origins] });
8804
+ }
8805
+ }
8806
+ return [...found.values()].slice(0, MAX_SYMBOLS);
8807
+ };
8808
+ var parseGrep = (output) => output.split("\n").filter((row) => row !== "").flatMap((row) => {
8809
+ const [file = "", line2 = "", ...rest] = row.split("\0");
8810
+ const number = Number(line2);
8811
+ return file === "" || !Number.isInteger(number) ? [] : [{ file: toPosixPath(file), line: number, text: rest.join("\0") }];
8812
+ });
8813
+ var grep = (git, names) => {
8814
+ if (names.length === 0) return [];
8815
+ const patterns = names.flatMap((name) => ["-e", name]);
8816
+ return parseGrep(
8817
+ git.out(["grep", "-n", "-z", "-w", "-I", "--untracked", "--no-color", ...patterns, "--", ".", EXCLUDE_FM]) ?? ""
8818
+ );
8819
+ };
8820
+ var mentions = (text6, name) => new RegExp(`(?<![\\w$])${name.replace(/[$?!]/g, "\\$&")}(?![\\w$])`).test(text6);
8821
+ var callSites = (runtime, git, symbols) => {
8822
+ const hits = grep(git, [...new Set(symbols.map((symbol) => symbol.name))]);
8823
+ const texts = /* @__PURE__ */ new Map();
8824
+ const textOf4 = (file) => {
8825
+ const cached2 = texts.get(file) ?? current(runtime, git.cwd, file);
8826
+ texts.set(file, cached2);
8827
+ return cached2;
8828
+ };
8829
+ return symbols.flatMap(
8830
+ (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 }))
8831
+ );
8832
+ };
8833
+
8834
+ // src/gate/checks/callers.ts
8835
+ var MAX_SITES = 10;
8836
+ var byName = (sites) => {
8837
+ const grouped = /* @__PURE__ */ new Map();
8838
+ for (const site of sites) grouped.set(site.name, [...grouped.get(site.name) ?? [], site]);
8839
+ return grouped;
8840
+ };
8841
+ var siteNotes = (open) => [
8842
+ ...[...open].map(([name, sites]) => note("gate.callers.open", { name, count: sites.length })),
8843
+ ...[...open.values()].flat().slice(0, MAX_SITES).map((site) => note("gate.callers.at", { name: site.name, file: site.file, line: site.line }))
8844
+ ];
8845
+ var record = (ctx, event, names) => {
8846
+ if (names.length > 0) logEvent(ctx.runtime, ctx.paths, event, { task: ctx.contract.id, names: [...names] });
8847
+ };
8848
+ var callersCheck = (_check, ctx) => {
8849
+ const symbols = removedSymbols(ctx.runtime, ctx.git, ctx.base, ctx.changes);
8850
+ const grouped = byName(callSites(ctx.runtime, ctx.git, symbols));
8851
+ const ignore = new Set(ctx.contract.callersIgnore ?? []);
8852
+ const open = new Map([...grouped].filter(([name]) => !ignore.has(name)));
8853
+ record(
8854
+ ctx,
8855
+ RELEASED_EVENT,
8856
+ [...grouped.keys()].filter((name) => ignore.has(name))
8857
+ );
8858
+ record(ctx, FLAGGED_EVENT, [...open.keys()]);
8859
+ if (open.size === 0) return passed([note("gate.callers.none", { count: symbols.length })]);
8860
+ const rate = callersRate(ctx.runtime, ctx.paths);
8861
+ if (!rate.downgraded) return failed(siteNotes(open));
8862
+ return passed([
8863
+ note("gate.callers.warning", {
8864
+ percent: Math.round(rate.rate * 100),
8865
+ released: rate.released,
8866
+ total: rate.flagged
8867
+ }),
8868
+ ...siteNotes(open)
8869
+ ]);
8870
+ };
8871
+
8083
8872
  // src/gate/test-summary.ts
8084
8873
  var ANSI2 = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
8085
8874
  var COUNTERS = [
@@ -8276,13 +9065,13 @@ var manualCheck = (check2, ctx) => {
8276
9065
  };
8277
9066
 
8278
9067
  // src/gate/checks/placeholders.ts
8279
- import { join as join15 } from "node:path";
9068
+ import { join as join17 } from "node:path";
8280
9069
  var MAX_LISTED2 = 10;
8281
9070
  var SKIP_FILES = /\.(md|mdx|txt|rst|lock|snap|svg|png|jpg|gif|json)$|(^|\/)(CHANGELOG|LICENSE)/i;
8282
9071
  var PLACEHOLDER = /\b(TODO|FIXME|XXX)\b|not implemented|NotImplementedError|unimplemented!|todo!\(|lorem ipsum/i;
8283
9072
  var findPlaceholders = (lines3) => lines3.filter((line2) => !isTestFile(line2.path) && !SKIP_FILES.test(line2.path) && PLACEHOLDER.test(line2.text));
8284
9073
  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)));
9074
+ const text6 = ctx.runtime.readText(toAbsolutePath(join17(ctx.paths.root, change.path)));
8286
9075
  return text6.ok ? text6.value.split("\n").map((line2, index) => ({ path: change.path, line: index + 1, text: line2 })) : [];
8287
9076
  });
8288
9077
  var placeholdersCheck = (_check, ctx) => {
@@ -8931,9 +9720,9 @@ var isFlagged = (report2) => report2.spans.length > 0;
8931
9720
  var MARKDOWN = /\.(?:md|markdown|mdx)$/i;
8932
9721
  var RECORD_FILE = /(?:^|\/)(?:CHANGELOG|RELEASE[-_]?NOTES|HISTORY)[^/]*$/i;
8933
9722
  var contextOf = (file) => RECORD_FILE.test(toPosixPath(file)) ? "record" : "docs";
8934
- var listed = (runtime, root, args) => {
8935
- const result = runtime.git(args, root);
8936
- if (!result.ok) return reject(`git ${args.join(" ")} failed`);
9723
+ var listed = (runtime, root, args2) => {
9724
+ const result = runtime.git(args2, root);
9725
+ if (!result.ok) return reject(`git ${args2.join(" ")} failed`);
8937
9726
  return accept(result.stdout.split("\0").map(toPosixPath));
8938
9727
  };
8939
9728
  var changedMarkdown = (runtime, root) => {
@@ -9013,16 +9802,131 @@ var reviewCheck = (check2, ctx) => {
9013
9802
  return failed([...sizeNotes(ctx), note("gate.review.missing", { check: check2.id })]);
9014
9803
  };
9015
9804
 
9805
+ // src/app/parse.ts
9806
+ var KINDS2 = [
9807
+ "ok",
9808
+ "crashed",
9809
+ "no_port",
9810
+ "wrong_status",
9811
+ "body_mismatch",
9812
+ "request_failed",
9813
+ "no_start"
9814
+ ];
9815
+ var optionalNumber = (value) => typeof value === "number" ? value : void 0;
9816
+ var parseAppCheck = (stdout) => {
9817
+ const envelope = parseJsonObject(stdout.trim());
9818
+ if (!envelope.ok) return void 0;
9819
+ const data = envelope.value["data"];
9820
+ if (!isRecord(data)) return void 0;
9821
+ const kind = KINDS2.find((item) => item === data["kind"]);
9822
+ const { output, reused, stopped, durationMs, exitCode, bodyExcerpt } = data;
9823
+ if (kind === void 0 || typeof output !== "string" || typeof reused !== "boolean" || typeof stopped !== "boolean")
9824
+ return void 0;
9825
+ const status = optionalNumber(data["status"]);
9826
+ return {
9827
+ kind,
9828
+ output,
9829
+ reused,
9830
+ stopped,
9831
+ durationMs: optionalNumber(durationMs) ?? 0,
9832
+ ...status === void 0 ? {} : { status },
9833
+ ...typeof exitCode === "number" || exitCode === null ? { exitCode } : {},
9834
+ ...typeof bodyExcerpt === "string" ? { bodyExcerpt } : {}
9835
+ };
9836
+ };
9837
+
9838
+ // src/gate/checks/runtime.ts
9839
+ var PROBE_MARGIN_MS = 15e3;
9840
+ var MIN_REMAINING_MS2 = 5e3;
9841
+ var TAIL2 = 160;
9842
+ var lastLine = (text6) => {
9843
+ const rows = text6.split(/\r?\n/).filter((row) => row.trim() !== "" && !/^\s*at\s|^Node\.js v\d/.test(row));
9844
+ const line2 = (rows.findLast((row) => /error/i.test(row)) ?? rows.at(-1))?.trim() ?? "";
9845
+ return line2.length > TAIL2 ? `${line2.slice(0, TAIL2)}…` : line2;
9846
+ };
9847
+ var args = (check2, timeoutS) => [
9848
+ "app",
9849
+ "check",
9850
+ "--json",
9851
+ "--port",
9852
+ String(check2.port ?? 0),
9853
+ "--route",
9854
+ check2.route ?? DEFAULT_ROUTE,
9855
+ "--status",
9856
+ String(check2.status ?? DEFAULT_STATUS),
9857
+ "--timeout",
9858
+ String(timeoutS),
9859
+ ...check2.run === void 0 ? [] : ["--start", check2.run],
9860
+ ...check2.body === void 0 ? [] : ["--body", check2.body]
9861
+ ];
9862
+ var resultNotes = (check2, result, ctx) => {
9863
+ const route = check2.route ?? DEFAULT_ROUTE;
9864
+ const line2 = ctx.mask(lastLine(result.output));
9865
+ const output = line2 === "" ? [] : [note("gate.note.output", { line: line2 })];
9866
+ switch (result.kind) {
9867
+ case "ok":
9868
+ return [note(result.reused ? "gate.runtime.reused" : "gate.runtime.ok", { route, status: result.status ?? 0 })];
9869
+ case "crashed":
9870
+ return [note("gate.runtime.crashed", { code: result.exitCode ?? -1 }), ...output];
9871
+ case "no_port":
9872
+ return [note("gate.runtime.no_port", { port: check2.port ?? 0 }), ...output];
9873
+ case "no_start":
9874
+ return [note("gate.runtime.no_start", { port: check2.port ?? 0 })];
9875
+ case "wrong_status":
9876
+ return [
9877
+ note("gate.runtime.wrong_status", {
9878
+ route,
9879
+ status: result.status ?? 0,
9880
+ expected: check2.status ?? DEFAULT_STATUS
9881
+ })
9882
+ ];
9883
+ case "body_mismatch":
9884
+ return [note("gate.runtime.body_mismatch", { route, body: ctx.mask(check2.body ?? "") })];
9885
+ case "request_failed":
9886
+ return [note("gate.runtime.request_failed", { route, detail: ctx.mask(result.bodyExcerpt ?? "") })];
9887
+ }
9888
+ };
9889
+ var outcome = (check2, result, ctx) => {
9890
+ const notes2 = [
9891
+ ...resultNotes(check2, result, ctx),
9892
+ ...result.stopped ? [] : [note("gate.runtime.orphan", { port: check2.port ?? 0 })]
9893
+ ];
9894
+ const base = result.kind === "ok" && result.stopped ? passed(notes2) : failed(notes2);
9895
+ return {
9896
+ ...base,
9897
+ ...check2.run === void 0 ? {} : { command: check2.run },
9898
+ exitCode: result.exitCode ?? (result.kind === "ok" ? 0 : null),
9899
+ stdoutSha256: sha256Hex(result.output),
9900
+ durationMs: result.durationMs
9901
+ };
9902
+ };
9903
+ var runtimeCheck = (check2, ctx) => {
9904
+ if (check2.port === void 0) return failed([note("gate.runtime.no_port_declared")]);
9905
+ if (check2.run !== void 0 && !ctx.trusted)
9906
+ return { ...unverified("untrusted", [note("gate.note.untrusted", { task: ctx.contract.id })]), command: check2.run };
9907
+ const remaining = ctx.deadline - nowMs(ctx.runtime) - PROBE_MARGIN_MS;
9908
+ if (remaining < MIN_REMAINING_MS2) return unverified("budget", [note("gate.note.budget")]);
9909
+ const timeoutS = Math.max(1, Math.min(check2.timeoutS ?? DEFAULT_APP_TIMEOUT_S, Math.floor(remaining / 1e3)));
9910
+ const run4 = ctx.runtime.execSelf(args(check2, timeoutS), ctx.paths.root, timeoutS * 1e3 + PROBE_MARGIN_MS);
9911
+ if (run4.timedOut) return unverified("timeout", [note("gate.note.timeout", { seconds: timeoutS })]);
9912
+ const result = parseAppCheck(run4.stdout);
9913
+ if (result === void 0) {
9914
+ return failed([note("gate.runtime.probe_failed", { line: ctx.mask(lastLine(`${run4.stdout}
9915
+ ${run4.stderr}`)) })]);
9916
+ }
9917
+ return outcome(check2, result, ctx);
9918
+ };
9919
+
9016
9920
  // src/secrets/baseline.ts
9017
- import { join as join16 } from "node:path";
9921
+ import { join as join18 } from "node:path";
9018
9922
  var BASELINE_FILE2 = ".finitemonkey/findings/baseline.json";
9019
9923
  var BASELINE_SCHEMA = "finitemonkey/secrets-baseline@1";
9020
9924
  var BASELINE_STATES = ["aceito", "falso-positivo"];
9021
9925
  var EXCEPTION_MAX_DAYS = 90;
9022
9926
  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];
9927
+ var baselinePath = (root) => toAbsolutePath(join18(root, BASELINE_FILE2));
9928
+ var text5 = (record3, key) => {
9929
+ const value = record3[key];
9026
9930
  return typeof value === "string" && value !== "" ? value : void 0;
9027
9931
  };
9028
9932
  var stateOf = (value) => BASELINE_STATES.find((state) => state === value);
@@ -9080,22 +9984,22 @@ var withoutFixed = (baseline, fixed) => {
9080
9984
  var acceptedFingerprints = (baseline, now) => new Set(baseline.entries.filter((entry2) => entry2.expiresAt >= now).map((entry2) => entry2.fingerprint));
9081
9985
 
9082
9986
  // src/secrets/incidents.ts
9083
- import { join as join17 } from "node:path";
9987
+ import { join as join19 } from "node:path";
9084
9988
  var INCIDENTS_DIR = ".finitemonkey/incidents";
9085
9989
  var INCIDENT_STATES = ["aberto", "exposicao-local", "rotacionado", "fechado"];
9086
9990
  var INCIDENT_SOURCES = ["tool-output", "prompt", "history", "scan"];
9087
9991
  var ID = /^inc-[0-9a-f]{12}$/;
9088
9992
  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];
9993
+ var incidentsDir = (root) => toAbsolutePath(join19(root, INCIDENTS_DIR));
9994
+ var incidentFile = (root, id) => toAbsolutePath(join19(incidentsDir(root), `${id}.json`));
9995
+ var pick3 = (options, value) => options.find((option) => option === value);
9996
+ var str2 = (record3, key) => {
9997
+ const value = record3[key];
9094
9998
  return typeof value === "string" ? value : void 0;
9095
9999
  };
9096
10000
  var parseEvent = (value) => {
9097
10001
  if (!isRecord(value)) return void 0;
9098
- const state = pick2(INCIDENT_STATES, value["state"]);
10002
+ const state = pick3(INCIDENT_STATES, value["state"]);
9099
10003
  const at = str2(value, "at");
9100
10004
  const by = str2(value, "by");
9101
10005
  return state === void 0 || at === void 0 || by === void 0 ? void 0 : { state, at, by };
@@ -9108,12 +10012,12 @@ var parseHistory = (value) => {
9108
10012
  var parseIncident = (raw) => {
9109
10013
  const parsed = parseJsonObject(raw);
9110
10014
  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");
10015
+ const record3 = parsed.value;
10016
+ const [id, rule, where, fingerprint, createdAt, updatedAt] = ["id", "rule", "where", "fingerprint", "createdAt", "updatedAt"].map((key) => str2(record3, key));
10017
+ const state = pick3(INCIDENT_STATES, record3["state"]);
10018
+ const source = pick3(INCIDENT_SOURCES, record3["source"]);
10019
+ const history = parseHistory(record3["history"]);
10020
+ const firstCommit = str2(record3, "firstCommit");
9117
10021
  if (id === void 0 || !ID.test(id) || rule === void 0 || where === void 0) return reject("incomplete");
9118
10022
  if (fingerprint === void 0 || createdAt === void 0 || updatedAt === void 0) return reject("incomplete");
9119
10023
  if (state === void 0 || source === void 0 || history === void 0) return reject("incomplete");
@@ -9142,7 +10046,7 @@ var listIncidents = (runtime, root) => {
9142
10046
  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
10047
  };
9144
10048
  var saveIncident = (runtime, root, incident) => {
9145
- const ignore = runtime.writeText(toAbsolutePath(join17(incidentsDir(root), ".gitignore")), "*\n");
10049
+ const ignore = runtime.writeText(toAbsolutePath(join19(incidentsDir(root), ".gitignore")), "*\n");
9146
10050
  if (!ignore.ok) return ignore;
9147
10051
  return runtime.writeText(incidentFile(root, incident.id), `${JSON.stringify(incident, null, 2)}
9148
10052
  `);
@@ -9189,7 +10093,7 @@ var moveIncident = (incident, to, now, by) => ({
9189
10093
  var isRotated = (incident) => incident.state === "rotacionado" || incident.state === "fechado";
9190
10094
 
9191
10095
  // src/secrets/scan.ts
9192
- import { join as join18, relative as relative4 } from "node:path";
10096
+ import { join as join20, relative as relative4 } from "node:path";
9193
10097
 
9194
10098
  // src/secrets/located.ts
9195
10099
  var findingId = (ruleId, file, fingerprint) => `fm-${sha256Hex(`${ruleId}\0${file}\0${fingerprint}`).slice(0, 16)}`;
@@ -9289,7 +10193,7 @@ var listed2 = (runtime, root, target) => {
9289
10193
  const base = toAbsolutePath(target, root);
9290
10194
  const walked = runtime.walkFiles(base);
9291
10195
  if (!walked.ok) return walked;
9292
- return accept(walked.value.map((name) => toPosixPath(relative4(root, join18(base, toPosixPath(name))))));
10196
+ return accept(walked.value.map((name) => toPosixPath(relative4(root, join20(base, toPosixPath(name))))));
9293
10197
  };
9294
10198
  var scanFile = (runtime, root, file) => {
9295
10199
  const text6 = runtime.readText(toAbsolutePath(file, root));
@@ -9307,15 +10211,15 @@ var diffFindings = (diff) => {
9307
10211
  return scanner.end().filter((finding3) => !SKIPPED.test(finding3.file));
9308
10212
  };
9309
10213
  var scanStaged = (runtime, root, includeUnstaged = false) => {
9310
- const args = includeUnstaged ? ["diff", "HEAD", ...DIFF_FLAGS] : ["diff", "--cached", ...DIFF_FLAGS];
9311
- const git = runtime.git(args, root);
10214
+ const args2 = includeUnstaged ? ["diff", "HEAD", ...DIFF_FLAGS] : ["diff", "--cached", ...DIFF_FLAGS];
10215
+ const git = runtime.git(args2, root);
9312
10216
  if (!git.ok) return reject(git.found ? "git diff failed" : "git not found");
9313
10217
  return accept(diffFindings(git.stdout));
9314
10218
  };
9315
10219
  var scanHistory = async (runtime, root) => {
9316
10220
  const scanner = diffScanner();
9317
- const args = ["log", "-p", "--all", "--format=commit %H", ...DIFF_FLAGS];
9318
- const git = await runtime.gitLines(args, root, scanner.push);
10221
+ const args2 = ["log", "-p", "--all", "--format=commit %H", ...DIFF_FLAGS];
10222
+ const git = await runtime.gitLines(args2, root, scanner.push);
9319
10223
  if (!git.ok) return reject(git.found ? "git log failed" : "git not found");
9320
10224
  return accept(scanner.end().filter((finding3) => !SKIPPED.test(finding3.file)));
9321
10225
  };
@@ -9331,11 +10235,11 @@ var secretsGateStatus = (runtime, root, since) => {
9331
10235
  if (!changed.ok) return changed;
9332
10236
  const baseline = loadBaseline(runtime, root);
9333
10237
  if (!baseline.ok) return baseline;
9334
- const outcome = ratchet([...changed.value, ...untracked2(runtime, root)], baseline.value, runtime.now(), void 0);
10238
+ const outcome2 = ratchet([...changed.value, ...untracked2(runtime, root)], baseline.value, runtime.now(), void 0);
9335
10239
  const openIncidents = listIncidents(runtime, root).filter(
9336
10240
  (incident) => incident.createdAt >= since && incident.state !== "rotacionado" && incident.state !== "fechado"
9337
10241
  );
9338
- return accept({ findings: [...outcome.fresh, ...outcome.expired], openIncidents });
10242
+ return accept({ findings: [...outcome2.fresh, ...outcome2.expired], openIncidents });
9339
10243
  };
9340
10244
 
9341
10245
  // src/gate/checks/secrets.ts
@@ -9359,7 +10263,7 @@ var secretsCheck = (_check, ctx) => {
9359
10263
  };
9360
10264
 
9361
10265
  // src/gate/checks/tests.ts
9362
- import { join as join19 } from "node:path";
10266
+ import { join as join21 } from "node:path";
9363
10267
 
9364
10268
  // src/guards/tests/tree.ts
9365
10269
  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 +10295,7 @@ var treeViolations = (ctx) => {
9391
10295
  changes: relevant,
9392
10296
  before: (path) => fileAt(ctx.git, ctx.base, path),
9393
10297
  after: (path) => {
9394
- const text6 = ctx.runtime.readText(toAbsolutePath(join19(ctx.paths.root, path)));
10298
+ const text6 = ctx.runtime.readText(toAbsolutePath(join21(ctx.paths.root, path)));
9395
10299
  return text6.ok ? text6.value : void 0;
9396
10300
  },
9397
10301
  acceptedConfigs: ctx.trust.configs,
@@ -9407,12 +10311,12 @@ var testsCheck = (check2, ctx) => {
9407
10311
  const released2 = grant === void 0 ? {} : { releasedBy: `person@${grant.grantedAt}` };
9408
10312
  const releaseNotes = grant === void 0 ? [] : [...notes2, note("gate.note.released", { guard: "tests", reason: grant.reason })];
9409
10313
  if (check2.run === void 0) return { ...passed([note("gate.tests.clean"), ...releaseNotes]), ...released2 };
9410
- const outcome = runShellCheck(check2, check2.run, ctx);
9411
- return { ...outcome, notes: [...outcome.notes, ...releaseNotes], ...released2 };
10314
+ const outcome2 = runShellCheck(check2, check2.run, ctx);
10315
+ return { ...outcome2, notes: [...outcome2.notes, ...releaseNotes], ...released2 };
9412
10316
  };
9413
10317
 
9414
10318
  // src/git/tree.ts
9415
- import { join as join20 } from "node:path";
10319
+ import { join as join22 } from "node:path";
9416
10320
 
9417
10321
  // src/secrets/guards/paths.ts
9418
10322
  var SAFE_ENV = /^\.env\.(?:example|sample|template|dist|defaults|schema)$/i;
@@ -9434,13 +10338,13 @@ var IDENTITY_ENV = {
9434
10338
  GIT_COMMITTER_EMAIL: "finitemonkey@localhost"
9435
10339
  };
9436
10340
  var failure = (step3, result) => result.timedOut ? `${step3}: timed out` : `${step3}: ${result.stderr.trim() || "git failed"}`;
9437
- var run = (runtime, repo, args, env, options, input) => {
9438
- const result = runtime.gitWith(args, repo.root, {
10341
+ var run = (runtime, repo, args2, env, options, input) => {
10342
+ const result = runtime.gitWith(args2, repo.root, {
9439
10343
  env,
9440
10344
  timeoutMs: options.timeoutMs,
9441
10345
  ...input === void 0 ? {} : { input }
9442
10346
  });
9443
- return result.ok ? accept(result.stdout.trim()) : reject(failure(args[0] ?? "git", result));
10347
+ return result.ok ? accept(result.stdout.trim()) : reject(failure(args2[0] ?? "git", result));
9444
10348
  };
9445
10349
  var dropSensitive = (runtime, repo, env, options) => {
9446
10350
  const listed4 = run(runtime, repo, ["ls-files", "-z"], env, options);
@@ -9451,7 +10355,7 @@ var dropSensitive = (runtime, repo, env, options) => {
9451
10355
  return removed.ok ? accept(void 0) : removed;
9452
10356
  };
9453
10357
  var withTempIndex = (runtime, repo, options, work) => {
9454
- const path = toAbsolutePath(join20(repo.gitDir, `finitemonkey-${options.stamp}.index`));
10358
+ const path = toAbsolutePath(join22(repo.gitDir, `finitemonkey-${options.stamp}.index`));
9455
10359
  const env = { GIT_INDEX_FILE: path, GIT_OPTIONAL_LOCKS: "0" };
9456
10360
  try {
9457
10361
  return work(env);
@@ -9503,7 +10407,7 @@ var flushHunk = (draft) => {
9503
10407
  if (draft.current !== void 0) draft.hunks.push(draft.current);
9504
10408
  draft.current = void 0;
9505
10409
  };
9506
- var finish = (draft, out) => {
10410
+ var finish2 = (draft, out) => {
9507
10411
  if (draft === void 0) return;
9508
10412
  flushHunk(draft);
9509
10413
  out.push({
@@ -9563,7 +10467,7 @@ var parseUnifiedDiff = (text6) => {
9563
10467
  const line2 = withoutCarriageReturn(raw);
9564
10468
  const start = DIFF_GIT.exec(line2);
9565
10469
  if (start !== null) {
9566
- finish(draft, out);
10470
+ finish2(draft, out);
9567
10471
  draft = {
9568
10472
  oldPath: start[1],
9569
10473
  newPath: start[2],
@@ -9574,7 +10478,7 @@ var parseUnifiedDiff = (text6) => {
9574
10478
  };
9575
10479
  } else if (draft !== void 0) bodyLine(draft, line2);
9576
10480
  }
9577
- finish(draft, out);
10481
+ finish2(draft, out);
9578
10482
  return out;
9579
10483
  };
9580
10484
  var DIFF_ARGS = [
@@ -9684,26 +10588,26 @@ var entryOf2 = (kind, xy, path) => ({
9684
10588
  staged: kind !== "untracked" && !xy.startsWith("."),
9685
10589
  unstaged: kind === "untracked" || xy.charAt(1) !== "."
9686
10590
  });
9687
- var afterFields = (record2, count4) => record2.split(" ").slice(count4).join(" ");
10591
+ var afterFields = (record3, count4) => record3.split(" ").slice(count4).join(" ");
9688
10592
  var parsePorcelainV2 = (stdout) => {
9689
10593
  const records = stdout.split("\0");
9690
10594
  const out = [];
9691
10595
  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)));
10596
+ const record3 = records[index] ?? "";
10597
+ const xy = record3.split(" ")[1] ?? "..";
10598
+ if (record3.startsWith("1 ")) out.push(entryOf2("changed", xy, afterFields(record3, PORCELAIN_FIELDS.ordinary)));
10599
+ else if (record3.startsWith("2 ")) {
10600
+ out.push(entryOf2("changed", xy, afterFields(record3, PORCELAIN_FIELDS.renamed)));
9697
10601
  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)));
10602
+ } else if (record3.startsWith("u ")) {
10603
+ out.push(entryOf2("unmerged", xy, afterFields(record3, PORCELAIN_FIELDS.unmerged)));
10604
+ } else if (record3.startsWith("? ")) out.push(entryOf2("untracked", "??", record3.slice(2)));
9701
10605
  }
9702
10606
  return out.filter((entry2) => entry2.path !== "" && !entry2.path.startsWith(STATE_PREFIX));
9703
10607
  };
9704
10608
 
9705
10609
  // src/git/merge/dropped.ts
9706
- import { join as join21 } from "node:path";
10610
+ import { join as join23 } from "node:path";
9707
10611
 
9708
10612
  // src/git/merge/lines.ts
9709
10613
  var normalizeLine = (line2) => line2.replace(/\r$/, "").trim().replace(/\s*,$/, "");
@@ -9783,7 +10687,7 @@ var parseDropped = (text6) => {
9783
10687
  return accept({ items, invalid });
9784
10688
  };
9785
10689
  var loadDropped = (runtime, root, file) => {
9786
- const path = toAbsolutePath(file ?? join21(root, DROPPED_FILE), root);
10690
+ const path = toAbsolutePath(file ?? join23(root, DROPPED_FILE), root);
9787
10691
  const text6 = runtime.readText(path);
9788
10692
  if (!text6.ok) return file === void 0 ? accept({ items: [], invalid: [] }) : reject(`${file}: ${text6.error}`);
9789
10693
  return parseDropped(text6.value);
@@ -9793,7 +10697,7 @@ var isDropped = (finding3, items) => items.some(
9793
10697
  );
9794
10698
 
9795
10699
  // src/git/merge/generated.ts
9796
- import { join as join22 } from "node:path";
10700
+ import { join as join24 } from "node:path";
9797
10701
  var LOCKFILES3 = /* @__PURE__ */ new Set([
9798
10702
  "package-lock.json",
9799
10703
  "npm-shrinkwrap.json",
@@ -9856,7 +10760,7 @@ var verifyGenerated = (runtime, repo, inputs, paths, generators) => {
9856
10760
  if (declared.length === 0) return accept([]);
9857
10761
  const commit2 = resultCommit(runtime, repo, inputs);
9858
10762
  if (!commit2.ok) return commit2;
9859
- const dir = join22(repo.gitDir, `finitemonkey-generate-${runtime.now().replace(/[:.]/g, "-")}`);
10763
+ const dir = join24(repo.gitDir, `finitemonkey-generate-${runtime.now().replace(/[:.]/g, "-")}`);
9860
10764
  if (!runtime.git(["worktree", "add", "--detach", dir, commit2.value], repo.root).ok)
9861
10765
  return reject("could not check out the merge result");
9862
10766
  try {
@@ -9866,7 +10770,7 @@ var verifyGenerated = (runtime, repo, inputs, paths, generators) => {
9866
10770
  }
9867
10771
  return accept(
9868
10772
  declared.flatMap(({ path }) => {
9869
- const fresh3 = runtime.readText(toAbsolutePath(join22(dir, path)));
10773
+ const fresh3 = runtime.readText(toAbsolutePath(join24(dir, path)));
9870
10774
  const merged = blobText(runtime, repo, commit2.value, path);
9871
10775
  if (fresh3.ok && fresh3.value === merged) return [];
9872
10776
  return [{ kind: "generated-differs", file: path, side: void 0, line: void 0, text: path, weak: false }];
@@ -10080,16 +10984,16 @@ var failed2 = (error, inputs) => ({
10080
10984
  generated: [],
10081
10985
  error
10082
10986
  });
10083
- var keyOf = (diff) => (diff.status === "added" || diff.status === "copied" ? diff.newPath : diff.oldPath ?? diff.newPath) ?? "";
10987
+ var keyOf2 = (diff) => (diff.status === "added" || diff.status === "copied" ? diff.newPath : diff.oldPath ?? diff.newPath) ?? "";
10084
10988
  var pairsOf = (ours, theirs) => {
10085
- const keys = [...new Set([...ours, ...theirs].map(keyOf))].filter((key) => key !== "").sort();
10989
+ const keys = [...new Set([...ours, ...theirs].map(keyOf2))].filter((key) => key !== "").sort();
10086
10990
  return keys.map((key) => ({
10087
10991
  key,
10088
- ours: ours.find((diff) => keyOf(diff) === key),
10089
- theirs: theirs.find((diff) => keyOf(diff) === key)
10992
+ ours: ours.find((diff) => keyOf2(diff) === key),
10993
+ theirs: theirs.find((diff) => keyOf2(diff) === key)
10090
10994
  }));
10091
10995
  };
10092
- var contextOf2 = (runtime, repo, inputs, pairs) => {
10996
+ var contextOf2 = (runtime, repo, inputs, pairs2) => {
10093
10997
  const resultText = memoBy((path) => blobText(runtime, repo, inputs.result, path));
10094
10998
  const context = {
10095
10999
  runtime,
@@ -10097,7 +11001,7 @@ var contextOf2 = (runtime, repo, inputs, pairs) => {
10097
11001
  inputs,
10098
11002
  resultText,
10099
11003
  resultFiles: memo2(
10100
- () => pairs.flatMap((pair) => {
11004
+ () => pairs2.flatMap((pair) => {
10101
11005
  const path = resultPathOf(context, pair);
10102
11006
  return path === void 0 ? [] : [path];
10103
11007
  })
@@ -10114,24 +11018,24 @@ var verifyMerge = (runtime, repo, options = {}) => {
10114
11018
  const theirs = sideDiff(runtime, repo, inputs.value.base, inputs.value.theirs);
10115
11019
  if (!ours.ok) return failed2(ours.error, inputs.value);
10116
11020
  if (!theirs.ok) return failed2(theirs.error, inputs.value);
10117
- const pairs = pairsOf(ours.value, theirs.value);
11021
+ const pairs2 = pairsOf(ours.value, theirs.value);
10118
11022
  const generators = configMap(loadProjectConfig(runtime, repo.root), "merge", "generators");
10119
11023
  const generated = generatedPaths(
10120
11024
  runtime,
10121
11025
  repo,
10122
- pairs.map((pair) => pair.key),
11026
+ pairs2.map((pair) => pair.key),
10123
11027
  generators
10124
11028
  );
10125
11029
  const context = contextOf2(
10126
11030
  runtime,
10127
11031
  repo,
10128
11032
  inputs.value,
10129
- pairs.filter((pair) => !generated.has(pair.key))
11033
+ pairs2.filter((pair) => !generated.has(pair.key))
10130
11034
  );
10131
11035
  const regenerated = verifyGenerated(runtime, repo, inputs.value, [...generated], generators);
10132
11036
  if (!regenerated.ok) return failed2(regenerated.error, inputs.value);
10133
11037
  const all = [
10134
- ...pairs.filter((pair) => !generated.has(pair.key)).flatMap((pair) => verifyFile(context, pair)),
11038
+ ...pairs2.filter((pair) => !generated.has(pair.key)).flatMap((pair) => verifyFile(context, pair)),
10135
11039
  ...regenerated.value
10136
11040
  ];
10137
11041
  const open = [...dropped.value.invalid, ...all.filter((item) => !isDropped(item, dropped.value.items))];
@@ -10143,7 +11047,7 @@ var verifyMerge = (runtime, repo, options = {}) => {
10143
11047
  theirs: inputs.value.theirs,
10144
11048
  result: inputs.value.result,
10145
11049
  bases: inputs.value.bases,
10146
- files: pairs.length,
11050
+ files: pairs2.length,
10147
11051
  findings: open,
10148
11052
  justified: all.length - (open.length - dropped.value.invalid.length),
10149
11053
  generated: [...generated].sort(),
@@ -10152,19 +11056,19 @@ var verifyMerge = (runtime, repo, options = {}) => {
10152
11056
  };
10153
11057
 
10154
11058
  // src/git/ownership.ts
10155
- import { join as join23 } from "node:path";
11059
+ import { join as join25 } from "node:path";
10156
11060
  var OWNERSHIP_DIR = ".finitemonkey/git";
10157
11061
  var ownerKey = (raw) => {
10158
11062
  const cleaned = (raw ?? "").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80);
10159
11063
  return cleaned === "" ? "unknown" : cleaned;
10160
11064
  };
10161
- var fileOf = (repo, kind, owner) => toAbsolutePath(join23(repo.root, OWNERSHIP_DIR, kind, `${ownerKey(owner)}.json`));
11065
+ var fileOf = (repo, kind, owner) => toAbsolutePath(join25(repo.root, OWNERSHIP_DIR, kind, `${ownerKey(owner)}.json`));
10162
11066
  var readDirty = (runtime, root) => {
10163
11067
  const status = runtime.git(["status", "--porcelain=v2", "-z", "--untracked-files=all"], root);
10164
11068
  return status.ok ? parsePorcelainV2(status.stdout) : void 0;
10165
11069
  };
10166
11070
  var hashDirty = (runtime, repo, entries) => {
10167
- const present2 = entries.filter((entry2) => runtime.stat(toAbsolutePath(join23(repo.root, entry2.path)))?.kind === "file");
11071
+ const present2 = entries.filter((entry2) => runtime.stat(toAbsolutePath(join25(repo.root, entry2.path)))?.kind === "file");
10168
11072
  const hashed = present2.length === 0 ? void 0 : runtime.gitWith(["hash-object", "--stdin-paths"], repo.root, {
10169
11073
  input: `${present2.map((entry2) => entry2.path).join("\n")}
10170
11074
  `
@@ -10291,8 +11195,8 @@ var openRepo = (runtime, dir) => {
10291
11195
  };
10292
11196
  var globRegex2 = (pattern) => new RegExp(`^${pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`);
10293
11197
  var matchesBranch = (branch, patterns) => patterns.some((pattern) => globRegex2(pattern).test(branch));
10294
- var line = (runtime, root, args) => {
10295
- const result = runtime.git(args, root);
11198
+ var line = (runtime, root, args2) => {
11199
+ const result = runtime.git(args2, root);
10296
11200
  const value = result.stdout.trim();
10297
11201
  return result.ok && value !== "" ? value : void 0;
10298
11202
  };
@@ -10417,20 +11321,22 @@ var DEFAULT_CHECKS = {
10417
11321
  prose: proseCheck,
10418
11322
  worktree: worktreeCheck,
10419
11323
  merge: mergeCheck,
10420
- review: reviewCheck
11324
+ review: reviewCheck,
11325
+ callers: callersCheck,
11326
+ runtime: runtimeCheck
10421
11327
  };
10422
11328
 
10423
11329
  // src/gate/run.ts
10424
11330
  var FAST_PATH_MS = 10 * 6e4;
10425
- var toEvidence = (ctx, check2, outcome) => ({
11331
+ var toEvidence = (ctx, check2, outcome2) => ({
10426
11332
  schema: EVIDENCE_SCHEMA,
10427
11333
  task: ctx.contract.id,
10428
11334
  check: check2.id,
10429
11335
  kind: check2.kind,
10430
11336
  treeHash: ctx.treeHash,
10431
11337
  at: ctx.runtime.now(),
10432
- ...outcome,
10433
- ...check2.run === void 0 || outcome.command !== void 0 ? {} : { command: check2.run }
11338
+ ...outcome2,
11339
+ ...check2.run === void 0 || outcome2.command !== void 0 ? {} : { command: check2.run }
10434
11340
  });
10435
11341
  var reusable = (ledger, ctx, check2) => {
10436
11342
  if (!REUSABLE_KINDS.includes(check2.kind)) return false;
@@ -10611,7 +11517,7 @@ var subagentStopGate = (input) => {
10611
11517
  };
10612
11518
 
10613
11519
  // src/gate/hooks/subagent.ts
10614
- import { join as join24 } from "node:path";
11520
+ import { join as join26 } from "node:path";
10615
11521
  var NO_CONTRACT_AGENTS = /(^|:)(reviewer|verifier|explore|plan|claude-code-guide|statusline-setup)$/i;
10616
11522
  var KNOWN_KINDS = /* @__PURE__ */ new Set([
10617
11523
  "command",
@@ -10625,7 +11531,9 @@ var KNOWN_KINDS = /* @__PURE__ */ new Set([
10625
11531
  "prose",
10626
11532
  "worktree",
10627
11533
  "merge",
10628
- "review"
11534
+ "review",
11535
+ "callers",
11536
+ "runtime"
10629
11537
  ]);
10630
11538
  var requirementText = (t, check2) => {
10631
11539
  const key = `gate.require.${check2.kind}`;
@@ -10646,12 +11554,12 @@ var subagentStart = (input) => {
10646
11554
  const active = readState(runtime, paths).active;
10647
11555
  if (active === void 0) return { kind: "allow" };
10648
11556
  const disk = (relative9) => {
10649
- const text6 = runtime.readText(toAbsolutePath(join24(paths.root, relative9)));
11557
+ const text6 = runtime.readText(toAbsolutePath(join26(paths.root, relative9)));
10650
11558
  return text6.ok ? text6.value : void 0;
10651
11559
  };
10652
11560
  const loaded = loadTask(runtime, paths, { disk, base: () => void 0 }, active);
10653
11561
  if (!loaded.ok) return { kind: "allow" };
10654
- const briefing = runtime.stat(toAbsolutePath(join24(paths.dir, "briefing.md"))) !== void 0;
11562
+ const briefing = runtime.stat(toAbsolutePath(join26(paths.dir, "briefing.md"))) !== void 0;
10655
11563
  return { kind: "allow", output: contextOutput("SubagentStart", contractFacts(t, loaded.value.contract, briefing)) };
10656
11564
  };
10657
11565
 
@@ -10668,7 +11576,7 @@ var PORTAO_HANDLERS = {
10668
11576
  };
10669
11577
 
10670
11578
  // src/prod/guard.ts
10671
- import { join as join26 } from "node:path";
11579
+ import { join as join28 } from "node:path";
10672
11580
 
10673
11581
  // src/git/command-text.ts
10674
11582
  var MAX_SHOWN = 160;
@@ -10818,10 +11726,10 @@ var HELM_READ = /* @__PURE__ */ new Set([
10818
11726
  "env"
10819
11727
  ]);
10820
11728
  var CURL_WRITE = /^(?:-X|--request|-d|--data.*|-F|--form|-T|--upload-file|-o|--output|-O|--remote-name)$/;
10821
- var firstPositional = (args, valueFlags = /* @__PURE__ */ new Set()) => {
11729
+ var firstPositional = (args2, valueFlags = /* @__PURE__ */ new Set()) => {
10822
11730
  const out = [];
10823
- for (let index = 0; index < args.length; index += 1) {
10824
- const arg = args[index] ?? "";
11731
+ for (let index = 0; index < args2.length; index += 1) {
11732
+ const arg = args2[index] ?? "";
10825
11733
  if (arg.startsWith("-")) {
10826
11734
  if (valueFlags.has(arg)) index += 1;
10827
11735
  } else out.push(arg);
@@ -10856,16 +11764,16 @@ var KUBE_VALUE_FLAGS = /* @__PURE__ */ new Set([
10856
11764
  "--container",
10857
11765
  "--kube-context"
10858
11766
  ]);
10859
- var dockerReadOnly = (args) => {
10860
- const words = firstPositional(args, DOCKER_VALUE_FLAGS);
11767
+ var dockerReadOnly = (args2) => {
11768
+ const words = firstPositional(args2, DOCKER_VALUE_FLAGS);
10861
11769
  const [first, second] = words;
10862
11770
  if (first === void 0) return true;
10863
11771
  if (DOCKER_GROUPS.has(first))
10864
11772
  return second === void 0 || DOCKER_READ.has(second) || first === "compose" && second === "top";
10865
11773
  return DOCKER_READ.has(first);
10866
11774
  };
10867
- var kubectlReadOnly = (program, args) => {
10868
- const [verb, object] = firstPositional(args, KUBE_VALUE_FLAGS);
11775
+ var kubectlReadOnly = (program, args2) => {
11776
+ const [verb, object] = firstPositional(args2, KUBE_VALUE_FLAGS);
10869
11777
  if (verb === void 0) return true;
10870
11778
  if (program === "helm") return HELM_READ.has(verb);
10871
11779
  if (verb === "config") return object === "view" || object === "get-contexts" || object === "current-context";
@@ -10875,24 +11783,24 @@ var kubectlReadOnly = (program, args) => {
10875
11783
  var writesFile = (command) => command.redirects.some(
10876
11784
  (redirect) => (redirect.op === ">" || redirect.op === ">>" || redirect.op === ">|") && !/^(?:\/dev\/null|&?\d)$/.test(redirect.target) && !redirect.op.startsWith("<")
10877
11785
  );
10878
- var gitReadOnly = (args) => {
10879
- const [verb, ...rest] = firstPositional(args, /* @__PURE__ */ new Set(["-C", "-c"]));
11786
+ var gitReadOnly = (args2) => {
11787
+ const [verb, ...rest] = firstPositional(args2, /* @__PURE__ */ new Set(["-C", "-c"]));
10880
11788
  if (verb === void 0) return true;
10881
11789
  if (GIT_READ.has(verb)) return true;
10882
11790
  return GIT_LIST.has(verb) && (rest.length === 0 || verb === "stash" && rest[0] === "list");
10883
11791
  };
10884
11792
  var programReadOnly = (command) => {
10885
- const { program, args } = command;
10886
- if (program === "git") return gitReadOnly(args);
11793
+ const { program, args: args2 } = command;
11794
+ if (program === "git") return gitReadOnly(args2);
10887
11795
  if (program === "docker" || program === "docker-compose")
10888
- return dockerReadOnly(program === "docker-compose" ? ["compose", ...args] : args);
10889
- if (program === "systemctl") return SYSTEMCTL_READ.has(firstPositional(args)[0] ?? "status");
10890
- if (program === "kubectl" || program === "helm") return kubectlReadOnly(program, args);
10891
- if (program === "curl") return !args.some((arg) => CURL_WRITE.test(arg));
11796
+ return dockerReadOnly(program === "docker-compose" ? ["compose", ...args2] : args2);
11797
+ if (program === "systemctl") return SYSTEMCTL_READ.has(firstPositional(args2)[0] ?? "status");
11798
+ if (program === "kubectl" || program === "helm") return kubectlReadOnly(program, args2);
11799
+ if (program === "curl") return !args2.some((arg) => CURL_WRITE.test(arg));
10892
11800
  if (program === "find")
10893
- return !args.some((arg) => arg === "-delete" || arg.startsWith("-exec") || arg.startsWith("-ok"));
10894
- if (program === "sed") return !args.some((arg) => arg.startsWith("-i") || arg === "--in-place");
10895
- if (program === "awk") return !args.some((arg) => arg.includes("system(") || arg.includes(">"));
11801
+ return !args2.some((arg) => arg === "-delete" || arg.startsWith("-exec") || arg.startsWith("-ok"));
11802
+ if (program === "sed") return !args2.some((arg) => arg.startsWith("-i") || arg === "--in-place");
11803
+ if (program === "awk") return !args2.some((arg) => arg.includes("system(") || arg.includes(">"));
10896
11804
  return READ_PROGRAMS.has(program);
10897
11805
  };
10898
11806
  var readOnlyCommand = (command) => !writesFile(command) && programReadOnly(command);
@@ -10952,11 +11860,11 @@ var RSYNC_LONG_VALUE = /* @__PURE__ */ new Set([
10952
11860
  "--files-from"
10953
11861
  ]);
10954
11862
  var parseSsh = (command) => {
10955
- const { args } = command;
11863
+ const { args: args2 } = command;
10956
11864
  let index = 0;
10957
11865
  let noCommand = false;
10958
- while (index < args.length && (args[index] ?? "").startsWith("-")) {
10959
- const arg = args[index] ?? "";
11866
+ while (index < args2.length && (args2[index] ?? "").startsWith("-")) {
11867
+ const arg = args2[index] ?? "";
10960
11868
  if (arg === "--") {
10961
11869
  index += 1;
10962
11870
  break;
@@ -10965,17 +11873,17 @@ var parseSsh = (command) => {
10965
11873
  const last = arg.charAt(arg.length - 1);
10966
11874
  index += arg.length === 2 && SSH_VALUE.includes(last) ? 2 : 1;
10967
11875
  }
10968
- const destination = args[index];
11876
+ const destination = args2[index];
10969
11877
  if (destination === void 0) return void 0;
10970
- return { destination: destination.replace(/^ssh:\/\//, ""), remote: args.slice(index + 1), noCommand };
11878
+ return { destination: destination.replace(/^ssh:\/\//, ""), remote: args2.slice(index + 1), noCommand };
10971
11879
  };
10972
11880
  var REMOTE_PATH = /^(?:(?:scp|rsync|sftp):\/\/)?(?:[^@/:\s]+@)?(\[[^\]]+\]|[A-Za-z0-9][A-Za-z0-9.-]*)(?::|::|\/)/;
10973
11881
  var remoteHostOf = (operand) => {
10974
11882
  if (/^[A-Za-z]:[\\/]/.test(operand) || operand.startsWith("/") || operand.startsWith(".")) return void 0;
10975
- const url = /^(?:scp|rsync|sftp):\/\//.test(operand);
11883
+ const url2 = /^(?:scp|rsync|sftp):\/\//.test(operand);
10976
11884
  const match = REMOTE_PATH.exec(operand);
10977
11885
  if (match === null) return void 0;
10978
- if (!url && !operand.slice(match[0].length - 1).startsWith(":")) return void 0;
11886
+ if (!url2 && !operand.slice(match[0].length - 1).startsWith(":")) return void 0;
10979
11887
  return (match[1] ?? "").replace(/^\[|\]$/g, "");
10980
11888
  };
10981
11889
  var copyOperands = (command, shortValue) => {
@@ -11010,7 +11918,7 @@ var remoteHostsOf = (command) => {
11010
11918
  };
11011
11919
 
11012
11920
  // src/prod/targets.ts
11013
- import { join as join25 } from "node:path";
11921
+ import { join as join27 } from "node:path";
11014
11922
  var DEPLOY_FILES = [
11015
11923
  "deploy.sh",
11016
11924
  "scripts/deploy.sh",
@@ -11034,12 +11942,12 @@ var hostsInText = (text6) => {
11034
11942
  return [...urls, ...keyed, ...commands].filter((host) => host !== "" && !host.includes("$"));
11035
11943
  };
11036
11944
  var workflowFiles = (runtime, root) => {
11037
- const listed4 = runtime.listDir(toAbsolutePath(join25(root, ".github", "workflows")));
11945
+ const listed4 = runtime.listDir(toAbsolutePath(join27(root, ".github", "workflows")));
11038
11946
  if (!listed4.ok) return [];
11039
11947
  return listed4.value.filter((name) => /deploy/i.test(name) && /\.ya?ml$/i.test(name)).map((name) => `.github/workflows/${name}`);
11040
11948
  };
11041
11949
  var detectedHosts = (runtime, root, files) => files.flatMap((file) => {
11042
- const text6 = runtime.readText(toAbsolutePath(join25(root, file)));
11950
+ const text6 = runtime.readText(toAbsolutePath(join27(root, file)));
11043
11951
  return text6.ok ? hostsInText(text6.value) : [];
11044
11952
  });
11045
11953
  var loadProdTargets = (runtime, root, config) => {
@@ -11051,7 +11959,7 @@ var loadProdTargets = (runtime, root, config) => {
11051
11959
  ...detectedHosts(runtime, root, deployFiles)
11052
11960
  ];
11053
11961
  const declaredDeploy = configAt(config, "prod", "deploy");
11054
- const existing = deployFiles.find((file) => runtime.stat(toAbsolutePath(join25(root, file))) !== void 0);
11962
+ const existing = deployFiles.find((file) => runtime.stat(toAbsolutePath(join27(root, file))) !== void 0);
11055
11963
  return {
11056
11964
  hosts: new Set(hosts.map(hostKey).filter((host) => host !== "")),
11057
11965
  kubeContexts: configList(config, "prod", "kube_contexts"),
@@ -11080,12 +11988,12 @@ var copyHit = (command, isProd) => {
11080
11988
  const source = call.sources.map(remoteHostOf).find((host) => host !== void 0 && isProd(host));
11081
11989
  return source === void 0 ? void 0 : hit2("read", hostKey(source), command.program);
11082
11990
  };
11083
- var valueAfter = (args, ...names) => {
11084
- for (let index = 0; index < args.length; index += 1) {
11085
- const arg = args[index] ?? "";
11991
+ var valueAfter = (args2, ...names) => {
11992
+ for (let index = 0; index < args2.length; index += 1) {
11993
+ const arg = args2[index] ?? "";
11086
11994
  const equals = names.find((name) => arg.startsWith(`${name}=`));
11087
11995
  if (equals !== void 0) return arg.slice(equals.length + 1);
11088
- if (names.includes(arg)) return args[index + 1];
11996
+ if (names.includes(arg)) return args2[index + 1];
11089
11997
  }
11090
11998
  return void 0;
11091
11999
  };
@@ -11127,8 +12035,8 @@ var DUMPS = /* @__PURE__ */ new Set(["pg_dump", "mysqldump"]);
11127
12035
  var databaseHost = (command) => {
11128
12036
  const flag2 = valueAfter(command.args, "-h", "--host");
11129
12037
  if (flag2 !== void 0) return flag2;
11130
- const url = [...command.args, command.env["DATABASE_URL"] ?? ""].map((arg) => URL_HOST.exec(arg)?.[1]).find((host) => host !== void 0);
11131
- return url ?? command.env["PGHOST"];
12038
+ const url2 = [...command.args, command.env["DATABASE_URL"] ?? ""].map((arg) => URL_HOST.exec(arg)?.[1]).find((host) => host !== void 0);
12039
+ return url2 ?? command.env["PGHOST"];
11132
12040
  };
11133
12041
  var databaseReadOnly = (command) => {
11134
12042
  if (DUMPS.has(command.program)) return true;
@@ -11228,8 +12136,8 @@ var recordEvidence = (runtime, root, owner, reads) => {
11228
12136
  ] : []
11229
12137
  );
11230
12138
  if (lines3.length === 0) return;
11231
- const path = toAbsolutePath(join26(root, EVIDENCE_FILE));
11232
- runtime.mkdir(toAbsolutePath(join26(root, ".finitemonkey")));
12139
+ const path = toAbsolutePath(join28(root, EVIDENCE_FILE));
12140
+ runtime.mkdir(toAbsolutePath(join28(root, ".finitemonkey")));
11233
12141
  const before = runtime.readText(path);
11234
12142
  const text6 = before.ok ? before.value : "";
11235
12143
  runtime.writeAtomic(path, `${text6}${text6 === "" || text6.endsWith("\n") ? "" : "\n"}${lines3.join("\n")}
@@ -11302,14 +12210,14 @@ var globalOption = (globals, name, value) => {
11302
12210
  var gitInvocation = (command) => {
11303
12211
  if (command.program !== "git") return void 0;
11304
12212
  const globals = { dirs: [], config: {} };
11305
- const { args } = command;
12213
+ const { args: args2 } = command;
11306
12214
  let index = 0;
11307
- while (index < args.length && (args[index] ?? "").startsWith("-")) {
11308
- const arg = args[index] ?? "";
12215
+ while (index < args2.length && (args2[index] ?? "").startsWith("-")) {
12216
+ const arg = args2[index] ?? "";
11309
12217
  const equals = arg.indexOf("=");
11310
12218
  const name = equals > 2 && arg.startsWith("--") ? arg.slice(0, equals) : arg;
11311
12219
  if (WITH_VALUE.has(name) && equals < 0) {
11312
- globalOption(globals, name, args[index + 1] ?? "");
12220
+ globalOption(globals, name, args2[index + 1] ?? "");
11313
12221
  index += 2;
11314
12222
  } else {
11315
12223
  if (equals > 0) globalOption(globals, name, arg.slice(equals + 1));
@@ -11319,8 +12227,8 @@ var gitInvocation = (command) => {
11319
12227
  return {
11320
12228
  dirs: globals.dirs,
11321
12229
  config: globals.config,
11322
- subcommand: args[index] ?? "",
11323
- args: args.slice(index + 1),
12230
+ subcommand: args2[index] ?? "",
12231
+ args: args2.slice(index + 1),
11324
12232
  command
11325
12233
  };
11326
12234
  };
@@ -11331,7 +12239,7 @@ var withAlias = (invocation, expansion) => ({
11331
12239
  });
11332
12240
 
11333
12241
  // src/git/path-key.ts
11334
- import { posix as posix2 } from "node:path";
12242
+ import { posix as posix3 } from "node:path";
11335
12243
  var WSL_MOUNT3 = /^\/mnt\/([a-zA-Z])(\/|$)/;
11336
12244
  var DRIVE = /^([a-zA-Z]):(\/|$)/;
11337
12245
  var pathKey = (path) => {
@@ -11340,7 +12248,7 @@ var pathKey = (path) => {
11340
12248
  (_match, letter, slash) => `${letter}:${slash || "/"}`
11341
12249
  );
11342
12250
  const drive = DRIVE.exec(posixPath);
11343
- const normalized = posix2.normalize(drive === null ? posixPath : posixPath.slice(2) || "/");
12251
+ const normalized = posix3.normalize(drive === null ? posixPath : posixPath.slice(2) || "/");
11344
12252
  const trimmed = normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
11345
12253
  return drive === null ? trimmed : `${(drive[1] ?? "").toLowerCase()}:${trimmed}`.toLowerCase();
11346
12254
  };
@@ -11356,15 +12264,15 @@ var isRootLike = (path, home) => {
11356
12264
  if (key === "/" || /^[a-z]:\/?$/.test(key)) return true;
11357
12265
  return home !== void 0 && home !== "" && key === pathKey(home);
11358
12266
  };
11359
- var resolveFrom = (base, ...parts) => parts.reduce((current, part2) => {
11360
- if (part2 === void 0 || part2 === "") return current;
12267
+ var resolveFrom = (base, ...parts) => parts.reduce((current2, part2) => {
12268
+ if (part2 === void 0 || part2 === "") return current2;
11361
12269
  const next2 = toPosixPath(part2);
11362
12270
  if (next2.startsWith("/") || DRIVE.test(next2)) return next2;
11363
- return posix2.join(toPosixPath(current), next2);
12271
+ return posix3.join(toPosixPath(current2), next2);
11364
12272
  }, toPosixPath(base));
11365
12273
 
11366
12274
  // src/git/pathspec.ts
11367
- import { posix as posix3 } from "node:path";
12275
+ import { posix as posix4 } from "node:path";
11368
12276
  var MAGIC = /^:(?:\(([^)]*)\)|([!^/]*))(.*)$/s;
11369
12277
  var GLOB = /[*?[]/;
11370
12278
  var specOf = (raw, prefix) => {
@@ -11374,7 +12282,7 @@ var specOf = (raw, prefix) => {
11374
12282
  const rest = magic === null ? posixRaw : magic[3] ?? "";
11375
12283
  const exclude = /(?:^|,)exclude(?:,|$)|[!^]/.test(words);
11376
12284
  const top = /(?:^|,)top(?:,|$)|\//.test(words);
11377
- const joined = posix3.normalize(top ? rest : posix3.join(prefix === "" ? "." : prefix, rest));
12285
+ const joined = posix4.normalize(top ? rest : posix4.join(prefix === "" ? "." : prefix, rest));
11378
12286
  return { pattern: joined === "./" ? "." : joined.replace(/\/$/, ""), exclude };
11379
12287
  };
11380
12288
  var globRegex3 = (pattern) => {
@@ -11407,7 +12315,7 @@ var discard = (op, scope2, pathspecs = []) => ({
11407
12315
  discard: { scope: scope2, pathspecs }
11408
12316
  });
11409
12317
  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);
12318
+ var worst = (verdicts) => verdicts.reduce((current2, next2) => SEVERITY[next2.kind] > SEVERITY[current2.kind] ? next2 : current2, PASS);
11411
12319
 
11412
12320
  // src/git/options.ts
11413
12321
  var canonical = (name, spec2) => {
@@ -11454,14 +12362,14 @@ var shortCluster = (building, arg, next2, spec2) => {
11454
12362
  }
11455
12363
  return 1;
11456
12364
  };
11457
- var parseOptions = (args, spec2) => {
12365
+ var parseOptions = (args2, spec2) => {
11458
12366
  const building = { flags: /* @__PURE__ */ new Set(), values: /* @__PURE__ */ new Map(), positionals: [] };
11459
12367
  let index = 0;
11460
- while (index < args.length) {
11461
- const arg = args[index] ?? "";
11462
- const next2 = args[index + 1];
12368
+ while (index < args2.length) {
12369
+ const arg = args2[index] ?? "";
12370
+ const next2 = args2[index + 1];
11463
12371
  if (arg === "--") {
11464
- return { ...building, paths: args.slice(index + 1), dashdash: true };
12372
+ return { ...building, paths: args2.slice(index + 1), dashdash: true };
11465
12373
  }
11466
12374
  if (arg.startsWith("--")) index += longOption(building, arg, next2, spec2);
11467
12375
  else if (arg.startsWith("-") && arg.length > 1 && !/^-\d+$/.test(arg)) {
@@ -11768,24 +12676,24 @@ var branchName = (ref) => {
11768
12676
  if (ref.startsWith("refs/tags/")) return void 0;
11769
12677
  return ref.replace(/^refs\/heads\//, "");
11770
12678
  };
11771
- var refspecTarget = (refspec, current) => {
12679
+ var refspecTarget = (refspec, current2) => {
11772
12680
  const plain = refspec.replace(/^\+/, "");
11773
12681
  const colon = plain.indexOf(":");
11774
12682
  const destination = colon < 0 ? plain : plain.slice(colon + 1);
11775
- if (destination === "HEAD" || colon < 0 && plain === "HEAD") return current;
12683
+ if (destination === "HEAD" || colon < 0 && plain === "HEAD") return current2;
11776
12684
  if (destination === "" || destination.startsWith("refs/tags/")) return void 0;
11777
12685
  return branchName(destination);
11778
12686
  };
11779
12687
  var pushDestinations = (parsed, facts) => {
11780
12688
  if (hasAny(parsed, "--all", "--branches", "--mirror")) return "all";
11781
12689
  const refspecs = parsed.positionals.slice(1).filter((refspec) => refspec !== ":");
11782
- const current = facts.currentBranch();
12690
+ const current2 = facts.currentBranch();
11783
12691
  if (refspecs.length === 0) {
11784
- if (hasAny(parsed, "--tags") || current === void 0) return [];
11785
- return [facts.upstream(current)?.branch ?? current];
12692
+ if (hasAny(parsed, "--tags") || current2 === void 0) return [];
12693
+ return [facts.upstream(current2)?.branch ?? current2];
11786
12694
  }
11787
12695
  return refspecs.flatMap((refspec) => {
11788
- const target = refspecTarget(refspec, current);
12696
+ const target = refspecTarget(refspec, current2);
11789
12697
  return target === void 0 ? [] : [target];
11790
12698
  });
11791
12699
  };
@@ -11793,10 +12701,10 @@ var leaseVerdict = (parsed, facts) => {
11793
12701
  const leases = valuesOf(parsed, "--force-with-lease");
11794
12702
  const explicit = leases.length > 0 && leases.every((lease) => /^[^:]+:.+$/.test(lease));
11795
12703
  if (!explicit || !hasAny(parsed, "--force-if-includes")) return block2("force-push", "--force-with-lease");
11796
- const current = facts.currentBranch();
12704
+ const current2 = facts.currentBranch();
11797
12705
  const leased = leases.map((lease) => branchName(lease.slice(0, lease.indexOf(":"))));
11798
12706
  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);
12707
+ const own = current2 !== void 0 && !facts.isProtected(current2) && leased.every((name) => name === current2) && destinations !== "all" && destinations.every((name) => name === current2);
11800
12708
  return own ? PASS : block2("lease-not-own", leased.join(", "));
11801
12709
  };
11802
12710
  var pushVerdict = (invocation, facts) => {
@@ -11819,8 +12727,8 @@ var LEAVES = /* @__PURE__ */ new Set(["--abort", "--quit", "--skip", "--dry-run"
11819
12727
  var commitVerdict = (invocation, facts) => {
11820
12728
  if (invocation.args.some((arg) => LEAVES.has(arg))) return PASS;
11821
12729
  if (invocation.subcommand === "rebase") return PASS;
11822
- const current = facts.currentBranch();
11823
- return current !== void 0 && facts.isProtected(current) ? block2("protected-commit", current) : PASS;
12730
+ const current2 = facts.currentBranch();
12731
+ return current2 !== void 0 && facts.isProtected(current2) ? block2("protected-commit", current2) : PASS;
11824
12732
  };
11825
12733
  var pushVerdict2 = (invocation, facts) => {
11826
12734
  const parsed = parseOptions(invocation.args, PUSH_OPTIONS);
@@ -11899,9 +12807,9 @@ var gitVerdict = (invocation, facts) => {
11899
12807
 
11900
12808
  // src/git/switch-target.ts
11901
12809
  var CREATES = /* @__PURE__ */ new Set(["-c", "-C", "-b", "-B", "--create", "--force-create", "--orphan"]);
11902
- var operands = (args) => {
11903
- if (args.includes("--")) return void 0;
11904
- return args.filter((arg) => !arg.startsWith("-"));
12810
+ var operands = (args2) => {
12811
+ if (args2.includes("--")) return void 0;
12812
+ return args2.filter((arg) => !arg.startsWith("-"));
11905
12813
  };
11906
12814
  var switchTarget = (invocation, facts) => {
11907
12815
  if (invocation.subcommand !== "switch" && invocation.subcommand !== "checkout") return void 0;
@@ -12041,33 +12949,33 @@ var listWorktrees = (runtime, cwd) => {
12041
12949
  };
12042
12950
 
12043
12951
  // src/git/journal.ts
12044
- import { join as join27 } from "node:path";
12952
+ import { join as join29 } from "node:path";
12045
12953
  var JOURNAL_FILE = ".finitemonkey/git-journal.jsonl";
12046
12954
  var JOURNAL_SCHEMA = "finitemonkey/git-journal@1";
12047
- var journalPath = (root) => toAbsolutePath(join27(root, JOURNAL_FILE));
12955
+ var journalPath = (root) => toAbsolutePath(join29(root, JOURNAL_FILE));
12048
12956
  var headsOf = (value) => isRecord(value) ? Object.fromEntries(
12049
12957
  Object.entries(value).filter((entry2) => typeof entry2[1] === "string")
12050
12958
  ) : {};
12051
12959
  var parseJournalLine = (line2) => {
12052
12960
  const parsed = parseJsonObject(line2);
12053
12961
  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");
12962
+ const record3 = parsed.value;
12963
+ const ref = optionalString(record3, "ref");
12964
+ const commit2 = optionalString(record3, "commit");
12965
+ const at = optionalString(record3, "at");
12058
12966
  if (ref === void 0 || commit2 === void 0 || at === void 0) return void 0;
12059
12967
  return {
12060
12968
  schema: JOURNAL_SCHEMA,
12061
12969
  at,
12062
- op: optionalString(record2, "op") ?? "",
12970
+ op: optionalString(record3, "op") ?? "",
12063
12971
  ref,
12064
12972
  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"])
12973
+ head: optionalString(record3, "head"),
12974
+ branch: optionalString(record3, "branch"),
12975
+ index: optionalString(record3, "index"),
12976
+ owner: optionalString(record3, "owner"),
12977
+ command: optionalString(record3, "command"),
12978
+ heads: headsOf(record3["heads"])
12071
12979
  };
12072
12980
  };
12073
12981
  var readJournal = (runtime, root) => {
@@ -12080,7 +12988,7 @@ var readJournal = (runtime, root) => {
12080
12988
  };
12081
12989
  var appendJournal = (runtime, root, entry2) => {
12082
12990
  const path = journalPath(root);
12083
- const made = runtime.mkdir(toAbsolutePath(join27(root, ".finitemonkey")));
12991
+ const made = runtime.mkdir(toAbsolutePath(join29(root, ".finitemonkey")));
12084
12992
  if (!made.ok) return made;
12085
12993
  const previous = runtime.readText(path);
12086
12994
  const before = previous.ok ? previous.value : "";
@@ -12095,8 +13003,8 @@ var BACKUP_PREFIX = "refs/finitemonkey/backup/";
12095
13003
  var DEFAULT_SNAPSHOT_TIMEOUT_MS = 2500;
12096
13004
  var stampOf = (iso) => iso.replace(/[:.]/g, "-");
12097
13005
  var slug = (op) => op.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "manual";
12098
- var optionalLine = (runtime, repo, args) => {
12099
- const result = runtime.git(args, repo.root);
13006
+ var optionalLine = (runtime, repo, args2) => {
13007
+ const result = runtime.git(args2, repo.root);
12100
13008
  const value = result.stdout.trim();
12101
13009
  return result.ok && value !== "" ? value : void 0;
12102
13010
  };
@@ -12201,17 +13109,17 @@ var fileList = (items) => {
12201
13109
  return items.length > MAX_FILES2 ? `${head} (+${String(items.length - MAX_FILES2)})` : head;
12202
13110
  };
12203
13111
  var foreignLabel = (t, file) => t(file.reason === "before-session" ? "git.foreign.before" : "git.foreign.other", { path: file.path });
12204
- var outcomeMessage = (t, outcome) => {
12205
- switch (outcome.kind) {
13112
+ var outcomeMessage = (t, outcome2) => {
13113
+ switch (outcome2.kind) {
12206
13114
  case "block":
12207
- return ruleMessage(t, outcome.rule, outcome.command, outcome.subject);
13115
+ return ruleMessage(t, outcome2.rule, outcome2.command, outcome2.subject);
12208
13116
  case "foreign":
12209
13117
  return t("git.block.foreign", {
12210
- command: outcome.command,
12211
- files: fileList(outcome.files.map((file) => foreignLabel(t, file)))
13118
+ command: outcome2.command,
13119
+ files: fileList(outcome2.files.map((file) => foreignLabel(t, file)))
12212
13120
  });
12213
13121
  case "unknown":
12214
- return t("git.block.unknown", { command: outcome.command });
13122
+ return t("git.block.unknown", { command: outcome2.command });
12215
13123
  case "pass":
12216
13124
  case "snapshot":
12217
13125
  return void 0;
@@ -12255,10 +13163,10 @@ var review = (input, commands, owner) => {
12255
13163
  const decision = PROD_PROGRAMS.has(command.program) ? prod().decide(command) : void 0;
12256
13164
  if (decision?.kind === "block") return { blocked: decision.reason, snapshots, reads };
12257
13165
  if (decision !== void 0) reads.push(decision);
12258
- for (const outcome of guardGitCommand({ runtime: input.runtime, cwd: input.cwd, owner, chain }, command)) {
12259
- const message = outcomeMessage(input.t, outcome);
13166
+ for (const outcome2 of guardGitCommand({ runtime: input.runtime, cwd: input.cwd, owner, chain }, command)) {
13167
+ const message = outcomeMessage(input.t, outcome2);
12260
13168
  if (message !== void 0) return { blocked: message, snapshots, reads };
12261
- if (outcome.kind === "snapshot") snapshots.push(outcome);
13169
+ if (outcome2.kind === "snapshot") snapshots.push(outcome2);
12262
13170
  }
12263
13171
  }
12264
13172
  return { blocked: void 0, snapshots, reads };
@@ -12322,6 +13230,83 @@ var GIT_HANDLERS = {
12322
13230
  PostToolUse: [gitPostToolUse]
12323
13231
  };
12324
13232
 
13233
+ // src/policy/guards.ts
13234
+ var SHADOW_FILE = "shadow.json";
13235
+ var numberOf = (value) => typeof value === "number" && Number.isInteger(value) ? value : 0;
13236
+ var readShadow = (runtime, paths) => {
13237
+ const text6 = runtime.readText(paths.cache(SHADOW_FILE));
13238
+ const parsed = text6.ok ? parseJsonObject(text6.value) : void 0;
13239
+ if (parsed?.ok !== true) return {};
13240
+ return Object.fromEntries(
13241
+ Object.entries(parsed.value).flatMap(
13242
+ ([id, value]) => isRecord(value) ? [
13243
+ [
13244
+ id,
13245
+ {
13246
+ firings: numberOf(value["firings"]),
13247
+ falsePositives: numberOf(value["falsePositives"]),
13248
+ promoted: value["promoted"] === true
13249
+ }
13250
+ ]
13251
+ ] : []
13252
+ )
13253
+ );
13254
+ };
13255
+ var writeShadow = (runtime, paths, state) => {
13256
+ runtime.writeText(paths.cache(SHADOW_FILE), `${JSON.stringify(state, null, 2)}
13257
+ `);
13258
+ };
13259
+ var modeOf2 = (set, policy, shadow) => {
13260
+ if (set.damage) return "active";
13261
+ const chosen = policy.guards[set.id];
13262
+ if (chosen !== void 0) return chosen;
13263
+ if (!policy.exists) return "active";
13264
+ return shadow[set.id]?.promoted === true ? "active" : "shadow";
13265
+ };
13266
+ var recordShadow = (input, paths, set, policy, reason) => {
13267
+ const { runtime } = input;
13268
+ const state = readShadow(runtime, paths);
13269
+ const before = state[set.id] ?? { firings: 0, falsePositives: 0, promoted: false };
13270
+ const firings = before.firings + 1;
13271
+ const promoted = !before.promoted && firings >= policy.promoteAfter && before.falsePositives === 0;
13272
+ writeShadow(runtime, paths, { ...state, [set.id]: { ...before, firings, promoted: before.promoted || promoted } });
13273
+ logEvent(runtime, paths, "guard_shadow", {
13274
+ guard: set.id,
13275
+ hook: input.event,
13276
+ tool: toolName2(input.payload),
13277
+ wouldHave: "block",
13278
+ reason
13279
+ });
13280
+ if (promoted) {
13281
+ logEvent(runtime, paths, "guard_promoted", {
13282
+ guard: set.id,
13283
+ firings,
13284
+ falsePositives: before.falsePositives,
13285
+ promoteAfter: policy.promoteAfter
13286
+ });
13287
+ }
13288
+ };
13289
+ var governed = (set, handler) => async (input) => {
13290
+ const paths = projectPaths(input);
13291
+ if (paths === void 0) return handler(input);
13292
+ const policy = loadPolicy(input.runtime, paths.root);
13293
+ const mode = modeOf2(set, policy, readShadow(input.runtime, paths));
13294
+ if (mode === "off") return { kind: "allow" };
13295
+ const decision = await handler(input);
13296
+ if (mode === "active" || decision.kind !== "block") return decision;
13297
+ recordShadow(input, paths, set, policy, decision.reason);
13298
+ return { kind: "allow" };
13299
+ };
13300
+ var withPolicy = (set) => {
13301
+ if (set.damage) return set.handlers;
13302
+ const wrapped = {};
13303
+ for (const event of HOOK_EVENTS) {
13304
+ const list4 = set.handlers[event];
13305
+ if (list4 !== void 0) wrapped[event] = list4.map((handler) => governed(set, handler));
13306
+ }
13307
+ return wrapped;
13308
+ };
13309
+
12325
13310
  // src/prose/marker-guard.ts
12326
13311
  import { relative as relative6 } from "node:path";
12327
13312
 
@@ -12427,7 +13412,7 @@ var addsMarker = (input, tool, inputs, file) => {
12427
13412
  return false;
12428
13413
  }
12429
13414
  };
12430
- var isReadOnly = (program, args) => READ_ONLY.has(program) || program === "git" && GIT_READ_ONLY.has(args[0] ?? "");
13415
+ var isReadOnly = (program, args2) => READ_ONLY.has(program) || program === "git" && GIT_READ_ONLY.has(args2[0] ?? "");
12431
13416
  var shellAddsMarker = (command) => markerCount(command) > 0 && shellWords(command).segments.some((segment) => !isReadOnly(segment.program, segment.args));
12432
13417
  var shown2 = (cwd, file) => {
12433
13418
  const rel = toPosixPath(relative6(cwd, file));
@@ -12465,17 +13450,17 @@ var GIT_FLAGS = { title: [], body: ["-m", "--message"], file: ["-F", "--file"] }
12465
13450
  var GH_FLAGS = { title: ["-t", "--title"], body: ["-b", "--body"], file: ["-F", "--body-file"] };
12466
13451
  var GIT_GLOBAL_WITH_VALUE = /* @__PURE__ */ new Set(["-C", "-c", "--git-dir", "--work-tree", "--namespace"]);
12467
13452
  var CLUSTER_WITH_MESSAGE = /^-[a-zA-Z]*m$/;
12468
- var flagValues = (args, spec2) => {
13453
+ var flagValues = (args2, spec2) => {
12469
13454
  const known2 = [...spec2.title, ...spec2.body, ...spec2.file];
12470
13455
  const out = [];
12471
- for (let index = 0; index < args.length; index += 1) {
12472
- const arg = args[index] ?? "";
13456
+ for (let index = 0; index < args2.length; index += 1) {
13457
+ const arg = args2[index] ?? "";
12473
13458
  const equals = arg.indexOf("=");
12474
13459
  const name = arg.startsWith("--") && equals > 0 ? arg.slice(0, equals) : arg;
12475
13460
  if (name !== arg && known2.includes(name)) {
12476
13461
  out.push({ flag: name, value: arg.slice(equals + 1) });
12477
13462
  } else if (known2.includes(arg) || spec2 === GIT_FLAGS && CLUSTER_WITH_MESSAGE.test(arg)) {
12478
- out.push({ flag: known2.includes(arg) ? arg : "-m", value: args[index + 1] ?? "" });
13463
+ out.push({ flag: known2.includes(arg) ? arg : "-m", value: args2[index + 1] ?? "" });
12479
13464
  index += 1;
12480
13465
  }
12481
13466
  }
@@ -12489,17 +13474,17 @@ var sourceOf = (value, isFile, segment, heredocs) => {
12489
13474
  const piped = segment.args.map((arg) => heredocIn(arg, heredocs)).find((text6) => text6 !== void 0);
12490
13475
  return { kind: "inline", text: piped ?? "" };
12491
13476
  };
12492
- var collect3 = (target, segment, args, spec2, heredocs) => {
12493
- const values = flagValues(args, spec2);
13477
+ var collect3 = (target, segment, args2, spec2, heredocs) => {
13478
+ const values = flagValues(args2, spec2);
12494
13479
  if (values.length === 0) return void 0;
12495
13480
  const title = values.find((item) => spec2.title.includes(item.flag));
12496
13481
  const bodies = values.filter((item) => !spec2.title.includes(item.flag)).map((item) => sourceOf(item.value, spec2.file.includes(item.flag), segment, heredocs));
12497
13482
  return { target, title: title === void 0 ? void 0 : sourceOf(title.value, false, segment, heredocs), bodies };
12498
13483
  };
12499
- var gitSubcommand = (args) => {
13484
+ var gitSubcommand = (args2) => {
12500
13485
  let index = 0;
12501
- while (index < args.length && (args[index] ?? "").startsWith("-")) {
12502
- index += GIT_GLOBAL_WITH_VALUE.has(args[index] ?? "") ? 2 : 1;
13486
+ while (index < args2.length && (args2[index] ?? "").startsWith("-")) {
13487
+ index += GIT_GLOBAL_WITH_VALUE.has(args2[index] ?? "") ? 2 : 1;
12503
13488
  }
12504
13489
  return index;
12505
13490
  };
@@ -12631,7 +13616,7 @@ var liveFindings = (input, texts) => {
12631
13616
  const accepted2 = acceptedFingerprints(baseline.ok ? baseline.value : EMPTY_BASELINE, input.runtime.now());
12632
13617
  return texts.flatMap((text6) => scanText(text6, { rules: CREDENTIAL_RULES })).filter((finding3) => !accepted2.has(finding3.fingerprint));
12633
13618
  };
12634
- var record = (input, findings, draft, where) => {
13619
+ var record2 = (input, findings, draft, where) => {
12635
13620
  const ids2 = findings.map((finding3) => {
12636
13621
  const saved = recordIncident(
12637
13622
  input.runtime,
@@ -12647,7 +13632,7 @@ var secretsUserPromptSubmit = (input) => {
12647
13632
  const prompt = optionalString(input.payload.raw, "prompt") ?? "";
12648
13633
  const findings = liveFindings(input, [prompt]);
12649
13634
  if (findings.length === 0) return { kind: "allow" };
12650
- const incident = record(input, findings, { source: "prompt", state: "exposicao-local" }, "prompt");
13635
+ const incident = record2(input, findings, { source: "prompt", state: "exposicao-local" }, "prompt");
12651
13636
  const items = findings.map((finding3) => ({ ruleId: finding3.ruleId, where: lineWhere(input.t, finding3) }));
12652
13637
  return {
12653
13638
  kind: "block",
@@ -12658,7 +13643,7 @@ var secretsPostToolUse = (input) => {
12658
13643
  const findings = liveFindings(input, stringLeaves(input.payload.raw["tool_response"]));
12659
13644
  if (findings.length === 0) return { kind: "allow" };
12660
13645
  const tool = toolName2(input.payload) || "tool";
12661
- const incident = record(input, findings, { source: "tool-output", state: "aberto" }, `tool-output:${tool}`);
13646
+ const incident = record2(input, findings, { source: "tool-output", state: "aberto" }, `tool-output:${tool}`);
12662
13647
  const items = findings.map((finding3) => ({ ruleId: finding3.ruleId, where: lineWhere(input.t, finding3) }));
12663
13648
  const context = input.t("secrets.post.context", { tool, found: describeFindings(input.t, items), incident });
12664
13649
  return {
@@ -12715,7 +13700,7 @@ var READERS = /* @__PURE__ */ new Set([
12715
13700
  var SECRET_NAME = /(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PASS|PWD|CREDENTIAL)/i;
12716
13701
  var PERSON_ONLY = /\b(?:finitemonkey|fm)(?:\.mjs)?\s+secrets\s+(?:accept\b|incident\s+(?:rotated|close)\b)/;
12717
13702
  var LISTS_ENV = /* @__PURE__ */ new Set(["get-childitem", "gci", "dir", "ls"]);
12718
- var onlyFlags = (args) => args.every((arg) => arg.startsWith("-"));
13703
+ var onlyFlags = (args2) => args2.every((arg) => arg.startsWith("-"));
12719
13704
  var readTarget = (segment) => {
12720
13705
  const redirected = segment.redirectedFrom.find(isSensitivePath);
12721
13706
  if (redirected !== void 0) return redirected;
@@ -12849,17 +13834,17 @@ var secretsPreToolUse = (input) => {
12849
13834
  // src/secrets/remotes.ts
12850
13835
  var USER_AND_SECRET = /^[a-z][a-z0-9+.-]*:\/\/[^\s/@:]+:[^\s/@]+@/i;
12851
13836
  var USERINFO = /^[a-z][a-z0-9+.-]*:\/\/(?<user>[^\s/@]+)@/i;
12852
- var hasCredential = (url) => {
12853
- if (USER_AND_SECRET.test(url)) return true;
12854
- const user = USERINFO.exec(url)?.groups?.["user"];
13837
+ var hasCredential = (url2) => {
13838
+ if (USER_AND_SECRET.test(url2)) return true;
13839
+ const user = USERINFO.exec(url2)?.groups?.["user"];
12855
13840
  return user !== void 0 && scanText(user, { rules: CREDENTIAL_RULES }).length > 0;
12856
13841
  };
12857
13842
  var parseRemotes = (output) => {
12858
13843
  const remotes = /* @__PURE__ */ new Map();
12859
13844
  for (const line2 of output.split(/\r?\n/)) {
12860
- const [name, url] = line2.trim().split(/\s+/);
12861
- if (name === void 0 || url === void 0 || name === "") continue;
12862
- remotes.set(name, [...remotes.get(name) ?? [], url]);
13845
+ const [name, url2] = line2.trim().split(/\s+/);
13846
+ if (name === void 0 || url2 === void 0 || name === "") continue;
13847
+ remotes.set(name, [...remotes.get(name) ?? [], url2]);
12863
13848
  }
12864
13849
  return remotes;
12865
13850
  };
@@ -12940,12 +13925,13 @@ var SECRETS_HANDLERS = {
12940
13925
  PostToolUse: [secretsPostToolUse],
12941
13926
  SessionStart: [secretsSessionStart]
12942
13927
  };
12943
- var DEFAULT_HANDLERS = mergeHandlers(
12944
- SECRETS_HANDLERS,
12945
- PORTAO_HANDLERS,
12946
- PROSE_HANDLERS,
12947
- GIT_HANDLERS
12948
- );
13928
+ var GUARD_SETS = [
13929
+ { id: "secrets", damage: true, handlers: SECRETS_HANDLERS },
13930
+ { id: "gate", damage: true, handlers: PORTAO_HANDLERS },
13931
+ { id: "prose", damage: false, handlers: PROSE_HANDLERS },
13932
+ { id: "git", damage: true, handlers: GIT_HANDLERS }
13933
+ ];
13934
+ var DEFAULT_HANDLERS = mergeHandlers(...GUARD_SETS.map(withPolicy));
12949
13935
 
12950
13936
  // src/hooks/payload.ts
12951
13937
  var EMPTY_PAYLOAD = {
@@ -13107,9 +14093,9 @@ var invocationOf = (entry2, vars) => {
13107
14093
  const seconds = typeof timeout === "number" && timeout > 0 ? timeout : DEFAULT_TIMEOUT_S;
13108
14094
  const timeoutMs = Math.min(seconds * 1e3, PROBE_TIMEOUT_MS2);
13109
14095
  const command = expand(raw, vars);
13110
- const args = strings3(entry2.handler["args"]).map((arg) => expand(arg, vars));
13111
- const shell = args.length === 0;
13112
- return { command, args, shell, timeoutMs, display: [command, ...args].join(" ") };
14096
+ const args2 = strings3(entry2.handler["args"]).map((arg) => expand(arg, vars));
14097
+ const shell = args2.length === 0;
14098
+ return { command, args: args2, shell, timeoutMs, display: [command, ...args2].join(" ") };
13113
14099
  };
13114
14100
  var versionInvocation = (invocation) => {
13115
14101
  if (invocation.shell) {
@@ -13118,8 +14104,8 @@ var versionInvocation = (invocation) => {
13118
14104
  }
13119
14105
  const at = invocation.args.indexOf("hook");
13120
14106
  if (at < 0) return void 0;
13121
- const args = [...invocation.args.slice(0, at), "version", "--json"];
13122
- return { ...invocation, args, display: [invocation.command, ...args].join(" ") };
14107
+ const args2 = [...invocation.args.slice(0, at), "version", "--json"];
14108
+ return { ...invocation, args: args2, display: [invocation.command, ...args2].join(" ") };
13123
14109
  };
13124
14110
  var syntheticPayload = (event, cwd) => {
13125
14111
  const base = { session_id: "finitemonkey-doctor", transcript_path: "", cwd, hook_event_name: event };
@@ -13320,10 +14306,10 @@ var handlerProblems = (at, handler, claude) => {
13320
14306
  const type = handler["type"];
13321
14307
  if (typeof type !== "string" || !HANDLER_TYPES.some((known2) => known2 === type)) return [`${at}.type: unknown`];
13322
14308
  const problems = [];
13323
- const { command, args, timeout, onFailure } = handler;
14309
+ const { command, args: args2, timeout, onFailure } = handler;
13324
14310
  if (type === "command" && (typeof command !== "string" || command.trim() === ""))
13325
14311
  problems.push(`${at}.command: empty`);
13326
- if (args !== void 0 && !(Array.isArray(args) && args.every((arg) => typeof arg === "string"))) {
14312
+ if (args2 !== void 0 && !(Array.isArray(args2) && args2.every((arg) => typeof arg === "string"))) {
13327
14313
  problems.push(`${at}.args: not a list of strings`);
13328
14314
  }
13329
14315
  if (timeout !== void 0 && !(typeof timeout === "number" && timeout > 0))
@@ -13356,12 +14342,12 @@ var hookSchemaProblems = (hooks, claude) => {
13356
14342
  };
13357
14343
 
13358
14344
  // src/doctor/hook-sources.ts
13359
- import { join as join28 } from "node:path";
14345
+ import { join as join30 } from "node:path";
13360
14346
  var entriesOf = (source, hooks, own) => Object.entries(asRecord(hooks)).flatMap(
13361
14347
  ([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 }));
14348
+ const record3 = asRecord(group);
14349
+ const matcher = optionalString(record3, "matcher");
14350
+ return asList(record3["hooks"]).filter((handler) => isRecord(handler) && own(handler)).map((handler) => ({ source, event, matcher, handler }));
13365
14351
  })
13366
14352
  );
13367
14353
  var PROJECT_SCOPES = { project: "project", local: "local" };
@@ -13369,10 +14355,10 @@ var projectHookFiles = (env) => readSettings(env).flatMap(({ scope: scope2, path
13369
14355
  const source = PROJECT_SCOPES[scope2];
13370
14356
  return source === void 0 || !("hooks" in settings) ? [] : [{ source, path, hooks: settings["hooks"] }];
13371
14357
  });
13372
- var pluginsDir = ({ env, runtime }) => join28(env["CLAUDE_CONFIG_DIR"] ?? join28(runtime.homeDir(), ".claude"), "plugins");
14358
+ var pluginsDir = ({ env, runtime }) => join30(env["CLAUDE_CONFIG_DIR"] ?? join30(runtime.homeDir(), ".claude"), "plugins");
13373
14359
  var installRecord = (value, root) => {
13374
14360
  const records = Array.isArray(value) ? value.filter(isRecord) : isRecord(value) ? [value] : [];
13375
- return records.find((record2) => record2["projectPath"] === root) ?? records[0];
14361
+ return records.find((record3) => record3["projectPath"] === root) ?? records[0];
13376
14362
  };
13377
14363
  var readJson = (env, path) => {
13378
14364
  const text6 = env.runtime.readText(path);
@@ -13380,18 +14366,18 @@ var readJson = (env, path) => {
13380
14366
  return parsed?.ok === true ? parsed.value : void 0;
13381
14367
  };
13382
14368
  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;
14369
+ const index = readJson(env, toAbsolutePath(join30(pluginsDir(env), "installed_plugins.json")));
14370
+ const record3 = installRecord(asRecord(index?.["plugins"])[PLUGIN_ID], env.root);
14371
+ const installPath = record3 === void 0 ? void 0 : optionalString(record3, "installPath");
14372
+ if (record3 === void 0 || installPath === void 0) return void 0;
13387
14373
  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"));
14374
+ const manifest = readJson(env, toAbsolutePath(join30(root, ".claude-plugin", "plugin.json")));
14375
+ const hooksPath = toAbsolutePath(join30(root, "hooks", "hooks.json"));
13390
14376
  const text6 = env.runtime.readText(hooksPath);
13391
14377
  const parsed = text6.ok ? parseJsonObject(text6.value) : void 0;
13392
14378
  return {
13393
14379
  root,
13394
- version: (manifest === void 0 ? void 0 : optionalString(manifest, "version")) ?? optionalString(record2, "version"),
14380
+ version: (manifest === void 0 ? void 0 : optionalString(manifest, "version")) ?? optionalString(record3, "version"),
13395
14381
  hooksFile: parsed?.ok === true ? { source: "plugin", path: hooksPath, hooks: parsed.value["hooks"] } : void 0,
13396
14382
  hooksText: text6.ok ? text6.value : void 0
13397
14383
  };
@@ -13431,7 +14417,7 @@ var schemaFindings = (env, plugin, claude) => {
13431
14417
  const files = [...projectHookFiles(env), ...plugin?.hooksFile === void 0 ? [] : [plugin.hooksFile]];
13432
14418
  const invalidJson = [
13433
14419
  ...unreadableProjectFiles(env),
13434
- ...plugin?.hooksText !== void 0 && plugin.hooksFile === void 0 ? [toAbsolutePath(join29(plugin.root, "hooks", "hooks.json"))] : []
14420
+ ...plugin?.hooksText !== void 0 && plugin.hooksFile === void 0 ? [toAbsolutePath(join31(plugin.root, "hooks", "hooks.json"))] : []
13435
14421
  ];
13436
14422
  return [
13437
14423
  ...invalidJson.flatMap((file) => schemaFinding(file, ["not valid JSON"], label2)),
@@ -13452,7 +14438,7 @@ var pluginFindings = (env, projectOwn) => {
13452
14438
  ];
13453
14439
  };
13454
14440
  var coverageFindings = (env, entries) => {
13455
- const installed = env.runtime.stat(toAbsolutePath(join29(env.root, ...LOCK_FILE.split("/")))) !== void 0;
14441
+ const installed = env.runtime.stat(toAbsolutePath(join31(env.root, ...LOCK_FILE.split("/")))) !== void 0;
13456
14442
  if (entries.length === 0 && !installed) {
13457
14443
  return [finding("hooks.registered", "warn", "doctor.hooks.none", {}, "doctor.hooks.fix_none")];
13458
14444
  }
@@ -13504,17 +14490,17 @@ var checkHooks = (env) => {
13504
14490
  };
13505
14491
 
13506
14492
  // src/doctor/instructions.ts
13507
- import { join as join30 } from "node:path";
14493
+ import { join as join32 } from "node:path";
13508
14494
  var INSTRUCTION_LINE_CEILING = 200;
13509
14495
  var TOP_INSTRUCTIONS = ["AGENTS.md", "CLAUDE.md", ".claude/CLAUDE.md", "CLAUDE.local.md"];
13510
14496
  var RULES_DIR = ".claude/rules";
13511
14497
  var SHOWN = 3;
13512
14498
  var readProjectFile = (env, relative9) => {
13513
- const text6 = env.runtime.readText(toAbsolutePath(join30(env.root, ...relative9.split("/"))));
14499
+ const text6 = env.runtime.readText(toAbsolutePath(join32(env.root, ...relative9.split("/"))));
13514
14500
  return text6.ok ? text6.value : void 0;
13515
14501
  };
13516
14502
  var instructionFiles = (env) => {
13517
- const listed4 = env.runtime.listDir(toAbsolutePath(join30(env.root, ...RULES_DIR.split("/"))));
14503
+ const listed4 = env.runtime.listDir(toAbsolutePath(join32(env.root, ...RULES_DIR.split("/"))));
13518
14504
  const rules = listed4.ok ? listed4.value.filter((name) => name.endsWith(".md")).map((name) => `${RULES_DIR}/${name}`) : [];
13519
14505
  return [...TOP_INSTRUCTIONS, ...rules].flatMap((path) => {
13520
14506
  const text6 = readProjectFile(env, path);
@@ -13619,12 +14605,12 @@ var checkInstructions = (env) => {
13619
14605
  };
13620
14606
 
13621
14607
  // src/doctor/line-endings.ts
13622
- import { join as join31 } from "node:path";
14608
+ import { join as join33 } from "node:path";
13623
14609
  var LF_RULE = /^\*\s.*\beol=lf\b/m;
13624
14610
  var checkLineEndings = ({ root, runtime }) => {
13625
14611
  const autocrlf = runtime.git(["config", "--get", "core.autocrlf"], root);
13626
14612
  if (!autocrlf.ok || autocrlf.stdout.trim().toLowerCase() !== "true") return [];
13627
- const attributes = runtime.readText(toAbsolutePath(join31(root, ".gitattributes")));
14613
+ const attributes = runtime.readText(toAbsolutePath(join33(root, ".gitattributes")));
13628
14614
  if (attributes.ok && LF_RULE.test(attributes.value)) return [];
13629
14615
  return [
13630
14616
  finding("git.autocrlf", "warn", "doctor.git.autocrlf", { file: ".gitattributes" }, "doctor.git.fix_autocrlf")
@@ -13632,7 +14618,7 @@ var checkLineEndings = ({ root, runtime }) => {
13632
14618
  };
13633
14619
 
13634
14620
  // src/doctor/memory.ts
13635
- import { join as join32 } from "node:path";
14621
+ import { join as join34 } from "node:path";
13636
14622
  var ceilingOf = (config) => {
13637
14623
  const value = configAt(parseConfigYaml(config ?? ""), "memory", "maxLines");
13638
14624
  const parsed = typeof value === "string" ? Number.parseInt(value, 10) : Number.NaN;
@@ -13640,9 +14626,9 @@ var ceilingOf = (config) => {
13640
14626
  };
13641
14627
  var lineCount2 = (text6) => text6.replace(/\r?\n$/, "").split(/\r?\n/).length;
13642
14628
  var checkAgentsFile = ({ root, runtime }) => {
13643
- const text6 = runtime.readText(toAbsolutePath(join32(root, AGENTS_FILE)));
14629
+ const text6 = runtime.readText(toAbsolutePath(join34(root, AGENTS_FILE)));
13644
14630
  if (!text6.ok) return [];
13645
- const config = runtime.readText(toAbsolutePath(join32(root, CONFIG_FILE)));
14631
+ const config = runtime.readText(toAbsolutePath(join34(root, CONFIG_FILE)));
13646
14632
  const max = ceilingOf(config.ok ? config.value : void 0);
13647
14633
  const lines3 = lineCount2(text6.value);
13648
14634
  const params2 = { file: AGENTS_FILE, lines: lines3, max };
@@ -13650,7 +14636,7 @@ var checkAgentsFile = ({ root, runtime }) => {
13650
14636
  };
13651
14637
 
13652
14638
  // src/doctor/project-files.ts
13653
- import { join as join33 } from "node:path";
14639
+ import { join as join35 } from "node:path";
13654
14640
  var variants = (base, extensions) => extensions.map((extension) => `${base}${extension}`);
13655
14641
  var SCRIPT = [".js", ".cjs", ".mjs", ".ts", ".cts", ".mts"];
13656
14642
  var DATA = ["", ".json", ".yml", ".yaml"];
@@ -13681,7 +14667,7 @@ var CONFIG_GROUPS = [
13681
14667
  files: [...variants("commitlint.config", SCRIPT), ...variants(".commitlintrc", [...DATA, ".js"])]
13682
14668
  }
13683
14669
  ];
13684
- var present = (env, files) => files.filter((file) => env.runtime.stat(toAbsolutePath(join33(env.root, file)))?.kind === "file");
14670
+ var present = (env, files) => files.filter((file) => env.runtime.stat(toAbsolutePath(join35(env.root, file)))?.kind === "file");
13685
14671
  var crowded = (env, groups, build) => groups.flatMap(({ name, files }) => {
13686
14672
  const found = present(env, files);
13687
14673
  return found.length > 1 ? [build(name, found.join(", "))] : [];
@@ -13742,9 +14728,9 @@ var CHECKS = [
13742
14728
  ];
13743
14729
  var probeOnce = (probe) => {
13744
14730
  const seen = /* @__PURE__ */ new Map();
13745
- return (command, args, timeoutMs) => {
13746
- const key = [command, ...args].join(" ");
13747
- const known2 = seen.get(key) ?? probe(command, args, timeoutMs);
14731
+ return (command, args2, timeoutMs) => {
14732
+ const key = [command, ...args2].join(" ");
14733
+ const known2 = seen.get(key) ?? probe(command, args2, timeoutMs);
13748
14734
  seen.set(key, known2);
13749
14735
  return known2;
13750
14736
  };
@@ -13855,93 +14841,7 @@ var doctorCommand = {
13855
14841
  usage: "finitemonkey doctor [--fix] [--report] [--dry-run] [--json]",
13856
14842
  status: "ready",
13857
14843
  options: { fix: { type: "boolean" }, report: { type: "boolean" } },
13858
- run: (ctx, args) => Promise.resolve(runDoctor(ctx, args.values["fix"] === true, args.values["report"] === true))
13859
- };
13860
-
13861
- // src/core/args.ts
13862
- import { parseArgs } from "node:util";
13863
-
13864
- // src/core/problem.ts
13865
- var usageProblem = (code, messageKey, fix, params2 = {}) => ({
13866
- code,
13867
- messageKey,
13868
- params: params2,
13869
- fix,
13870
- exitCode: ExitCode.Usage
13871
- });
13872
- var needsPersonProblem = (messageKey, fix, params2 = {}) => ({
13873
- code: "needs_confirmation",
13874
- messageKey,
13875
- params: params2,
13876
- fix,
13877
- exitCode: ExitCode.NeedsPerson
13878
- });
13879
- var FmError = class extends Error {
13880
- problem;
13881
- constructor(problem4) {
13882
- super(problem4.code);
13883
- this.name = "FmError";
13884
- this.problem = problem4;
13885
- }
13886
- };
13887
-
13888
- // src/core/args.ts
13889
- var GLOBAL_OPTIONS = {
13890
- json: { type: "boolean" },
13891
- lang: { type: "string" },
13892
- yes: { type: "boolean", short: "y" },
13893
- "dry-run": { type: "boolean" },
13894
- version: { type: "boolean", short: "v" },
13895
- help: { type: "boolean", short: "h" }
13896
- };
13897
- var flag = (values, name) => values[name] === true;
13898
- var looseArgs = (argv) => {
13899
- const { values, positionals } = parseArgs({
13900
- args: [...argv],
13901
- options: GLOBAL_OPTIONS,
13902
- strict: false,
13903
- allowPositionals: true
13904
- });
13905
- const lang = values.lang;
13906
- return {
13907
- positionals,
13908
- json: flag(values, "json"),
13909
- yes: flag(values, "yes"),
13910
- dryRun: flag(values, "dry-run"),
13911
- version: flag(values, "version"),
13912
- help: flag(values, "help"),
13913
- lang: typeof lang === "string" ? lang : void 0
13914
- };
13915
- };
13916
- var resolveCommand = (registry, positionals) => registry.filter((spec2) => spec2.path.every((part2, index) => positionals[index] === part2)).reduce(
13917
- (best, spec2) => best === void 0 || spec2.path.length > best.consumed ? { spec: spec2, consumed: spec2.path.length } : best,
13918
- void 0
13919
- );
13920
- var strictArgs = (argv, spec2) => {
13921
- const fix = `finitemonkey help ${spec2.path.join(" ")}`;
13922
- let parsed;
13923
- try {
13924
- parsed = parseArgs({
13925
- args: [...argv],
13926
- options: { ...GLOBAL_OPTIONS, ...spec2.options },
13927
- strict: true,
13928
- allowPositionals: true
13929
- });
13930
- } catch (error) {
13931
- const detail = error instanceof Error ? error.message : String(error);
13932
- return reject(usageProblem("invalid_arguments", "error.invalid_arguments", fix, { detail }));
13933
- }
13934
- const rest = parsed.positionals.slice(spec2.path.length);
13935
- const max = spec2.maxPositionals ?? 0;
13936
- const extra = rest[max];
13937
- if (extra !== void 0) {
13938
- return reject(usageProblem("unexpected_argument", "error.unexpected_argument", fix, { argument: extra }));
13939
- }
13940
- return accept({ values: parsed.values, positionals: rest });
13941
- };
13942
- var stringOption = (values, name) => {
13943
- const value = values[name];
13944
- return typeof value === "string" ? value : void 0;
14844
+ run: (ctx, args2) => Promise.resolve(runDoctor(ctx, args2.values["fix"] === true, args2.values["report"] === true))
13945
14845
  };
13946
14846
 
13947
14847
  // src/gate/baseline-run.ts
@@ -13956,8 +14856,8 @@ var runBaseline = (options) => {
13956
14856
  if (commands.length === 0) return { kind: "skipped", reason: "no_commands" };
13957
14857
  const checks = {};
13958
14858
  for (const check2 of commands) {
13959
- const outcome = runShellCheck(check2, check2.run ?? "", ctx);
13960
- checks[check2.id] = { exitCode: outcome.exitCode ?? null, failures: outcome.failures ?? [] };
14859
+ const outcome2 = runShellCheck(check2, check2.run ?? "", ctx);
14860
+ checks[check2.id] = { exitCode: outcome2.exitCode ?? null, failures: outcome2.failures ?? [] };
13961
14861
  }
13962
14862
  if (treeHash(gitAt(ctx.runtime, ctx.paths.root)) !== ctx.treeHash) return { kind: "skipped", reason: "tree_moved" };
13963
14863
  writeBaseline(ctx.runtime, ctx.paths, ctx.treeHash, checks);
@@ -14016,19 +14916,19 @@ var gateCommand = {
14016
14916
  baseline: { type: "boolean" },
14017
14917
  prs: { type: "boolean" }
14018
14918
  },
14019
- run: (ctx, args) => {
14020
- const taskId = stringOption(args.values, "task");
14919
+ run: (ctx, args2) => {
14920
+ const taskId = stringOption(args2.values, "task");
14021
14921
  const options = {
14022
14922
  runtime: ctx.runtime,
14023
14923
  cwd: ctx.cwd,
14024
14924
  env: ctx.env,
14025
14925
  sessionId: void 0,
14026
- budgetMs: budgetMs(stringOption(args.values, "budget")),
14027
- fast: args.values["fast"] === true,
14926
+ budgetMs: budgetMs(stringOption(args2.values, "budget")),
14927
+ fast: args2.values["fast"] === true,
14028
14928
  ...taskId === void 0 ? {} : { taskId }
14029
14929
  };
14030
- if (args.values["prs"] === true) return Promise.resolve(prsResult(ctx));
14031
- if (args.values["baseline"] === true) {
14930
+ if (args2.values["prs"] === true) return Promise.resolve(prsResult(ctx));
14931
+ if (args2.values["baseline"] === true) {
14032
14932
  const baseline = runBaseline(options);
14033
14933
  return Promise.resolve(
14034
14934
  success(baseline, [
@@ -14038,7 +14938,7 @@ var gateCommand = {
14038
14938
  ])
14039
14939
  );
14040
14940
  }
14041
- return Promise.resolve(runResult(ctx, runGate(options), args.values["strict"] === true));
14941
+ return Promise.resolve(runResult(ctx, runGate(options), args2.values["strict"] === true));
14042
14942
  }
14043
14943
  };
14044
14944
 
@@ -14119,11 +15019,11 @@ var mergeVerifyCommand = {
14119
15019
  status: "ready",
14120
15020
  options: { since: { type: "string" }, dropped: { type: "string" } },
14121
15021
  maxPositionals: 1,
14122
- run: (ctx, args) => {
15022
+ run: (ctx, args2) => {
14123
15023
  const repo = repoOf2(ctx);
14124
15024
  if (repo === void 0) return Promise.resolve(failureFrom(ctx.t, notRepository(ctx.cwd)));
14125
- const dropped = stringOption(args.values, "dropped");
14126
- const targets2 = targetsOf2(ctx, repo, args.positionals[0], stringOption(args.values, "since"));
15025
+ const dropped = stringOption(args2.values, "dropped");
15026
+ const targets2 = targetsOf2(ctx, repo, args2.positionals[0], stringOption(args2.values, "since"));
14127
15027
  if (targets2.length === 0) {
14128
15028
  const data = { schema: MERGE_VERIFY_RUN_SCHEMA, merges: [] };
14129
15029
  return Promise.resolve(success(data, [ctx.t("git.merge.none", { detail: "HEAD" })]));
@@ -14160,11 +15060,11 @@ var unreachable = (runtime, repo, shas) => {
14160
15060
  var fsckDangling = (runtime, repo) => {
14161
15061
  const result = runtime.git(["fsck", "--no-reflogs", "--no-progress", "--dangling"], repo.root);
14162
15062
  const lines3 = linesOf(result.stdout);
14163
- const pick3 = (kind) => lines3.flatMap((line2) => {
15063
+ const pick4 = (kind) => lines3.flatMap((line2) => {
14164
15064
  const [label2, type, sha] = line2.split(" ");
14165
15065
  return label2 === "dangling" && type === kind && sha !== void 0 ? [sha] : [];
14166
15066
  });
14167
- return { commits: pick3("commit"), blobs: pick3("blob").length };
15067
+ return { commits: pick4("commit"), blobs: pick4("blob").length };
14168
15068
  };
14169
15069
  var describe = (runtime, repo, shas) => {
14170
15070
  if (shas.length === 0) return /* @__PURE__ */ new Map();
@@ -14231,11 +15131,11 @@ var rescueCommand = {
14231
15131
  usage: "finitemonkey rescue [--restore <sha> --branch <name>] [--json]",
14232
15132
  status: "ready",
14233
15133
  options: { restore: { type: "string" }, branch: { type: "string" } },
14234
- run: (ctx, args) => {
15134
+ run: (ctx, args2) => {
14235
15135
  const repo = repoOf2(ctx);
14236
15136
  if (repo === void 0) return Promise.resolve(failureFrom(ctx.t, notRepository(ctx.cwd)));
14237
- const sha = stringOption(args.values, "restore");
14238
- const branch = stringOption(args.values, "branch");
15137
+ const sha = stringOption(args2.values, "restore");
15138
+ const branch = stringOption(args2.values, "branch");
14239
15139
  if (sha === void 0 && branch === void 0) {
14240
15140
  const report2 = rescueReport(ctx.runtime, repo);
14241
15141
  return Promise.resolve(success({ schema: RESCUE_SCHEMA, action: "list", ...report2 }, listing(ctx.t, report2)));
@@ -14264,10 +15164,10 @@ var rescueCommand = {
14264
15164
  };
14265
15165
 
14266
15166
  // src/git/undo.ts
14267
- import { basename as basename3, dirname as dirname8, join as join34 } from "node:path";
15167
+ import { basename as basename3, dirname as dirname8, join as join36 } from "node:path";
14268
15168
  var INDEX_SUBJECT = "finitemonkey index:";
14269
- var gitLine = (runtime, repo, args) => {
14270
- const result = runtime.git(args, repo.root);
15169
+ var gitLine = (runtime, repo, args2) => {
15170
+ const result = runtime.git(args2, repo.root);
14271
15171
  const value = result.stdout.trim();
14272
15172
  return result.ok && value !== "" ? value : void 0;
14273
15173
  };
@@ -14307,7 +15207,7 @@ var findSnapshot = (views, wanted) => {
14307
15207
  };
14308
15208
  var stampOf2 = (iso) => iso.replace(/[:.]/g, "-");
14309
15209
  var worktreePath = (runtime, repo) => {
14310
- const base = join34(dirname8(repo.root), `${basename3(repo.root)}-undo-${stampOf2(runtime.now())}`);
15210
+ const base = join36(dirname8(repo.root), `${basename3(repo.root)}-undo-${stampOf2(runtime.now())}`);
14311
15211
  return toAbsolutePath(runtime.stat(toAbsolutePath(base)) === void 0 ? base : `${base}-2`);
14312
15212
  };
14313
15213
  var intoWorktree = (runtime, repo, view2) => {
@@ -14315,13 +15215,13 @@ var intoWorktree = (runtime, repo, view2) => {
14315
15215
  const added = runtime.gitWith(["worktree", "add", "--detach", path, view2.commit], repo.root, {});
14316
15216
  return added.ok ? accept(path) : reject(failure2("worktree add", added));
14317
15217
  };
14318
- var step2 = (runtime, repo, args) => {
14319
- const result = runtime.gitWith(args, repo.root, {});
14320
- return result.ok ? accept(void 0) : reject(failure2(args.join(" "), result));
15218
+ var step2 = (runtime, repo, args2) => {
15219
+ const result = runtime.gitWith(args2, repo.root, {});
15220
+ return result.ok ? accept(void 0) : reject(failure2(args2.join(" "), result));
14321
15221
  };
14322
15222
  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) {
15223
+ const current2 = gitLine(runtime, repo, ["rev-parse", "--verify", "--quiet", "HEAD"]);
15224
+ if (view2.head !== void 0 && view2.head !== current2) {
14325
15225
  const moved = step2(runtime, repo, ["reset", "--soft", view2.head]);
14326
15226
  if (!moved.ok) return moved;
14327
15227
  }
@@ -14350,10 +15250,10 @@ var snapshotCommand = {
14350
15250
  usage: "finitemonkey snapshot [--message <text>] [--json]",
14351
15251
  status: "ready",
14352
15252
  options: { message: { type: "string", short: "m" } },
14353
- run: (ctx, args) => {
15253
+ run: (ctx, args2) => {
14354
15254
  const repo = repoOf2(ctx);
14355
15255
  if (repo === void 0) return Promise.resolve(failureFrom(ctx.t, notRepository(ctx.cwd)));
14356
- const message = args.values["message"];
15256
+ const message = args2.values["message"];
14357
15257
  const op = typeof message === "string" && message.trim() !== "" ? message.trim() : "manual";
14358
15258
  const taken = takeSnapshot(ctx.runtime, repo, { op });
14359
15259
  if (!taken.ok) {
@@ -14383,12 +15283,12 @@ var undoCommand = {
14383
15283
  status: "ready",
14384
15284
  options: { list: { type: "boolean" }, "in-place": { type: "boolean" } },
14385
15285
  maxPositionals: 1,
14386
- run: (ctx, args) => {
15286
+ run: (ctx, args2) => {
14387
15287
  const repo = repoOf2(ctx);
14388
15288
  if (repo === void 0) return Promise.resolve(failureFrom(ctx.t, notRepository(ctx.cwd)));
14389
15289
  const views = snapshotViews(ctx.runtime, repo);
14390
- if (args.values["list"] === true) return Promise.resolve(listResult(ctx, views));
14391
- const wanted = args.positionals[0];
15290
+ if (args2.values["list"] === true) return Promise.resolve(listResult(ctx, views));
15291
+ const wanted = args2.positionals[0];
14392
15292
  const view2 = findSnapshot(views, wanted);
14393
15293
  if (view2 === void 0) {
14394
15294
  return Promise.resolve(
@@ -14401,7 +15301,7 @@ var undoCommand = {
14401
15301
  })
14402
15302
  );
14403
15303
  }
14404
- const inPlace2 = args.values["in-place"] === true;
15304
+ const inPlace2 = args2.values["in-place"] === true;
14405
15305
  if (inPlace2 && !(ctx.yes && isPerson(ctx))) {
14406
15306
  return Promise.resolve(
14407
15307
  failureFrom(ctx.t, needsPersonProblem("git.undo.confirm", `finitemonkey undo ${view2.ref} --in-place --yes`))
@@ -14490,8 +15390,8 @@ var helpCommand = {
14490
15390
  usage: "finitemonkey help [command]",
14491
15391
  status: "ready",
14492
15392
  maxPositionals: 3,
14493
- run: (ctx, args, registry) => Promise.resolve(
14494
- args.positionals.length === 0 ? overview(ctx, registry) : topicHelp(ctx, registry, args.positionals)
15393
+ run: (ctx, args2, registry) => Promise.resolve(
15394
+ args2.positionals.length === 0 ? overview(ctx, registry) : topicHelp(ctx, registry, args2.positionals)
14495
15395
  )
14496
15396
  };
14497
15397
 
@@ -14503,8 +15403,8 @@ var hookCommand = {
14503
15403
  status: "ready",
14504
15404
  maxPositionals: 1,
14505
15405
  options: { via: { type: "string" } },
14506
- run: async (ctx, args) => {
14507
- const raw = args.positionals[0] ?? "";
15406
+ run: async (ctx, args2) => {
15407
+ const raw = args2.positionals[0] ?? "";
14508
15408
  const event = parseHookEvent(raw);
14509
15409
  if (event === void 0) {
14510
15410
  const params2 = { event: raw, events: HOOK_EVENTS.join(", ") };
@@ -14513,10 +15413,10 @@ var hookCommand = {
14513
15413
  usageProblem("unknown_hook_event", "error.unknown_hook_event", "finitemonkey help hook", params2)
14514
15414
  );
14515
15415
  }
14516
- if (stringOption(args.values, "via") === PLUGIN_VIA && projectOwnsHooks(ctx)) {
15416
+ if (stringOption(args2.values, "via") === PLUGIN_VIA && projectOwnsHooks(ctx)) {
14517
15417
  return { ok: true, exitCode: 0, data: { event, verdict: "allow" }, raw: { stdout: YIELD_STDOUT, stderr: "" } };
14518
15418
  }
14519
- const outcome = await runHook(event, {
15419
+ const outcome2 = await runHook(event, {
14520
15420
  stdin: ctx.io.stdin,
14521
15421
  deadlineMs: deadlineFor(event, ctx.env["FINITEMONKEY_HOOK_DEADLINE_MS"]),
14522
15422
  handlers: DEFAULT_HANDLERS,
@@ -14526,11 +15426,11 @@ var hookCommand = {
14526
15426
  runtime: ctx.runtime,
14527
15427
  env: ctx.env
14528
15428
  });
14529
- logHookFailure(ctx, event, outcome);
14530
- const data = { event, verdict: outcome.verdict };
14531
- const output = { stdout: outcome.stdout, stderr: outcome.stderr };
14532
- if (outcome.exitCode === 0) return { ok: true, exitCode: 0, data, raw: output };
14533
- const message = outcome.stderr.trim();
15429
+ logHookFailure(ctx, event, outcome2);
15430
+ const data = { event, verdict: outcome2.verdict };
15431
+ const output = { stdout: outcome2.stdout, stderr: outcome2.stderr };
15432
+ if (outcome2.exitCode === 0) return { ok: true, exitCode: 0, data, raw: output };
15433
+ const message = outcome2.stderr.trim();
14534
15434
  return {
14535
15435
  ok: false,
14536
15436
  exitCode: 2,
@@ -14542,7 +15442,7 @@ var hookCommand = {
14542
15442
  };
14543
15443
 
14544
15444
  // src/commands/i18n-check.ts
14545
- import { join as join35 } from "node:path";
15445
+ import { join as join37 } from "node:path";
14546
15446
 
14547
15447
  // src/i18n/message.ts
14548
15448
  var ARG = /^\s*([a-zA-Z][a-zA-Z0-9_]*)\s*(?:,\s*(plural|select)\s*,)?/;
@@ -14744,7 +15644,7 @@ var unreadable = (dir, file, detail) => ({
14744
15644
  exitCode: ExitCode.NotConfigured
14745
15645
  });
14746
15646
  var readCatalog = (ctx, dir, lang) => {
14747
- const file = toAbsolutePath(join35(dir, `${lang}.json`));
15647
+ const file = toAbsolutePath(join37(dir, `${lang}.json`));
14748
15648
  const text6 = ctx.runtime.readText(file);
14749
15649
  if (!text6.ok) return reject(unreadable(dir, file, text6.error));
14750
15650
  const parsed = parseJsonObject(text6.value);
@@ -14767,7 +15667,7 @@ var describeProblem = (ctx, problem4) => {
14767
15667
  return ` ${problem4.lang} ${problem4.key} ${kind}`;
14768
15668
  };
14769
15669
  var loadReview = (ctx, dir) => {
14770
- const text6 = ctx.runtime.readText(toAbsolutePath(join35(dir, "review.json")));
15670
+ const text6 = ctx.runtime.readText(toAbsolutePath(join37(dir, "review.json")));
14771
15671
  const parsed = text6.ok ? parseJsonObject(text6.value) : void 0;
14772
15672
  if (parsed?.ok !== true) return void 0;
14773
15673
  const statuses = (lang) => {
@@ -14801,17 +15701,17 @@ var i18nCheckCommand = {
14801
15701
  usage: "finitemonkey i18n check [--dir <path>]",
14802
15702
  status: "ready",
14803
15703
  options: { dir: { type: "string" } },
14804
- run: (ctx, args) => {
14805
- const dir = stringOption(args.values, "dir");
15704
+ run: (ctx, args2) => {
15705
+ const dir = stringOption(args2.values, "dir");
14806
15706
  return Promise.resolve(runI18nCheck(ctx, dir === void 0 ? void 0 : toAbsolutePath(dir, ctx.cwd)));
14807
15707
  }
14808
15708
  };
14809
15709
 
14810
15710
  // src/history/look-back.ts
14811
- import { join as join36 } from "node:path";
15711
+ import { join as join38 } from "node:path";
14812
15712
 
14813
15713
  // src/history/patterns.ts
14814
- import { posix as posix4 } from "node:path";
15714
+ import { posix as posix5 } from "node:path";
14815
15715
  var PATTERNS = ["done_after_failing_tests", "env_read", "no_verify", "force_push", "test_skip"];
14816
15716
  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
15717
  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 +15733,7 @@ var stringField = (tool, name) => {
14833
15733
  return typeof value === "string" ? value : "";
14834
15734
  };
14835
15735
  var isEnvPath = (path) => {
14836
- const name = posix4.basename(path.replace(/\\/g, "/"));
15736
+ const name = posix5.basename(path.replace(/\\/g, "/"));
14837
15737
  return /^\.env(\.[A-Za-z0-9_-]+)?$/.test(name) && !ENV_TEMPLATE.test(name);
14838
15738
  };
14839
15739
  var envRead = (tool) => {
@@ -14921,7 +15821,7 @@ var addResults = (draft, blocks) => {
14921
15821
  };
14922
15822
  var parseTranscript = (jsonl) => {
14923
15823
  const turns = [];
14924
- let current;
15824
+ let current2;
14925
15825
  for (const line2 of jsonl.split(/\r?\n/)) {
14926
15826
  if (line2.trim() === "") continue;
14927
15827
  const parsed = parseJsonObject(line2);
@@ -14929,13 +15829,13 @@ var parseTranscript = (jsonl) => {
14929
15829
  const entry2 = parsed.value;
14930
15830
  const blocks = blocksOf(entry2["message"]);
14931
15831
  if (entry2["type"] === "user") {
14932
- if (current !== void 0 && addResults(current, blocks)) continue;
15832
+ if (current2 !== void 0 && addResults(current2, blocks)) continue;
14933
15833
  const prompt = blocks.map(textOf3).join("\n").trim();
14934
15834
  if (prompt === "") continue;
14935
- current = startTurn(prompt);
14936
- turns.push(current);
14937
- } else if (entry2["type"] === "assistant" && current !== void 0) {
14938
- addAssistant(current, blocks);
15835
+ current2 = startTurn(prompt);
15836
+ turns.push(current2);
15837
+ } else if (entry2["type"] === "assistant" && current2 !== void 0) {
15838
+ addAssistant(current2, blocks);
14939
15839
  }
14940
15840
  }
14941
15841
  return turns;
@@ -14944,7 +15844,7 @@ var parseTranscript = (jsonl) => {
14944
15844
  // src/history/look-back.ts
14945
15845
  var SAMPLES = 3;
14946
15846
  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));
15847
+ var projectLogDir = (runtime, root, env) => join38(env["CLAUDE_CONFIG_DIR"] ?? join38(runtime.homeDir(), ".claude"), "projects", projectLogKey(root));
14948
15848
  var lookBack = (runtime, root, env) => {
14949
15849
  const dir = projectLogDir(runtime, root, env);
14950
15850
  const listed4 = runtime.listDir(toAbsolutePath(dir));
@@ -14965,7 +15865,7 @@ var lookBack = (runtime, root, env) => {
14965
15865
  };
14966
15866
  let turns = 0;
14967
15867
  for (const name of files) {
14968
- const text6 = runtime.readText(toAbsolutePath(join36(dir, name)));
15868
+ const text6 = runtime.readText(toAbsolutePath(join38(dir, name)));
14969
15869
  if (!text6.ok) continue;
14970
15870
  for (const turn of parseTranscript(text6.value)) {
14971
15871
  turns += 1;
@@ -15146,11 +16046,11 @@ var runInstall = (ctx, request, presetFallback) => {
15146
16046
  if (ctx.dryRun || !ctx.yes || !needsWrite(plan.value)) return presentPlan(ctx, request, plan.value);
15147
16047
  return applyAndReport(ctx, request, plan.value);
15148
16048
  };
15149
- var initRequest = (args) => ({
16049
+ var initRequest = (args2) => ({
15150
16050
  mode: "init",
15151
- preset: stringOption(args.values, "preset"),
15152
- bin: stringOption(args.values, "bin"),
15153
- marketplace: args.values["marketplace"] === true,
16051
+ preset: stringOption(args2.values, "preset"),
16052
+ bin: stringOption(args2.values, "bin"),
16053
+ marketplace: args2.values["marketplace"] === true,
15154
16054
  showDiff: false
15155
16055
  });
15156
16056
  var withLookBack = (ctx, result) => {
@@ -15175,10 +16075,36 @@ var initCommand = {
15175
16075
  marketplace: { type: "boolean" },
15176
16076
  "look-back": { type: "boolean" }
15177
16077
  },
15178
- run: (ctx, args) => {
15179
- const result = runInstall(ctx, initRequest(args), "neutral");
15180
- return Promise.resolve(args.values["look-back"] === true ? withLookBack(ctx, result) : result);
15181
- }
16078
+ run: (ctx, args2) => {
16079
+ const result = runInstall(ctx, initRequest(args2), "neutral");
16080
+ return Promise.resolve(args2.values["look-back"] === true ? withLookBack(ctx, result) : result);
16081
+ }
16082
+ };
16083
+
16084
+ // src/commands/policy.ts
16085
+ var FIX = `finitemonkey policy false-positive <${OPTIONAL_GUARDS.join("|")}>`;
16086
+ var recordFalsePositive = (ctx, guard) => {
16087
+ if (!isPerson(ctx)) return failureFrom(ctx.t, needsPersonProblem("policy.error.person_only", FIX));
16088
+ if (!OPTIONAL_GUARDS.some((id) => id === guard)) {
16089
+ return failureFrom(ctx.t, usageProblem("invalid_arguments", "error.invalid_arguments", FIX, { detail: guard }));
16090
+ }
16091
+ const paths = fmPaths(findRepoRoot(ctx.runtime, ctx.cwd) ?? ctx.cwd);
16092
+ const state = readShadow(ctx.runtime, paths);
16093
+ const before = state[guard] ?? { firings: 0, falsePositives: 0, promoted: false };
16094
+ const falsePositives = before.falsePositives + 1;
16095
+ writeShadow(ctx.runtime, paths, { ...state, [guard]: { ...before, falsePositives } });
16096
+ logEvent(ctx.runtime, paths, "guard_false_positive", { guard, falsePositives });
16097
+ return success({ guard, falsePositives }, [
16098
+ ctx.t("policy.false_positive.recorded", { guard, count: falsePositives })
16099
+ ]);
16100
+ };
16101
+ var policyFalsePositiveCommand = {
16102
+ path: ["policy", "false-positive"],
16103
+ summaryKey: "command.policy_false_positive.summary",
16104
+ usage: FIX,
16105
+ status: "ready",
16106
+ maxPositionals: 1,
16107
+ run: (ctx, args2) => Promise.resolve(recordFalsePositive(ctx, args2.positionals[0] ?? ""))
15182
16108
  };
15183
16109
 
15184
16110
  // src/commands/prose-check.ts
@@ -15304,10 +16230,10 @@ var proseCheckCommand = {
15304
16230
  status: "ready",
15305
16231
  options: { stdin: { type: "boolean" }, record: { type: "boolean" } },
15306
16232
  maxPositionals: 1e3,
15307
- run: async (ctx, args) => {
16233
+ run: async (ctx, args2) => {
15308
16234
  const root = projectRoot(ctx);
15309
- const mode = { stdin: args.values["stdin"] === true, record: args.values["record"] === true };
15310
- const reports = await reportsOf(ctx, root, args.positionals, mode);
16235
+ const mode = { stdin: args2.values["stdin"] === true, record: args2.values["record"] === true };
16236
+ const reports = await reportsOf(ctx, root, args2.positionals, mode);
15311
16237
  if (!reports.ok) return failureFrom(ctx.t, reports.error);
15312
16238
  return resultOf2(ctx, reports.value);
15313
16239
  }
@@ -15362,7 +16288,7 @@ var inputProblem = (ctx, id, reason, days) => {
15362
16288
  }
15363
16289
  return void 0;
15364
16290
  };
15365
- var entryOf3 = (ctx, finding3, args) => {
16291
+ var entryOf3 = (ctx, finding3, args2) => {
15366
16292
  const now = ctx.runtime.now();
15367
16293
  return {
15368
16294
  id: finding3.id,
@@ -15370,30 +16296,30 @@ var entryOf3 = (ctx, finding3, args) => {
15370
16296
  file: finding3.file,
15371
16297
  line: finding3.line,
15372
16298
  fingerprint: finding3.fingerprint,
15373
- state: args.falsePositive ? "falso-positivo" : "aceito",
16299
+ state: args2.falsePositive ? "falso-positivo" : "aceito",
15374
16300
  owner: personName(ctx),
15375
- reason: args.reason,
16301
+ reason: args2.reason,
15376
16302
  acceptedAt: now,
15377
- expiresAt: plusDays(now, args.days)
16303
+ expiresAt: plusDays(now, args2.days)
15378
16304
  };
15379
16305
  };
15380
- var acceptEntry = (ctx, args) => {
16306
+ var acceptEntry = (ctx, args2) => {
15381
16307
  const root = projectRoot(ctx);
15382
16308
  const baseline = loadBaseline(ctx.runtime, root);
15383
16309
  if (!baseline.ok) {
15384
16310
  return failure3(ctx, baselineInvalid(baseline.error));
15385
16311
  }
15386
16312
  const scanned = scanPath(ctx.runtime, root, ".");
15387
- const finding3 = scanned.ok ? scanned.value.find((item) => item.id === args.id) : void 0;
16313
+ const finding3 = scanned.ok ? scanned.value.find((item) => item.id === args2.id) : void 0;
15388
16314
  if (finding3 === void 0) {
15389
16315
  return failure3(
15390
16316
  ctx,
15391
16317
  usageProblem("secrets_finding_not_found", "secrets.error.finding_not_found", "finitemonkey secrets scan", {
15392
- id: args.id
16318
+ id: args2.id
15393
16319
  })
15394
16320
  );
15395
16321
  }
15396
- const entry2 = entryOf3(ctx, finding3, args);
16322
+ const entry2 = entryOf3(ctx, finding3, args2);
15397
16323
  const entries = [...baseline.value.entries.filter((item) => item.id !== entry2.id), entry2];
15398
16324
  const saved = saveBaseline(ctx.runtime, root, { schema: baseline.value.schema, entries });
15399
16325
  if (!saved.ok) {
@@ -15416,10 +16342,10 @@ var secretsAcceptCommand = {
15416
16342
  status: "ready",
15417
16343
  maxPositionals: 1,
15418
16344
  options: { reason: { type: "string" }, days: { type: "string" }, "false-positive": { type: "boolean" } },
15419
- run: (ctx, args) => {
15420
- const id = args.positionals[0] ?? "";
15421
- const reason = stringOption(args.values, "reason");
15422
- const days = daysOf(args.values);
16345
+ run: (ctx, args2) => {
16346
+ const id = args2.positionals[0] ?? "";
16347
+ const reason = stringOption(args2.values, "reason");
16348
+ const days = daysOf(args2.values);
15423
16349
  const problem4 = inputProblem(ctx, id, reason, days);
15424
16350
  if (problem4 !== void 0 || reason === void 0 || days === void 0) {
15425
16351
  return Promise.resolve(
@@ -15430,7 +16356,7 @@ var secretsAcceptCommand = {
15430
16356
  );
15431
16357
  }
15432
16358
  return Promise.resolve(
15433
- acceptEntry(ctx, { id, reason, days, falsePositive: args.values["false-positive"] === true })
16359
+ acceptEntry(ctx, { id, reason, days, falsePositive: args2.values["false-positive"] === true })
15434
16360
  );
15435
16361
  }
15436
16362
  };
@@ -15511,18 +16437,18 @@ var secretsIncidentCommand = {
15511
16437
  usage: USAGE,
15512
16438
  status: "ready",
15513
16439
  maxPositionals: 2,
15514
- run: (ctx, args) => {
15515
- const action = actionOf(args.positionals[0]);
16440
+ run: (ctx, args2) => {
16441
+ const action = actionOf(args2.positionals[0]);
15516
16442
  const root = projectRoot(ctx);
15517
16443
  if (action === void 0)
15518
16444
  return Promise.resolve(
15519
16445
  fail2(
15520
16446
  ctx,
15521
- usageProblem("invalid_arguments", "error.invalid_arguments", USAGE, { detail: args.positionals[0] ?? "" })
16447
+ usageProblem("invalid_arguments", "error.invalid_arguments", USAGE, { detail: args2.positionals[0] ?? "" })
15522
16448
  )
15523
16449
  );
15524
16450
  if (action === "list") return Promise.resolve(list2(ctx, root));
15525
- const id = args.positionals[1] ?? "";
16451
+ const id = args2.positionals[1] ?? "";
15526
16452
  const incident = readIncident(ctx.runtime, root, id);
15527
16453
  if (!incident.ok) return Promise.resolve(notFound2(ctx, id));
15528
16454
  return Promise.resolve(action === "purge" ? purge(ctx, incident.value) : move(ctx, root, incident.value, action));
@@ -15543,7 +16469,7 @@ var view = (finding3, status) => ({
15543
16469
  status,
15544
16470
  ...finding3.commit === void 0 ? {} : { commit: finding3.commit }
15545
16471
  });
15546
- var modeOf2 = (values) => {
16472
+ var modeOf3 = (values) => {
15547
16473
  const chosen = [values["staged"] === true, values["history"] === true, stringOption(values, "path") !== void 0];
15548
16474
  if (chosen.filter(Boolean).length > 1) return void 0;
15549
16475
  if (chosen[0] === true) return "staged";
@@ -15572,25 +16498,25 @@ var historyIncidents = (ctx, root, fresh3) => fresh3.flatMap((finding3) => {
15572
16498
  const saved = recordIncident(ctx.runtime, root, draft, HOOK_ACTOR);
15573
16499
  return saved.ok ? [saved.value.id] : [];
15574
16500
  });
15575
- var dataOf = (mode, outcome, incidents) => {
16501
+ var dataOf = (mode, outcome2, incidents) => {
15576
16502
  const findings = [
15577
- ...outcome.fresh.map((finding3) => view(finding3, "new")),
15578
- ...outcome.known.map((finding3) => view(finding3, "known")),
15579
- ...outcome.expired.map((finding3) => view(finding3, "expired"))
16503
+ ...outcome2.fresh.map((finding3) => view(finding3, "new")),
16504
+ ...outcome2.known.map((finding3) => view(finding3, "known")),
16505
+ ...outcome2.expired.map((finding3) => view(finding3, "expired"))
15580
16506
  ];
15581
16507
  return {
15582
16508
  schema: SECRETS_SCAN_SCHEMA,
15583
16509
  mode,
15584
16510
  baseline: BASELINE_FILE2,
15585
16511
  findings,
15586
- fixed: outcome.fixed.map((entry2) => entry2.id),
16512
+ fixed: outcome2.fixed.map((entry2) => entry2.id),
15587
16513
  incidents,
15588
16514
  summary: {
15589
16515
  total: findings.length,
15590
- new: outcome.fresh.length,
15591
- known: outcome.known.length,
15592
- expired: outcome.expired.length,
15593
- fixed: outcome.fixed.length
16516
+ new: outcome2.fresh.length,
16517
+ known: outcome2.known.length,
16518
+ expired: outcome2.expired.length,
16519
+ fixed: outcome2.fixed.length
15594
16520
  }
15595
16521
  };
15596
16522
  };
@@ -15624,8 +16550,8 @@ var resultOf3 = (ctx, data) => {
15624
16550
  human2
15625
16551
  );
15626
16552
  };
15627
- var prune = (ctx, root, baseline, outcome) => {
15628
- if (outcome.fixed.length > 0) saveBaseline(ctx.runtime, root, withoutFixed(baseline, outcome.fixed));
16553
+ var prune = (ctx, root, baseline, outcome2) => {
16554
+ if (outcome2.fixed.length > 0) saveBaseline(ctx.runtime, root, withoutFixed(baseline, outcome2.fixed));
15629
16555
  };
15630
16556
  var secretsScanCommand = {
15631
16557
  path: ["secrets", "scan"],
@@ -15633,30 +16559,30 @@ var secretsScanCommand = {
15633
16559
  usage: "finitemonkey secrets scan [--staged | --history | --path <dir>] [--json]",
15634
16560
  status: "ready",
15635
16561
  options: { staged: { type: "boolean" }, history: { type: "boolean" }, path: { type: "string" } },
15636
- run: async (ctx, args) => {
15637
- const mode = modeOf2(args.values);
16562
+ run: async (ctx, args2) => {
16563
+ const mode = modeOf3(args2.values);
15638
16564
  const fix = "finitemonkey help secrets scan";
15639
16565
  if (mode === void 0) return failureFrom(ctx.t, usageProblem("invalid_arguments", "secrets.error.one_mode", fix));
15640
16566
  const root = projectRoot(ctx);
15641
- const target = stringOption(args.values, "path") ?? ".";
16567
+ const target = stringOption(args2.values, "path") ?? ".";
15642
16568
  const scanned = await runScan(ctx, root, mode, target);
15643
16569
  if (!scanned.ok) return failureFrom(ctx.t, gitUnavailable(scanned.error));
15644
16570
  const baseline = loadBaseline(ctx.runtime, root);
15645
16571
  if (!baseline.ok) return failureFrom(ctx.t, baselineInvalid(baseline.error));
15646
- const outcome = ratchet(
16572
+ const outcome2 = ratchet(
15647
16573
  scanned.value,
15648
16574
  baseline.value,
15649
16575
  ctx.runtime.now(),
15650
16576
  mode === "path" ? scopeOf(target) : void 0
15651
16577
  );
15652
- if (mode === "path") prune(ctx, root, baseline.value, outcome);
15653
- const incidents = mode === "history" ? historyIncidents(ctx, root, outcome.fresh) : [];
15654
- return resultOf3(ctx, dataOf(mode, outcome, incidents));
16578
+ if (mode === "path") prune(ctx, root, baseline.value, outcome2);
16579
+ const incidents = mode === "history" ? historyIncidents(ctx, root, outcome2.fresh) : [];
16580
+ return resultOf3(ctx, dataOf(mode, outcome2, incidents));
15655
16581
  }
15656
16582
  };
15657
16583
 
15658
16584
  // src/task/create.ts
15659
- import { join as join37 } from "node:path";
16585
+ import { join as join39 } from "node:path";
15660
16586
 
15661
16587
  // src/core/yaml/write.ts
15662
16588
  var PLAIN_SAFE = /^[A-Za-z_./][A-Za-z0-9_./ ()-]*$/;
@@ -15702,7 +16628,11 @@ var checkYaml = (check2) => ({
15702
16628
  ...check2.timeoutS === void 0 ? {} : { timeout_s: check2.timeoutS },
15703
16629
  ...check2.require === void 0 ? {} : { require: [...check2.require] },
15704
16630
  ...check2.requireOpen === void 0 ? {} : { require_open: check2.requireOpen },
15705
- ...check2.description === void 0 ? {} : { description: check2.description }
16631
+ ...check2.description === void 0 ? {} : { description: check2.description },
16632
+ ...check2.port === void 0 ? {} : { port: check2.port },
16633
+ ...check2.route === void 0 ? {} : { route: check2.route },
16634
+ ...check2.status === void 0 ? {} : { status: check2.status },
16635
+ ...check2.body === void 0 ? {} : { body: check2.body }
15706
16636
  });
15707
16637
  var contractYaml = (contract) => ({
15708
16638
  id: contract.id,
@@ -15715,7 +16645,8 @@ var contractYaml = (contract) => ({
15715
16645
  tests_protected: contract.scope.testsProtected
15716
16646
  },
15717
16647
  acceptance: contract.acceptance.map(checkYaml),
15718
- decisions_needed: [...contract.decisionsNeeded]
16648
+ decisions_needed: [...contract.decisionsNeeded],
16649
+ ...contract.callersIgnore === void 0 ? {} : { callers_ignore: [...contract.callersIgnore] }
15719
16650
  });
15720
16651
  var contractJson = (contract) => ({
15721
16652
  id: contract.id,
@@ -15740,10 +16671,10 @@ var explicitChecks = (draft) => [
15740
16671
  var draftChecks = (runtime, paths, draft) => {
15741
16672
  const disk = {
15742
16673
  read: (relative9) => {
15743
- const text6 = runtime.readText(toAbsolutePath(join37(paths.root, relative9)));
16674
+ const text6 = runtime.readText(toAbsolutePath(join39(paths.root, relative9)));
15744
16675
  return text6.ok ? text6.value : void 0;
15745
16676
  },
15746
- exists: (relative9) => runtime.stat(toAbsolutePath(join37(paths.root, relative9))) !== void 0
16677
+ exists: (relative9) => runtime.stat(toAbsolutePath(join39(paths.root, relative9))) !== void 0
15747
16678
  };
15748
16679
  const detected = detectChecks(disk);
15749
16680
  const base = draft.runs.length > 0 || draft.tests !== void 0 ? explicitChecks(draft) : detected;
@@ -15756,6 +16687,7 @@ var draftChecks = (runtime, paths, draft) => {
15756
16687
  { id: "prose", kind: "prose" },
15757
16688
  { id: "worktree", kind: "worktree" },
15758
16689
  { id: "merge", kind: "merge" },
16690
+ { id: "callers", kind: "callers" },
15759
16691
  ...draft.paths.length > 0 || draft.budget !== void 0 ? [{ id: "diff", kind: "diff" }] : [],
15760
16692
  ...draft.git.length > 0 ? [{ id: "git", kind: "git", require: draft.git }] : [],
15761
16693
  { id: "pr", kind: "pr", requireOpen: draft.git.includes("pr_open") },
@@ -15816,13 +16748,13 @@ var list3 = (values, name) => {
15816
16748
  return raw.filter((item) => typeof item === "string" && item.trim() !== "");
15817
16749
  };
15818
16750
  var csv = (values, name) => list3(values, name).flatMap((item) => item.split(",").map((part2) => part2.trim())).filter((part2) => part2 !== "");
15819
- var positive2 = (values, name) => {
16751
+ var positive3 = (values, name) => {
15820
16752
  const parsed = Number.parseInt(stringOption(values, name) ?? "", 10);
15821
16753
  return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
15822
16754
  };
15823
16755
  var draftFrom = (ctx, paths, id, values) => {
15824
- const files = positive2(values, "budget-files");
15825
- const lines3 = positive2(values, "budget-lines");
16756
+ const files = positive3(values, "budget-files");
16757
+ const lines3 = positive3(values, "budget-lines");
15826
16758
  const size = SIZES.find((item) => item === stringOption(values, "size")) ?? "S";
15827
16759
  return {
15828
16760
  id,
@@ -15925,7 +16857,7 @@ var taskNewCommand = {
15925
16857
  status: "ready",
15926
16858
  options: TASK_OPTIONS,
15927
16859
  maxPositionals: 1,
15928
- run: (ctx, args) => Promise.resolve(newTask(ctx, args.positionals[0], args.values))
16860
+ run: (ctx, args2) => Promise.resolve(newTask(ctx, args2.positionals[0], args2.values))
15929
16861
  };
15930
16862
  var taskShowCommand = {
15931
16863
  path: ["task", "show"],
@@ -15933,7 +16865,7 @@ var taskShowCommand = {
15933
16865
  usage: "finitemonkey task show [<id>]",
15934
16866
  status: "ready",
15935
16867
  maxPositionals: 1,
15936
- run: (ctx, args) => Promise.resolve(showTask(ctx, args.positionals[0]))
16868
+ run: (ctx, args2) => Promise.resolve(showTask(ctx, args2.positionals[0]))
15937
16869
  };
15938
16870
  var taskDoneCommand = {
15939
16871
  path: ["task", "done"],
@@ -15941,19 +16873,19 @@ var taskDoneCommand = {
15941
16873
  usage: "finitemonkey task done [<id>]",
15942
16874
  status: "ready",
15943
16875
  maxPositionals: 1,
15944
- run: (ctx, args) => Promise.resolve(doneTask(ctx, args.positionals[0]))
16876
+ run: (ctx, args2) => Promise.resolve(doneTask(ctx, args2.positionals[0]))
15945
16877
  };
15946
16878
 
15947
16879
  // src/install/uninstall.ts
15948
- var structural = (entry2, current) => {
16880
+ var structural = (entry2, current2) => {
15949
16881
  if (entry2.kind === "gitignore" || entry2.kind === "markdown") {
15950
- const text6 = entry2.kind === "gitignore" ? withoutBlock(current) : withoutMarkdownBlock(current);
16882
+ const text6 = entry2.kind === "gitignore" ? withoutBlock(current2) : withoutMarkdownBlock(current2);
15951
16883
  return text6 === "" && !entry2.existed ? void 0 : text6;
15952
16884
  }
15953
- const parsed = parseSettings(current);
15954
- if (!parsed.ok || entry2.added === void 0) return current;
16885
+ const parsed = parseSettings(current2);
16886
+ if (!parsed.ok || entry2.added === void 0) return current2;
15955
16887
  const settings = unmergeSettings(parsed.value, entry2.added, entry2.handlers ?? []);
15956
- return Object.keys(settings).length === 0 && !entry2.existed ? void 0 : renderJson(settings, styleOf(current));
16888
+ return Object.keys(settings).length === 0 && !entry2.existed ? void 0 : renderJson(settings, styleOf(current2));
15957
16889
  };
15958
16890
  var unchanged = (project, entry2) => {
15959
16891
  if (!entry2.existed) return { path: entry2.path, action: "delete", after: void 0 };
@@ -15961,17 +16893,17 @@ var unchanged = (project, entry2) => {
15961
16893
  return original === void 0 ? void 0 : { path: entry2.path, action: "restore", after: original };
15962
16894
  };
15963
16895
  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 };
16896
+ const current2 = project.read(entry2.path);
16897
+ if (current2 === void 0) return { path: entry2.path, action: "gone", after: void 0 };
15966
16898
  if (entry2.kind === "generated" && options.keepGenerated) {
15967
- return { path: entry2.path, action: "keep-generated", after: current };
16899
+ return { path: entry2.path, action: "keep-generated", after: current2 };
15968
16900
  }
15969
- if (hashText(current) === entry2.hash) {
16901
+ if (hashText(current2) === entry2.hash) {
15970
16902
  const exact2 = unchanged(project, entry2);
15971
16903
  if (exact2 !== void 0) return exact2;
15972
16904
  }
15973
- if (entry2.kind === "generated") return { path: entry2.path, action: "keep-edited", after: current };
15974
- const after = structural(entry2, current);
16905
+ if (entry2.kind === "generated") return { path: entry2.path, action: "keep-edited", after: current2 };
16906
+ const after = structural(entry2, current2);
15975
16907
  return { path: entry2.path, action: after === void 0 ? "delete" : "unmerge", after };
15976
16908
  };
15977
16909
  var planRemoval = (project, lock, options) => ({
@@ -15991,7 +16923,7 @@ var isEmptyDir = (project, relative9) => {
15991
16923
  const listed4 = project.runtime.listDir(project.abs(relative9));
15992
16924
  return listed4.ok && listed4.value.length === 0;
15993
16925
  };
15994
- var finish2 = (project, plan) => {
16926
+ var finish3 = (project, plan) => {
15995
16927
  const target = plan.purge ? STATE_DIR : LOCK_FILE;
15996
16928
  const removed = project.runtime.remove(project.abs(target));
15997
16929
  if (!removed.ok) return reject({ path: target, detail: removed.error });
@@ -16006,7 +16938,7 @@ var applyRemoval = (project, plan) => {
16006
16938
  }
16007
16939
  return accept(void 0);
16008
16940
  });
16009
- return changed.ok ? finish2(project, plan) : changed;
16941
+ return changed.ok ? finish3(project, plan) : changed;
16010
16942
  };
16011
16943
 
16012
16944
  // src/commands/uninstall.ts
@@ -16037,22 +16969,22 @@ var linesOf2 = (ctx, data) => [
16037
16969
  ctx.t("install.uninstall.outside"),
16038
16970
  ...OUTSIDE.map(({ key, command }) => ` ${ctx.t(key, { command })}`)
16039
16971
  ];
16040
- var fixFor = (args) => [
16972
+ var fixFor = (args2) => [
16041
16973
  "finitemonkey uninstall --yes",
16042
- ...args.values["purge"] === true ? ["--purge"] : [],
16043
- ...args.values["keep-generated"] === true ? ["--keep-generated"] : []
16974
+ ...args2.values["purge"] === true ? ["--purge"] : [],
16975
+ ...args2.values["keep-generated"] === true ? ["--keep-generated"] : []
16044
16976
  ].join(" ");
16045
- var runUninstall = (ctx, args) => {
16977
+ var runUninstall = (ctx, args2) => {
16046
16978
  const { project } = openProject(ctx);
16047
16979
  const lock = readPreviousLock(project);
16048
16980
  if (!lock.ok) return failureFrom(ctx.t, planProblem(lock.error));
16049
16981
  if (lock.value === void 0) return failureFrom(ctx.t, notInstalled());
16050
- const options = { purge: args.values["purge"] === true, keepGenerated: args.values["keep-generated"] === true };
16982
+ const options = { purge: args2.values["purge"] === true, keepGenerated: args2.values["keep-generated"] === true };
16051
16983
  const plan = planRemoval(project, lock.value, options);
16052
16984
  const preview = dataOf2(project.root, plan, false);
16053
16985
  if (ctx.dryRun) return success(preview, [...linesOf2(ctx, preview), ctx.t("install.dry_run")]);
16054
16986
  if (!ctx.yes) {
16055
- const next2 = fixFor(args);
16987
+ const next2 = fixFor(args2);
16056
16988
  const problem4 = confirmProblem("uninstall", ctx.t("install.uninstall.question"), next2);
16057
16989
  return { ...failureFrom(ctx.t, { ...problem4, data: preview }, linesOf2(ctx, preview)), next: next2 };
16058
16990
  }
@@ -16067,7 +16999,7 @@ var uninstallCommand = {
16067
16999
  usage: "finitemonkey uninstall [--purge] [--keep-generated] [--dry-run] [--yes]",
16068
17000
  status: "ready",
16069
17001
  options: { purge: { type: "boolean" }, "keep-generated": { type: "boolean" } },
16070
- run: (ctx, args) => Promise.resolve(runUninstall(ctx, args))
17002
+ run: (ctx, args2) => Promise.resolve(runUninstall(ctx, args2))
16071
17003
  };
16072
17004
 
16073
17005
  // src/commands/update.ts
@@ -16090,14 +17022,14 @@ var check = (ctx, bin) => {
16090
17022
  error: { code: "install_outdated", message: ctx.t("error.install_outdated"), fix: "finitemonkey update --yes" }
16091
17023
  };
16092
17024
  };
16093
- var runUpdate = (ctx, args) => {
16094
- const bin = stringOption(args.values, "bin");
16095
- if (args.values["check"] === true) return check(ctx, bin);
17025
+ var runUpdate = (ctx, args2) => {
17026
+ const bin = stringOption(args2.values, "bin");
17027
+ if (args2.values["check"] === true) return check(ctx, bin);
16096
17028
  const { project } = openProject(ctx);
16097
17029
  const previous = readPreviousLock(project);
16098
17030
  if (!previous.ok) return failureFrom(ctx.t, planProblem(previous.error));
16099
17031
  if (previous.value === void 0) return failureFrom(ctx.t, notInstalled());
16100
- const preset = stringOption(args.values, "preset");
17032
+ const preset = stringOption(args2.values, "preset");
16101
17033
  const request = { mode: "update", preset, bin, marketplace: false, showDiff: true };
16102
17034
  return runInstall(ctx, request, previous.value.preset);
16103
17035
  };
@@ -16107,7 +17039,7 @@ var updateCommand = {
16107
17039
  usage: "finitemonkey update [--check] [--preset neutral|gabriel] [--bin <path>] [--yes]",
16108
17040
  status: "ready",
16109
17041
  options: { check: { type: "boolean" }, preset: { type: "string" }, bin: { type: "string" } },
16110
- run: (ctx, args) => Promise.resolve(runUpdate(ctx, args))
17042
+ run: (ctx, args2) => Promise.resolve(runUpdate(ctx, args2))
16111
17043
  };
16112
17044
 
16113
17045
  // src/commands/version.ts
@@ -16136,18 +17068,20 @@ var REGISTRY = [
16136
17068
  taskDoneCommand,
16137
17069
  secretsScanCommand,
16138
17070
  secretsAcceptCommand,
17071
+ policyFalsePositiveCommand,
16139
17072
  secretsIncidentCommand,
16140
17073
  proseCheckCommand,
16141
17074
  snapshotCommand,
16142
17075
  undoCommand,
16143
17076
  rescueCommand,
16144
17077
  mergeVerifyCommand,
17078
+ appCheckCommand,
16145
17079
  versionCommand,
16146
17080
  helpCommand
16147
17081
  ];
16148
17082
 
16149
17083
  // src/i18n/lang-settings.ts
16150
- import { join as join38 } from "node:path";
17084
+ import { join as join40 } from "node:path";
16151
17085
  var read3 = (runtime, path) => {
16152
17086
  const text6 = runtime.readText(toAbsolutePath(path));
16153
17087
  return text6.ok ? text6.value : void 0;
@@ -16162,14 +17096,14 @@ var languageIn = (text6) => {
16162
17096
  const value = parsed.ok && isRecord(parsed.value) ? parsed.value["language"] : void 0;
16163
17097
  return typeof value === "string" && value.trim() !== "" ? value : void 0;
16164
17098
  };
16165
- var claudeDir = (runtime, env) => env["CLAUDE_CONFIG_DIR"] ?? join38(runtime.homeDir(), ".claude");
17099
+ var claudeDir = (runtime, env) => env["CLAUDE_CONFIG_DIR"] ?? join40(runtime.homeDir(), ".claude");
16166
17100
  var langSettings = (runtime, cwd, env, platform) => {
16167
17101
  const root = findGitRoot(cwd, runtime) ?? cwd;
16168
- const config = parseConfigYaml(read3(runtime, join38(root, CONFIG_FILE)) ?? "");
17102
+ const config = parseConfigYaml(read3(runtime, join40(root, CONFIG_FILE)) ?? "");
16169
17103
  const claudeFiles = [
16170
- join38(root, ".claude", "settings.local.json"),
16171
- join38(root, ".claude", "settings.json"),
16172
- join38(claudeDir(runtime, env), "settings.json")
17104
+ join40(root, ".claude", "settings.local.json"),
17105
+ join40(root, ".claude", "settings.json"),
17106
+ join40(claudeDir(runtime, env), "settings.json")
16173
17107
  ];
16174
17108
  return {
16175
17109
  config: configText(config, "lang"),
@@ -16220,9 +17154,9 @@ var failWith = (ctx, name, problem4) => ({
16220
17154
  });
16221
17155
  var invoke = (ctx, argv, spec2, registry) => {
16222
17156
  const name = commandName(spec2);
16223
- const args = strictArgs(argv, spec2);
16224
- if (!args.ok) return failWith(ctx, name, args.error);
16225
- return { name, execute: () => spec2.run(ctx, args.value, registry) };
17157
+ const args2 = strictArgs(argv, spec2);
17158
+ if (!args2.ok) return failWith(ctx, name, args2.error);
17159
+ return { name, execute: () => spec2.run(ctx, args2.value, registry) };
16226
17160
  };
16227
17161
  var builtin = (registry, name) => registry.find((spec2) => commandName(spec2) === name);
16228
17162
  var dispatch = (ctx, argv, loose, registry) => {