finitemonkey 0.1.0-next.17 → 0.1.0-next.19

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.
@@ -1156,8 +1156,8 @@ var isOwnHandler = (handler) => {
1156
1156
  if (args2.length === 0) return OWN_SHELL.test(`${command} `);
1157
1157
  const hookAt = args2.indexOf("hook");
1158
1158
  if (hookAt < 0) return false;
1159
- const target = hookAt === 0 ? command : args2[hookAt - 1] ?? "";
1160
- return OWN_SCRIPT.test(target);
1159
+ const target2 = hookAt === 0 ? command : args2[hookAt - 1] ?? "";
1160
+ return OWN_SCRIPT.test(target2);
1161
1161
  };
1162
1162
 
1163
1163
  // src/install/skill-template.ts
@@ -3356,8 +3356,8 @@ var readRedirect = (cursor, match) => {
3356
3356
  cursor.pos += match[0].length;
3357
3357
  skipBlank(cursor);
3358
3358
  const processSub = peek(cursor) === "(" && op === "<";
3359
- const target = processSub ? "" : readWord(cursor).text;
3360
- return { kind: "redirect", op, fd: match[1] ?? "", target, body: void 0 };
3359
+ const target2 = processSub ? "" : readWord(cursor).text;
3360
+ return { kind: "redirect", op, fd: match[1] ?? "", target: target2, body: void 0 };
3361
3361
  };
3362
3362
  var processSubstitution = (cursor) => {
3363
3363
  if (peek(cursor) !== "<" && peek(cursor) !== ">" || peek(cursor, 1) !== "(") return false;
@@ -3536,8 +3536,8 @@ var next = (cursor, tokens, cmd) => {
3536
3536
  if (redirect !== null) {
3537
3537
  cursor.pos += redirect[0].length;
3538
3538
  while (peek(cursor) === " ") cursor.pos += 1;
3539
- const target = redirect[3] ?? word(cursor, cmd).text;
3540
- tokens.push({ kind: "redirect", op: redirect[2] ?? ">", fd: redirect[1] ?? "", target, body: void 0 });
3539
+ const target2 = redirect[3] ?? word(cursor, cmd).text;
3540
+ tokens.push({ kind: "redirect", op: redirect[2] ?? ">", fd: redirect[1] ?? "", target: target2, body: void 0 });
3541
3541
  return;
3542
3542
  }
3543
3543
  const op = OPERATORS2.find((candidate) => cursor.text.startsWith(candidate, cursor.pos));
@@ -3759,8 +3759,8 @@ var PS_ENV = /^\$env:([A-Za-z_][A-Za-z0-9_]*)=?(.*)$/is;
3759
3759
  var CHANGES_DIR = /* @__PURE__ */ new Set(["cd", "pushd", "set-location", "push-location"]);
3760
3760
  var lex = (text6, language) => language === "bash" ? lexBash(text6) : lexPowerShell(text6, language === "cmd");
3761
3761
  var isWindowsAbsolute = (path) => /^[A-Za-z]:\//.test(path);
3762
- var joinDir = (current2, target) => {
3763
- const next2 = toPosixPath(target);
3762
+ var joinDir = (current2, target2) => {
3763
+ const next2 = toPosixPath(target2);
3764
3764
  if (next2.startsWith("/") || isWindowsAbsolute(next2) || next2.startsWith("~")) return next2;
3765
3765
  return current2 === void 0 ? posix.normalize(next2) : posix.join(current2, next2);
3766
3766
  };
@@ -3785,8 +3785,8 @@ var track = (words, language, context) => {
3785
3785
  if (assigned !== void 0) context.env = { ...context.env, ...assigned };
3786
3786
  const program = language === "bash" ? words[0] ?? "" : powershellProgram(words[0] ?? "");
3787
3787
  if (!CHANGES_DIR.has(program)) return;
3788
- const target = words.slice(1).find((word2) => !word2.startsWith("-"));
3789
- context.dir = target === void 0 || target === "-" ? "~" : joinDir(context.dir, target);
3788
+ const target2 = words.slice(1).find((word2) => !word2.startsWith("-"));
3789
+ context.dir = target2 === void 0 || target2 === "-" ? "~" : joinDir(context.dir, target2);
3790
3790
  };
3791
3791
  var emit = (stage, raw, language, context, collector) => {
3792
3792
  const first = stage.argv[0] ?? "";
@@ -3923,10 +3923,21 @@ var gitFindings = (args2) => {
3923
3923
  return found;
3924
3924
  };
3925
3925
  var FM_NAMES = /* @__PURE__ */ new Set(["finitemonkey", "fm"]);
3926
+ var FM_SCRIPT = /(^|[\\/])(finitemonkey|fm)(\.c?m?js)?$/;
3927
+ var RUNNERS = /* @__PURE__ */ new Set(["npx", "pnpm", "bunx", "yarn"]);
3928
+ var fmStart = (argv) => {
3929
+ const first = argv[0] ?? "";
3930
+ if (RUNNERS.has(first)) return argv.findIndex((arg) => FM_NAMES.has(arg.replace(/@.*$/, "")));
3931
+ if (first === "node" || first === "bun" || first === "deno") return FM_SCRIPT.test(argv[1] ?? "") ? 1 : -1;
3932
+ return FM_SCRIPT.test(first) ? 0 : -1;
3933
+ };
3926
3934
  var fmFindings = (argv) => {
3927
- const start = argv[0] === "npx" || argv[0] === "pnpm" || argv[0] === "bunx" ? argv.findIndex((arg) => FM_NAMES.has(arg.replace(/@.*$/, ""))) : 0;
3928
- const name = (argv[start] ?? "").replace(/@.*$/, "");
3929
- return start >= 0 && FM_NAMES.has(name) && argv[start + 1] === "allow" ? [{ kind: "fm_allow", detail: "finitemonkey allow" }] : [];
3935
+ const start = fmStart(argv);
3936
+ if (start < 0) return [];
3937
+ const sub = argv[start + 1];
3938
+ if (sub === "allow") return [{ kind: "fm_allow", detail: "finitemonkey allow" }];
3939
+ if (sub === "off" || sub === "on") return [{ kind: "fm_pause", detail: `finitemonkey ${sub}` }];
3940
+ return [];
3930
3941
  };
3931
3942
  var detectBypass = (commandLine) => parseShell(commandLine).flatMap((command) => [
3932
3943
  ...envFindings(command),
@@ -4244,9 +4255,6 @@ var testsGuard = (input) => {
4244
4255
  return deny(t("gate.guard.tests.model", { detail: reason }), t("gate.guard.tests.human", { detail: reason }));
4245
4256
  };
4246
4257
 
4247
- // src/gate/hooks/config.ts
4248
- import { join as join13 } from "node:path";
4249
-
4250
4258
  // src/gate/hooks/common.ts
4251
4259
  import { dirname as dirname5, join as join12 } from "node:path";
4252
4260
  var DEFAULT_GATE_BUDGET_MS = 3e5;
@@ -4277,7 +4285,90 @@ var contextOutput = (event, additionalContext, systemMessage) => ({
4277
4285
  });
4278
4286
  var human = (text6) => `finitemonkey: ${text6}`;
4279
4287
 
4288
+ // src/guards/release/markers.ts
4289
+ var LINE = /^\s*(?:finitemonkey|fm):\s+(allow|approve|trust)\s+(\S+)(?:\s+(.*))?$/i;
4290
+ var NAME = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,80}$/;
4291
+ var NOT_FROM_PERSON = [
4292
+ /<task-notification\b/i,
4293
+ /<agent-message\b/i,
4294
+ /^\s*\[julius2claude\]/,
4295
+ /\[Subagent hand-back\]/i
4296
+ ];
4297
+ var comesFromPerson = (prompt) => !NOT_FROM_PERSON.some((pattern) => pattern.test(prompt));
4298
+ var toMarker = (verb, target2, rest) => {
4299
+ if (!NAME.test(target2)) return void 0;
4300
+ if (verb === "trust") return { type: "trust", task: target2 };
4301
+ if (verb === "allow") {
4302
+ return rest.length >= 3 ? { type: "allow", guard: target2.toLowerCase(), reason: rest } : void 0;
4303
+ }
4304
+ const slash = target2.indexOf("/");
4305
+ const task = slash < 0 ? void 0 : target2.slice(0, slash);
4306
+ const check2 = slash < 0 ? target2 : target2.slice(slash + 1);
4307
+ if (check2 === "") return void 0;
4308
+ return { type: "approve", check: check2, reason: rest, ...task === void 0 || task === "" ? {} : { task } };
4309
+ };
4310
+ var parseMarkers = (prompt) => {
4311
+ if (!comesFromPerson(prompt)) return [];
4312
+ return prompt.split(/\r?\n/).flatMap((line2) => {
4313
+ const match = LINE.exec(line2);
4314
+ if (match === null) return [];
4315
+ const marker = toMarker((match[1] ?? "").toLowerCase(), match[2] ?? "", (match[3] ?? "").trim());
4316
+ return marker === void 0 ? [] : [marker];
4317
+ });
4318
+ };
4319
+
4320
+ // src/policy/pause.ts
4321
+ var PAUSE_FILE = "pause.json";
4322
+ var readPause = (runtime, paths) => {
4323
+ const text6 = runtime.readText(paths.cache(PAUSE_FILE));
4324
+ const parsed = text6.ok ? parseJsonObject(text6.value) : void 0;
4325
+ if (parsed?.ok !== true || !isRecord(parsed.value)) return void 0;
4326
+ const { sessionId, since, by } = parsed.value;
4327
+ if (typeof sessionId !== "string" || typeof since !== "string") return void 0;
4328
+ return { sessionId, since, by: by === "terminal" ? "terminal" : "prompt" };
4329
+ };
4330
+ var pausedFor = (runtime, paths, sessionId) => {
4331
+ const pause = readPause(runtime, paths);
4332
+ return pause !== void 0 && sessionId !== void 0 && pause.sessionId === sessionId ? pause : void 0;
4333
+ };
4334
+ var setPause = (runtime, paths, sessionId, by) => {
4335
+ const pause = { sessionId, since: runtime.now(), by };
4336
+ runtime.writeText(paths.cache(PAUSE_FILE), `${JSON.stringify(pause)}
4337
+ `);
4338
+ logEvent(runtime, paths, "gate_paused", { sessionId, by });
4339
+ return pause;
4340
+ };
4341
+ var clearPause = (runtime, paths, by) => {
4342
+ const pause = readPause(runtime, paths);
4343
+ if (pause === void 0) return void 0;
4344
+ runtime.remove(paths.cache(PAUSE_FILE));
4345
+ logEvent(runtime, paths, "gate_resumed", { sessionId: pause.sessionId, since: pause.since, by });
4346
+ return pause;
4347
+ };
4348
+
4349
+ // src/policy/pause-hooks.ts
4350
+ var COMMAND = /^\s*(fm|finitemonkey)\s+(off|on)\s*$/i;
4351
+ var pausePrompt = (input) => {
4352
+ const prompt = textField(input.payload.raw, "prompt", "prompt_text") ?? "";
4353
+ const match = COMMAND.exec(prompt);
4354
+ if (match === null || !comesFromPerson(prompt)) return { kind: "allow" };
4355
+ const paths = projectPaths(input);
4356
+ const sessionId = input.payload.sessionId;
4357
+ if (paths === void 0 || sessionId === void 0) return { kind: "allow" };
4358
+ const off = (match[2] ?? "").toLowerCase() === "off";
4359
+ if (off) setPause(input.runtime, paths, sessionId, "prompt");
4360
+ else clearPause(input.runtime, paths, "prompt");
4361
+ return { kind: "block", reason: human(input.t(off ? "pause.off" : "pause.on")) };
4362
+ };
4363
+ var unlessPaused = (handler) => (input) => {
4364
+ const paths = projectPaths(input);
4365
+ const pause = paths === void 0 ? void 0 : pausedFor(input.runtime, paths, input.payload.sessionId);
4366
+ if (pause === void 0) return handler(input);
4367
+ return allowWith({ systemMessage: human(input.t("pause.report", { since: pause.since })) });
4368
+ };
4369
+
4280
4370
  // src/gate/hooks/config.ts
4371
+ import { join as join13 } from "node:path";
4281
4372
  var CONFIG_FILE3 = "config-hooks.json";
4282
4373
  var GUARDED_SOURCES = /* @__PURE__ */ new Set(["user_settings", "project_settings", "local_settings"]);
4283
4374
  var countIn = (value) => {
@@ -4343,38 +4434,6 @@ var configChange = (input) => {
4343
4434
  };
4344
4435
  };
4345
4436
 
4346
- // src/guards/release/markers.ts
4347
- var LINE = /^\s*(?:finitemonkey|fm):\s+(allow|approve|trust)\s+(\S+)(?:\s+(.*))?$/i;
4348
- var NAME = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,80}$/;
4349
- var NOT_FROM_PERSON = [
4350
- /<task-notification\b/i,
4351
- /<agent-message\b/i,
4352
- /^\s*\[julius2claude\]/,
4353
- /\[Subagent hand-back\]/i
4354
- ];
4355
- var comesFromPerson = (prompt) => !NOT_FROM_PERSON.some((pattern) => pattern.test(prompt));
4356
- var toMarker = (verb, target, rest) => {
4357
- if (!NAME.test(target)) return void 0;
4358
- if (verb === "trust") return { type: "trust", task: target };
4359
- if (verb === "allow") {
4360
- return rest.length >= 3 ? { type: "allow", guard: target.toLowerCase(), reason: rest } : void 0;
4361
- }
4362
- const slash = target.indexOf("/");
4363
- const task = slash < 0 ? void 0 : target.slice(0, slash);
4364
- const check2 = slash < 0 ? target : target.slice(slash + 1);
4365
- if (check2 === "") return void 0;
4366
- return { type: "approve", check: check2, reason: rest, ...task === void 0 || task === "" ? {} : { task } };
4367
- };
4368
- var parseMarkers = (prompt) => {
4369
- if (!comesFromPerson(prompt)) return [];
4370
- return prompt.split(/\r?\n/).flatMap((line2) => {
4371
- const match = LINE.exec(line2);
4372
- if (match === null) return [];
4373
- const marker = toMarker((match[1] ?? "").toLowerCase(), match[2] ?? "", (match[3] ?? "").trim());
4374
- return marker === void 0 ? [] : [marker];
4375
- });
4376
- };
4377
-
4378
4437
  // src/task/implicit.ts
4379
4438
  var IMPLICIT_ID = "implicit";
4380
4439
  var LOCKFILES2 = [
@@ -5132,9 +5191,9 @@ var prStatus = (runtime, git, paths, ttlMs = PR_CACHE_TTL_MS) => {
5132
5191
  return { kind: "found", branch, pr: status.pr, localHead: headSha(git) };
5133
5192
  };
5134
5193
  var conflictFiles = (git, base) => {
5135
- for (const target of [`origin/${base}`, base]) {
5136
- if (git.out(["rev-parse", "--verify", "-q", `${target}^{commit}`]) === void 0) continue;
5137
- const result = git.run(["merge-tree", "--write-tree", "--name-only", "--no-messages", target, "HEAD"]);
5194
+ for (const target2 of [`origin/${base}`, base]) {
5195
+ if (git.out(["rev-parse", "--verify", "-q", `${target2}^{commit}`]) === void 0) continue;
5196
+ const result = git.run(["merge-tree", "--write-tree", "--name-only", "--no-messages", target2, "HEAD"]);
5138
5197
  if (result.found && result.exitCode === 1) return parseMergeTreeConflicts(result.stdout);
5139
5198
  if (result.found && result.exitCode === 0) return [];
5140
5199
  }
@@ -5953,6 +6012,7 @@ var en_default = {
5953
6012
  "gate.guard.bypass.hooks_path": "Finite Monkey: blocked: {detail} points git away from the project hooks. requires: core.hooksPath stays as configured.",
5954
6013
  "gate.guard.bypass.hook_env": "Finite Monkey: blocked: {detail} turns hooks or guards off. requires: hooks and guards stay on.",
5955
6014
  "gate.guard.bypass.fm_allow": "Finite Monkey: blocked: releases are made by the person, not by a tool call.",
6015
+ "gate.guard.bypass.fm_pause": "Finite Monkey: blocked: only the person pauses or resumes the gate, by typing `fm off` or `fm on` as the whole prompt.",
5956
6016
  "gate.guard.bypass.human": "Blocked {detail}: it would switch the safety net off. If you really want it, type on its own line: finitemonkey: allow bypass <reason>",
5957
6017
  "gate.guard.self.policy": "Finite Monkey: blocked: {file} is written only by the finitemonkey executable (evidence, policy and state). requires: change it through finitemonkey commands.",
5958
6018
  "gate.guard.self.allow_store": "Finite Monkey: blocked: {file} records what the person released; tools never write it.",
@@ -6129,7 +6189,24 @@ var en_default = {
6129
6189
  "gate.runtime.body_mismatch": '{route} did not contain "{body}"',
6130
6190
  "gate.runtime.request_failed": "the request to {route} failed: {detail}",
6131
6191
  "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}"
6192
+ "gate.runtime.probe_failed": "the app check did not report a result: {line}",
6193
+ "command.off.summary": "Pause the gate and warnings for the current Claude Code session (the person only)",
6194
+ "command.on.summary": "Resume the gate after fm off",
6195
+ "pause.off": "Gate paused for this session by the person. Secrets, production and destructive git guards stay on. Type `fm on` to resume.",
6196
+ "pause.on": "Gate resumed.",
6197
+ "pause.report": "The gate did not check this stop: the person paused it with fm off at {since}.",
6198
+ "pause.no_session": "No Claude Code session is recorded for this project yet; start one, then run this again or type `fm off` there.",
6199
+ "pause.error.person_only": "Only the person can pause or resume the gate, and this does not look like their terminal (no TTY, or running inside Claude Code)",
6200
+ "secrets.incident.runbook": " runbook: {link}",
6201
+ "secrets.command.live": "Check whether a key still works with its provider (off by default; sends only with --send, and prints the host first)",
6202
+ "secrets.action.live": "send a key to its provider",
6203
+ "secrets.live.dry": "Nothing was sent. With --send, the value of ${variable} would go to {host} ({url}) to check whether the {provider} key still works. Run it yourself, in your own terminal, with --send.",
6204
+ "secrets.live.contacting": "Contacting {host} ({url}) with the key; the value is passed to curl on stdin, never as an argument.",
6205
+ "secrets.live.live": "The key in ${variable} still works at {host} (HTTP {status}). Revoke it now: see the runbook in `finitemonkey secrets incident`.",
6206
+ "secrets.live.rejected": "{host} rejected the key in ${variable} (HTTP {status}): it no longer works.",
6207
+ "secrets.live.unknown": "{host} answered HTTP {status}; that does not say whether the key in ${variable} works. Check it in the provider's console.",
6208
+ "secrets.live.unreachable": "Could not reach {host} with curl, so the key in ${variable} was not checked.",
6209
+ "secrets.live.no_value": "${variable} is empty or not set in this terminal; export it first, nothing was sent"
6133
6210
  };
6134
6211
 
6135
6212
  // src/i18n/es.json
@@ -6506,6 +6583,7 @@ var es_default = {
6506
6583
  "gate.guard.bypass.hooks_path": "Finite Monkey: bloqueado: {detail} desvía git de los hooks del proyecto. exige: core.hooksPath queda como está configurado.",
6507
6584
  "gate.guard.bypass.hook_env": "Finite Monkey: bloqueado: {detail} apaga hooks o guardas. exige: hooks y guardas quedan encendidos.",
6508
6585
  "gate.guard.bypass.fm_allow": "Finite Monkey: bloqueado: las liberaciones las hace la persona, no una llamada de herramienta.",
6586
+ "gate.guard.bypass.fm_pause": "Finite Monkey: bloqueado: solo la persona pausa o reanuda la compuerta, escribiendo `fm off` o `fm on` como el prompt completo.",
6509
6587
  "gate.guard.bypass.human": "Bloqueé {detail}: apagaría la red de seguridad. Si de verdad lo quiere, escriba en una línea: finitemonkey: allow bypass <motivo>",
6510
6588
  "gate.guard.self.policy": "Finite Monkey: bloqueado: {file} solo lo escribe el ejecutable finitemonkey (evidencia, política y estado). exige: cambiarlo con los comandos de finitemonkey.",
6511
6589
  "gate.guard.self.allow_store": "Finite Monkey: bloqueado: {file} registra lo que la persona liberó; ninguna herramienta lo escribe.",
@@ -6682,7 +6760,24 @@ var es_default = {
6682
6760
  "gate.runtime.body_mismatch": '{route} no contenía "{body}"',
6683
6761
  "gate.runtime.request_failed": "la petición a {route} falló: {detail}",
6684
6762
  "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}"
6763
+ "gate.runtime.probe_failed": "la comprobación de la app no devolvió resultado: {line}",
6764
+ "command.off.summary": "Pausa la compuerta y los avisos en la sesión actual de Claude Code (solo la persona)",
6765
+ "command.on.summary": "Reanuda la compuerta después de fm off",
6766
+ "pause.off": "Compuerta pausada en esta sesión por la persona. Los bloqueos de secretos, producción y git destructivo siguen activos. Escribe `fm on` para reanudar.",
6767
+ "pause.on": "Compuerta reanudada.",
6768
+ "pause.report": "La compuerta no revisó esta parada: la persona la pausó con fm off a las {since}.",
6769
+ "pause.no_session": "Todavía no hay una sesión de Claude Code registrada en este proyecto; abre una y vuelve a ejecutarlo, o escribe `fm off` allí.",
6770
+ "pause.error.person_only": "Solo la persona puede pausar o reanudar la compuerta, y esto no parece su terminal (sin TTY, o dentro de Claude Code)",
6771
+ "secrets.incident.runbook": " guía: {link}",
6772
+ "secrets.command.live": "Comprueba si una clave aún funciona en su proveedor (apagado por defecto; solo envía con --send y muestra el host antes)",
6773
+ "secrets.action.live": "enviar una clave a su proveedor",
6774
+ "secrets.live.dry": "No se envió nada. Con --send, el valor de ${variable} iría a {host} ({url}) para comprobar si la clave de {provider} aún funciona. Ejecútalo tú, en tu terminal, con --send.",
6775
+ "secrets.live.contacting": "Contactando {host} ({url}) con la clave; el valor va a curl por la entrada estándar, nunca como argumento.",
6776
+ "secrets.live.live": "La clave en ${variable} aún funciona en {host} (HTTP {status}). Revócala ya: mira la guía en `finitemonkey secrets incident`.",
6777
+ "secrets.live.rejected": "{host} rechazó la clave en ${variable} (HTTP {status}): ya no funciona.",
6778
+ "secrets.live.unknown": "{host} respondió HTTP {status}; eso no dice si la clave en ${variable} funciona. Revísala en la consola del proveedor.",
6779
+ "secrets.live.unreachable": "No se pudo llegar a {host} con curl, así que la clave en ${variable} no se comprobó.",
6780
+ "secrets.live.no_value": "${variable} está vacía o no existe en esta terminal; expórtala antes, no se envió nada"
6686
6781
  };
6687
6782
 
6688
6783
  // src/i18n/pt-BR.json
@@ -7059,6 +7154,7 @@ var pt_BR_default = {
7059
7154
  "gate.guard.bypass.hooks_path": "Finite Monkey: barrado: {detail} desvia o git dos hooks do projeto. exige: core.hooksPath fica como está configurado.",
7060
7155
  "gate.guard.bypass.hook_env": "Finite Monkey: barrado: {detail} desliga hooks ou travas. exige: hooks e travas ficam ligados.",
7061
7156
  "gate.guard.bypass.fm_allow": "Finite Monkey: barrado: liberação é feita pela pessoa, não por chamada de ferramenta.",
7157
+ "gate.guard.bypass.fm_pause": "Finite Monkey: bloqueado: só a pessoa pausa ou retoma o portão, digitando `fm off` ou `fm on` como o prompt inteiro.",
7062
7158
  "gate.guard.bypass.human": "Barrei {detail}: desligaria a rede de proteção. Se quiser mesmo, digite numa linha: finitemonkey: allow bypass <motivo>",
7063
7159
  "gate.guard.self.policy": "Finite Monkey: barrado: {file} só é escrito pelo executável finitemonkey (evidência, política e estado). exige: mudar pelos comandos do finitemonkey.",
7064
7160
  "gate.guard.self.allow_store": "Finite Monkey: barrado: {file} registra o que a pessoa liberou; ferramenta nunca escreve nele.",
@@ -7235,7 +7331,24 @@ var pt_BR_default = {
7235
7331
  "gate.runtime.body_mismatch": '{route} não continha "{body}"',
7236
7332
  "gate.runtime.request_failed": "a requisição para {route} falhou: {detail}",
7237
7333
  "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}"
7334
+ "gate.runtime.probe_failed": "a checagem do app não devolveu resultado: {line}",
7335
+ "command.off.summary": "Pausa o portão e os avisos na sessão atual do Claude Code (só a pessoa)",
7336
+ "command.on.summary": "Retoma o portão depois de fm off",
7337
+ "pause.off": "Portão pausado nesta sessão pela pessoa. As travas de segredos, produção e git destrutivo continuam ligadas. Digite `fm on` para retomar.",
7338
+ "pause.on": "Portão retomado.",
7339
+ "pause.report": "O portão não conferiu esta parada: a pessoa pausou com fm off em {since}.",
7340
+ "pause.no_session": "Ainda não há sessão do Claude Code registrada neste projeto; abra uma e rode de novo, ou digite `fm off` nela.",
7341
+ "pause.error.person_only": "Só a pessoa pode pausar ou retomar o portão, e isto não parece o terminal dela (sem TTY, ou rodando dentro do Claude Code)",
7342
+ "secrets.incident.runbook": " passo a passo: {link}",
7343
+ "secrets.command.live": "Verifica se uma chave ainda funciona no provedor (desligado por padrão; só envia com --send e mostra o host antes)",
7344
+ "secrets.action.live": "enviar uma chave ao provedor",
7345
+ "secrets.live.dry": "Nada foi enviado. Com --send, o valor de ${variable} iria para {host} ({url}) para ver se a chave {provider} ainda funciona. Rode você mesmo, no seu terminal, com --send.",
7346
+ "secrets.live.contacting": "Contatando {host} ({url}) com a chave; o valor vai para o curl pela entrada padrão, nunca como argumento.",
7347
+ "secrets.live.live": "A chave em ${variable} ainda funciona em {host} (HTTP {status}). Revogue agora: veja o passo a passo em `finitemonkey secrets incident`.",
7348
+ "secrets.live.rejected": "{host} recusou a chave em ${variable} (HTTP {status}): ela não funciona mais.",
7349
+ "secrets.live.unknown": "{host} respondeu HTTP {status}; isso não diz se a chave em ${variable} funciona. Confira no painel do provedor.",
7350
+ "secrets.live.unreachable": "Não deu para alcançar {host} com o curl, então a chave em ${variable} não foi verificada.",
7351
+ "secrets.live.no_value": "${variable} está vazia ou não existe neste terminal; exporte antes, nada foi enviado"
7239
7352
  };
7240
7353
 
7241
7354
  // src/i18n/review.json
@@ -7613,6 +7726,7 @@ var review_default = {
7613
7726
  "gate.guard.bypass.hooks_path": "machine",
7614
7727
  "gate.guard.bypass.hook_env": "machine",
7615
7728
  "gate.guard.bypass.fm_allow": "machine",
7729
+ "gate.guard.bypass.fm_pause": "machine",
7616
7730
  "gate.guard.bypass.human": "machine",
7617
7731
  "gate.guard.self.policy": "machine",
7618
7732
  "gate.guard.self.allow_store": "machine",
@@ -7789,7 +7903,24 @@ var review_default = {
7789
7903
  "gate.runtime.body_mismatch": "machine",
7790
7904
  "gate.runtime.request_failed": "machine",
7791
7905
  "gate.runtime.orphan": "machine",
7792
- "gate.runtime.probe_failed": "machine"
7906
+ "gate.runtime.probe_failed": "machine",
7907
+ "command.off.summary": "machine",
7908
+ "command.on.summary": "machine",
7909
+ "pause.off": "machine",
7910
+ "pause.on": "machine",
7911
+ "pause.report": "machine",
7912
+ "pause.no_session": "machine",
7913
+ "pause.error.person_only": "machine",
7914
+ "secrets.incident.runbook": "machine",
7915
+ "secrets.command.live": "machine",
7916
+ "secrets.action.live": "machine",
7917
+ "secrets.live.dry": "machine",
7918
+ "secrets.live.contacting": "machine",
7919
+ "secrets.live.live": "machine",
7920
+ "secrets.live.rejected": "machine",
7921
+ "secrets.live.unknown": "machine",
7922
+ "secrets.live.unreachable": "machine",
7923
+ "secrets.live.no_value": "machine"
7793
7924
  },
7794
7925
  es: {
7795
7926
  "command.init.summary": "machine",
@@ -8164,6 +8295,7 @@ var review_default = {
8164
8295
  "gate.guard.bypass.hooks_path": "machine",
8165
8296
  "gate.guard.bypass.hook_env": "machine",
8166
8297
  "gate.guard.bypass.fm_allow": "machine",
8298
+ "gate.guard.bypass.fm_pause": "machine",
8167
8299
  "gate.guard.bypass.human": "machine",
8168
8300
  "gate.guard.self.policy": "machine",
8169
8301
  "gate.guard.self.allow_store": "machine",
@@ -8340,7 +8472,24 @@ var review_default = {
8340
8472
  "gate.runtime.body_mismatch": "machine",
8341
8473
  "gate.runtime.request_failed": "machine",
8342
8474
  "gate.runtime.orphan": "machine",
8343
- "gate.runtime.probe_failed": "machine"
8475
+ "gate.runtime.probe_failed": "machine",
8476
+ "command.off.summary": "machine",
8477
+ "command.on.summary": "machine",
8478
+ "pause.off": "machine",
8479
+ "pause.on": "machine",
8480
+ "pause.report": "machine",
8481
+ "pause.no_session": "machine",
8482
+ "pause.error.person_only": "machine",
8483
+ "secrets.incident.runbook": "machine",
8484
+ "secrets.command.live": "machine",
8485
+ "secrets.action.live": "machine",
8486
+ "secrets.live.dry": "machine",
8487
+ "secrets.live.contacting": "machine",
8488
+ "secrets.live.live": "machine",
8489
+ "secrets.live.rejected": "machine",
8490
+ "secrets.live.unknown": "machine",
8491
+ "secrets.live.unreachable": "machine",
8492
+ "secrets.live.no_value": "machine"
8344
8493
  }
8345
8494
  };
8346
8495
 
@@ -8637,8 +8786,8 @@ var callersRate = (runtime, paths) => {
8637
8786
  const parsed = line2.trim() === "" ? void 0 : parseJsonObject(line2);
8638
8787
  if (parsed === void 0 || !parsed.ok || !isRecord(parsed.value)) continue;
8639
8788
  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);
8789
+ const target2 = event === FLAGGED_EVENT ? flagged : event === RELEASED_EVENT ? released2 : void 0;
8790
+ if (target2 !== void 0) for (const pair of pairs(parsed.value)) target2.add(pair);
8642
8791
  }
8643
8792
  const total = (/* @__PURE__ */ new Set([...flagged, ...released2])).size;
8644
8793
  const rate = total === 0 ? 0 : released2.size / total;
@@ -8660,13 +8809,13 @@ var JS_EXTENSION = /\.(?:[cm]?[jt]sx?)$/;
8660
8809
  var stripModule = (path) => path.replace(JS_EXTENSION, "").replace(/\/index$/, "");
8661
8810
  var jsSpecifiers = (text6) => [...text6.matchAll(JS_SPECIFIER)].map((match) => match[1] ?? "");
8662
8811
  var jsImports = (callerPath, callerText, origin) => {
8663
- const target = stripModule(origin);
8812
+ const target2 = stripModule(origin);
8664
8813
  return jsSpecifiers(callerText).some((specifier) => {
8665
8814
  if (specifier.startsWith(".")) {
8666
- return stripModule(posix2.normalize(posix2.join(posix2.dirname(callerPath), specifier))) === target;
8815
+ return stripModule(posix2.normalize(posix2.join(posix2.dirname(callerPath), specifier))) === target2;
8667
8816
  }
8668
8817
  const bare = stripModule(specifier.replace(/^[@~#]\//, ""));
8669
- return target === bare || target.endsWith(`/${bare}`);
8818
+ return target2 === bare || target2.endsWith(`/${bare}`);
8670
8819
  });
8671
8820
  };
8672
8821
  var pyImports = (callerText, origin) => {
@@ -10184,13 +10333,13 @@ var MAX_FILE_BYTES = 2 * 1024 * 1024;
10184
10333
  var SKIPPED = /(?:^|\/)(?:\.git|node_modules|\.finitemonkey)\//;
10185
10334
  var DIFF_FLAGS = ["-U0", "--no-color", "--no-ext-diff", "--no-renames"];
10186
10335
  var binary = (text6) => text6.slice(0, 8e3).includes("\0");
10187
- var listed2 = (runtime, root, target) => {
10188
- const git = runtime.git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", target], root);
10336
+ var listed2 = (runtime, root, target2) => {
10337
+ const git = runtime.git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", target2], root);
10189
10338
  if (git.ok)
10190
10339
  return accept(
10191
10340
  git.stdout.split("\0").filter((name) => name !== "").map(toPosixPath)
10192
10341
  );
10193
- const base = toAbsolutePath(target, root);
10342
+ const base = toAbsolutePath(target2, root);
10194
10343
  const walked = runtime.walkFiles(base);
10195
10344
  if (!walked.ok) return walked;
10196
10345
  return accept(walked.value.map((name) => toPosixPath(relative4(root, join20(base, toPosixPath(name))))));
@@ -10200,8 +10349,8 @@ var scanFile = (runtime, root, file) => {
10200
10349
  if (!text6.ok || text6.value.length > MAX_FILE_BYTES || binary(text6.value)) return [];
10201
10350
  return scanText(text6.value, { path: file }).map((finding3) => locate(finding3, file, finding3.line));
10202
10351
  };
10203
- var scanPath = (runtime, root, target) => {
10204
- const files = listed2(runtime, root, target);
10352
+ var scanPath = (runtime, root, target2) => {
10353
+ const files = listed2(runtime, root, target2);
10205
10354
  if (!files.ok) return files;
10206
10355
  return accept(files.value.filter((file) => !SKIPPED.test(file)).flatMap((file) => scanFile(runtime, root, file)));
10207
10356
  };
@@ -11221,7 +11370,7 @@ var integrationRefs = (runtime, root, defaultBranch) => [`refs/heads/${defaultBr
11221
11370
  var contained = (runtime, root, branch, targets2) => {
11222
11371
  const tip = line(runtime, root, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]);
11223
11372
  if (tip === void 0) return true;
11224
- return targets2.filter((target) => target !== `refs/heads/${branch}`).some((target) => runtime.git(["merge-base", "--is-ancestor", tip, target], root).ok);
11373
+ return targets2.filter((target2) => target2 !== `refs/heads/${branch}`).some((target2) => runtime.git(["merge-base", "--is-ancestor", tip, target2], root).ok);
11225
11374
  };
11226
11375
  var shared = (runtime, root, upstream, base) => {
11227
11376
  if (upstream === void 0 || !refExists(runtime, root, upstream.remoteRef)) return false;
@@ -11566,9 +11715,9 @@ var subagentStart = (input) => {
11566
11715
  // src/gate/hooks/index.ts
11567
11716
  var PORTAO_HANDLERS = {
11568
11717
  PreToolUse: [selfGuard, bypassGuard, testsGuard],
11569
- UserPromptSubmit: [promptRelease],
11570
- Stop: [stopGate],
11571
- SubagentStop: [subagentStopGate],
11718
+ UserPromptSubmit: [pausePrompt, promptRelease],
11719
+ Stop: [unlessPaused(stopGate)],
11720
+ SubagentStop: [unlessPaused(subagentStopGate)],
11572
11721
  SubagentStart: [subagentStart],
11573
11722
  PreCompact: [preCompact],
11574
11723
  SessionStart: [sessionStart],
@@ -11970,7 +12119,7 @@ var loadProdTargets = (runtime, root, config) => {
11970
12119
  var isProdContext = (name, declared) => declared.includes(name) || /(?:^|[-_.:/])(?:prod|production)(?:$|[-_.:/])/i.test(name);
11971
12120
 
11972
12121
  // src/prod/classify.ts
11973
- var hit2 = (effect, target, tool) => ({ effect, target, tool });
12122
+ var hit2 = (effect, target2, tool) => ({ effect, target: target2, tool });
11974
12123
  var sshHit = (command, isProd) => {
11975
12124
  const call = parseSsh(command);
11976
12125
  if (call === void 0 || !isProd(call.destination)) return void 0;
@@ -12006,9 +12155,9 @@ var dockerHit = (command, targets2, isProd) => {
12006
12155
  return hit2("effect", switching, "docker context");
12007
12156
  const prodHost = remote !== void 0 && isProd(remote) ? hostKey(remote) : void 0;
12008
12157
  const prodContext = context !== void 0 && isProdContext(context, targets2.dockerContexts) ? context : void 0;
12009
- const target = prodHost ?? prodContext;
12010
- if (target === void 0) return void 0;
12011
- return hit2(dockerReadOnly(command.args) ? "read" : "effect", target, "docker");
12158
+ const target2 = prodHost ?? prodContext;
12159
+ if (target2 === void 0) return void 0;
12160
+ return hit2(dockerReadOnly(command.args) ? "read" : "effect", target2, "docker");
12012
12161
  };
12013
12162
  var kubeHit = (command, targets2) => {
12014
12163
  const context = valueAfter(command.args, "--context", "--kube-context");
@@ -12693,8 +12842,8 @@ var pushDestinations = (parsed, facts) => {
12693
12842
  return [facts.upstream(current2)?.branch ?? current2];
12694
12843
  }
12695
12844
  return refspecs.flatMap((refspec) => {
12696
- const target = refspecTarget(refspec, current2);
12697
- return target === void 0 ? [] : [target];
12845
+ const target2 = refspecTarget(refspec, current2);
12846
+ return target2 === void 0 ? [] : [target2];
12698
12847
  });
12699
12848
  };
12700
12849
  var leaseVerdict = (parsed, facts) => {
@@ -12874,8 +13023,8 @@ var guardGitCommand = (context, command, depth = 0) => {
12874
13023
  if (repo === void 0) return [];
12875
13024
  const facts = withChain(context.facts?.(repo) ?? repoFacts(context.runtime, repo), context.chain);
12876
13025
  const expanded = expandAlias(context.runtime, repo, invocation);
12877
- const target = switchTarget(expanded, facts);
12878
- if (target !== void 0 && context.chain !== void 0) context.chain.branch = target;
13026
+ const target2 = switchTarget(expanded, facts);
13027
+ if (target2 !== void 0 && context.chain !== void 0) context.chain.branch = target2;
12879
13028
  const nested = depth < MAX_ALIAS_DEPTH ? shellAliasCommands(expanded, context.runtime, repo).flatMap(
12880
13029
  (inner) => guardGitCommand(context, inner, depth + 1)
12881
13030
  ) : [];
@@ -12912,11 +13061,11 @@ var removalTargets = (command) => {
12912
13061
  if (command.program === "remove-item") return powershellTargets(command);
12913
13062
  return void 0;
12914
13063
  };
12915
- var globBase = (target) => {
12916
- const at = target.search(GLOB2);
12917
- if (at < 0) return target;
12918
- const slash = target.lastIndexOf("/", at);
12919
- return slash < 0 ? "." : target.slice(0, slash) || "/";
13064
+ var globBase = (target2) => {
13065
+ const at = target2.search(GLOB2);
13066
+ if (at < 0) return target2;
13067
+ const slash = target2.lastIndexOf("/", at);
13068
+ return slash < 0 ? "." : target2.slice(0, slash) || "/";
12920
13069
  };
12921
13070
  var containsGitDir = (runtime, path) => runtime.stat(toAbsolutePath(resolveFrom(path, ".git"))) !== void 0;
12922
13071
  var targetVerdict = (runtime, context, command, raw) => {
@@ -12936,8 +13085,8 @@ var targetVerdict = (runtime, context, command, raw) => {
12936
13085
  var rmVerdict = (runtime, context, command) => {
12937
13086
  const targets2 = removalTargets(command);
12938
13087
  if (targets2 === void 0) return PASS;
12939
- for (const target of targets2) {
12940
- const verdict = targetVerdict(runtime, context, command, target);
13088
+ for (const target2 of targets2) {
13089
+ const verdict = targetVerdict(runtime, context, command, target2);
12941
13090
  if (verdict.kind === "block") return verdict;
12942
13091
  }
12943
13092
  return PASS;
@@ -13000,7 +13149,7 @@ var appendJournal = (runtime, root, entry2) => {
13000
13149
 
13001
13150
  // src/git/snapshot.ts
13002
13151
  var BACKUP_PREFIX = "refs/finitemonkey/backup/";
13003
- var DEFAULT_SNAPSHOT_TIMEOUT_MS = 2500;
13152
+ var DEFAULT_SNAPSHOT_TIMEOUT_MS = 5e3;
13004
13153
  var stampOf = (iso) => iso.replace(/[:.]/g, "-");
13005
13154
  var slug = (op) => op.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "manual";
13006
13155
  var optionalLine = (runtime, repo, args2) => {
@@ -13289,6 +13438,7 @@ var recordShadow = (input, paths, set, policy, reason) => {
13289
13438
  var governed = (set, handler) => async (input) => {
13290
13439
  const paths = projectPaths(input);
13291
13440
  if (paths === void 0) return handler(input);
13441
+ if (pausedFor(input.runtime, paths, input.payload.sessionId) !== void 0) return { kind: "allow" };
13292
13442
  const policy = loadPolicy(input.runtime, paths.root);
13293
13443
  const mode = modeOf2(set, policy, readShadow(input.runtime, paths));
13294
13444
  if (mode === "off") return { kind: "allow" };
@@ -13474,12 +13624,12 @@ var sourceOf = (value, isFile, segment, heredocs) => {
13474
13624
  const piped = segment.args.map((arg) => heredocIn(arg, heredocs)).find((text6) => text6 !== void 0);
13475
13625
  return { kind: "inline", text: piped ?? "" };
13476
13626
  };
13477
- var collect3 = (target, segment, args2, spec2, heredocs) => {
13627
+ var collect3 = (target2, segment, args2, spec2, heredocs) => {
13478
13628
  const values = flagValues(args2, spec2);
13479
13629
  if (values.length === 0) return void 0;
13480
13630
  const title = values.find((item) => spec2.title.includes(item.flag));
13481
13631
  const bodies = values.filter((item) => !spec2.title.includes(item.flag)).map((item) => sourceOf(item.value, spec2.file.includes(item.flag), segment, heredocs));
13482
- return { target, title: title === void 0 ? void 0 : sourceOf(title.value, false, segment, heredocs), bodies };
13632
+ return { target: target2, title: title === void 0 ? void 0 : sourceOf(title.value, false, segment, heredocs), bodies };
13483
13633
  };
13484
13634
  var gitSubcommand = (args2) => {
13485
13635
  let index = 0;
@@ -13698,7 +13848,7 @@ var READERS = /* @__PURE__ */ new Set([
13698
13848
  "dotenv"
13699
13849
  ]);
13700
13850
  var SECRET_NAME = /(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PASS|PWD|CREDENTIAL)/i;
13701
- var PERSON_ONLY = /\b(?:finitemonkey|fm)(?:\.mjs)?\s+secrets\s+(?:accept\b|incident\s+(?:rotated|close)\b)/;
13851
+ var PERSON_ONLY = /\b(?:finitemonkey|fm)(?:\.mjs)?\s+secrets\s+(?:accept\b|incident\s+(?:rotated|close)\b|live\b[^;&|\n]*\s--send\b)/;
13702
13852
  var LISTS_ENV = /* @__PURE__ */ new Set(["get-childitem", "gci", "dir", "ls"]);
13703
13853
  var onlyFlags = (args2) => args2.every((arg) => arg.startsWith("-"));
13704
13854
  var readTarget = (segment) => {
@@ -13724,8 +13874,8 @@ var bashFindings = (command, segments) => {
13724
13874
  const person = PERSON_ONLY.exec(command);
13725
13875
  if (person !== null) return [{ kind: "person-only", command: person[0] }];
13726
13876
  return segments.flatMap((segment) => {
13727
- const target = readTarget(segment);
13728
- if (target !== void 0) return [{ kind: "read", target }];
13877
+ const target2 = readTarget(segment);
13878
+ if (target2 !== void 0) return [{ kind: "read", target: target2 }];
13729
13879
  if (dumpsEnv(segment)) return [{ kind: "env-dump", command: [segment.program, ...segment.args].join(" ") }];
13730
13880
  return [];
13731
13881
  });
@@ -13816,9 +13966,9 @@ var bashDecision = (input) => {
13816
13966
  };
13817
13967
  var readDecision = (input) => {
13818
13968
  const inputs = toolInput2(input.payload);
13819
- const target = optionalString(inputs, "file_path") ?? optionalString(inputs, "path") ?? "";
13820
- if (target === "" || !isSensitivePath(target)) return ALLOW5;
13821
- return { kind: "block", reason: input.t("secrets.block.read", { target: relativeFile(input.cwd, target) }) };
13969
+ const target2 = optionalString(inputs, "file_path") ?? optionalString(inputs, "path") ?? "";
13970
+ if (target2 === "" || !isSensitivePath(target2)) return ALLOW5;
13971
+ return { kind: "block", reason: input.t("secrets.block.read", { target: relativeFile(input.cwd, target2) }) };
13822
13972
  };
13823
13973
  var WRITERS2 = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
13824
13974
  var SHELLS4 = /* @__PURE__ */ new Set(["Bash", "PowerShell"]);
@@ -16081,6 +16231,35 @@ var initCommand = {
16081
16231
  }
16082
16232
  };
16083
16233
 
16234
+ // src/commands/pause.ts
16235
+ var toggle = (ctx, off) => {
16236
+ const command = off ? "finitemonkey off" : "finitemonkey on";
16237
+ if (!isPerson(ctx)) return failureFrom(ctx.t, needsPersonProblem("pause.error.person_only", command));
16238
+ const paths = fmPaths(findRepoRoot(ctx.runtime, ctx.cwd) ?? ctx.cwd);
16239
+ if (!off) {
16240
+ const ended = clearPause(ctx.runtime, paths, "terminal");
16241
+ return success({ paused: false, sessionId: ended?.sessionId ?? null }, [ctx.t("pause.on")]);
16242
+ }
16243
+ const session = readSession(ctx.runtime, paths);
16244
+ if (session === void 0) return success({ paused: false, sessionId: null }, [ctx.t("pause.no_session")]);
16245
+ setPause(ctx.runtime, paths, session.sessionId, "terminal");
16246
+ return success({ paused: true, sessionId: session.sessionId }, [ctx.t("pause.off")]);
16247
+ };
16248
+ var offCommand = {
16249
+ path: ["off"],
16250
+ summaryKey: "command.off.summary",
16251
+ usage: "finitemonkey off",
16252
+ status: "ready",
16253
+ run: (ctx) => Promise.resolve(toggle(ctx, true))
16254
+ };
16255
+ var onCommand = {
16256
+ path: ["on"],
16257
+ summaryKey: "command.on.summary",
16258
+ usage: "finitemonkey on",
16259
+ status: "ready",
16260
+ run: (ctx) => Promise.resolve(toggle(ctx, false))
16261
+ };
16262
+
16084
16263
  // src/commands/policy.ts
16085
16264
  var FIX = `finitemonkey policy false-positive <${OPTIONAL_GUARDS.join("|")}>`;
16086
16265
  var recordFalsePositive = (ctx, guard) => {
@@ -16169,15 +16348,15 @@ var problem2 = (code, messageKey, params2) => ({
16169
16348
  exitCode: ExitCode.Usage
16170
16349
  });
16171
16350
  var underRoot = (root, path) => toPosixPath(relative8(root, path));
16172
- var expand2 = (ctx, root, target) => {
16173
- const path = toAbsolutePath(target, ctx.cwd);
16351
+ var expand2 = (ctx, root, target2) => {
16352
+ const path = toAbsolutePath(target2, ctx.cwd);
16174
16353
  const stat = ctx.runtime.stat(path);
16175
16354
  if (stat === void 0)
16176
- return reject(problem2("prose_unreadable", "prose.error.unreadable", { file: target, detail: "not found" }));
16355
+ return reject(problem2("prose_unreadable", "prose.error.unreadable", { file: target2, detail: "not found" }));
16177
16356
  if (stat.kind === "file") return accept([underRoot(root, path)]);
16178
16357
  const walked = ctx.runtime.walkFiles(path);
16179
16358
  if (!walked.ok)
16180
- return reject(problem2("prose_unreadable", "prose.error.unreadable", { file: target, detail: walked.error }));
16359
+ return reject(problem2("prose_unreadable", "prose.error.unreadable", { file: target2, detail: walked.error }));
16181
16360
  return accept(
16182
16361
  walked.value.map((name) => underRoot(root, toAbsolutePath(toPosixPath(name), path))).filter((name) => TEXT_FILE.test(name) && !/(?:^|\/)(?:node_modules|\.git)\//.test(name)).sort()
16183
16362
  );
@@ -16361,6 +16540,33 @@ var secretsAcceptCommand = {
16361
16540
  }
16362
16541
  };
16363
16542
 
16543
+ // src/secrets/runbooks.ts
16544
+ import { dirname as dirname9, join as join39 } from "node:path";
16545
+ var PROVIDER_OF_RULE = {
16546
+ "aws-access-token": "aws",
16547
+ "aws-secret-access-key": "aws",
16548
+ "github-pat": "github",
16549
+ "github-oauth": "github",
16550
+ "github-app-token": "github",
16551
+ "github-refresh-token": "github",
16552
+ "github-fine-grained-pat": "github",
16553
+ "stripe-access-token": "stripe",
16554
+ "anthropic-api-key": "anthropic",
16555
+ "openai-api-key": "openai",
16556
+ "gcp-api-key": "google",
16557
+ "asaas-api-key": "asaas",
16558
+ "credential-in-url": "database",
16559
+ "password-assignment": "database",
16560
+ "secret-in-process-args": "database"
16561
+ };
16562
+ var RUNBOOKS_URL = "https://github.com/gabrielvieiraf/finitemonkey/blob/main/runbooks";
16563
+ var runbookProvider = (rule) => PROVIDER_OF_RULE[rule] ?? "generic";
16564
+ var runbookLink = (runtime, rule) => {
16565
+ const file = `${runbookProvider(rule)}.md`;
16566
+ const local = toAbsolutePath(join39(dirname9(dirname9(runtime.selfPath)), "runbooks", file));
16567
+ return runtime.stat(local)?.kind === "file" ? local : `${RUNBOOKS_URL}/${file}`;
16568
+ };
16569
+
16364
16570
  // src/commands/secrets-incident.ts
16365
16571
  var INCIDENT_ACTIONS = ["list", "rotated", "close", "purge"];
16366
16572
  var USAGE = "finitemonkey secrets incident [list | rotated <id> | close <id> | purge <id>]";
@@ -16386,9 +16592,19 @@ var itemLine = (ctx, incident) => ctx.t("secrets.incident.item", {
16386
16592
  where: incident.where,
16387
16593
  created: incident.createdAt.slice(0, 10)
16388
16594
  });
16595
+ var itemLines2 = (ctx, incident) => [
16596
+ itemLine(ctx, incident),
16597
+ ctx.t("secrets.incident.runbook", { link: incident.runbook })
16598
+ ];
16389
16599
  var list2 = (ctx, root) => {
16390
- const incidents = listIncidents(ctx.runtime, root);
16391
- const human2 = incidents.length === 0 ? [ctx.t("secrets.incident.none")] : [ctx.t("secrets.incident.title", { count: incidents.length }), ...incidents.map((item) => itemLine(ctx, item))];
16600
+ const incidents = listIncidents(ctx.runtime, root).map((incident) => ({
16601
+ ...incident,
16602
+ runbook: runbookLink(ctx.runtime, incident.rule)
16603
+ }));
16604
+ const human2 = incidents.length === 0 ? [ctx.t("secrets.incident.none")] : [
16605
+ ctx.t("secrets.incident.title", { count: incidents.length }),
16606
+ ...incidents.flatMap((item) => itemLines2(ctx, item))
16607
+ ];
16392
16608
  return success({ schema: "finitemonkey/secrets-incidents@1", incidents }, human2);
16393
16609
  };
16394
16610
  var notRotated = (ctx, incident) => fail2(ctx, {
@@ -16455,6 +16671,133 @@ var secretsIncidentCommand = {
16455
16671
  }
16456
16672
  };
16457
16673
 
16674
+ // src/secrets/live.ts
16675
+ var bearer = (key) => [`Authorization: Bearer ${key}`];
16676
+ var LIVE_PROBES = {
16677
+ github: {
16678
+ host: "api.github.com",
16679
+ path: "/user",
16680
+ headers: (key) => [...bearer(key), "User-Agent: finitemonkey"]
16681
+ },
16682
+ stripe: { host: "api.stripe.com", path: "/v1/balance", headers: bearer },
16683
+ openai: { host: "api.openai.com", path: "/v1/models", headers: bearer },
16684
+ anthropic: {
16685
+ host: "api.anthropic.com",
16686
+ path: "/v1/models",
16687
+ headers: (key) => [`x-api-key: ${key}`, "anthropic-version: 2023-06-01"]
16688
+ },
16689
+ digitalocean: { host: "api.digitalocean.com", path: "/v2/account", headers: bearer },
16690
+ cloudflare: { host: "api.cloudflare.com", path: "/client/v4/user/tokens/verify", headers: bearer },
16691
+ asaas: {
16692
+ host: "api.asaas.com",
16693
+ path: "/v3/finance/balance",
16694
+ headers: (key) => [`access_token: ${key}`, "User-Agent: finitemonkey"]
16695
+ }
16696
+ };
16697
+ var LIVE_PROVIDERS = Object.keys(LIVE_PROBES).filter((name) => name in LIVE_PROBES);
16698
+ var LIVE_HOSTS = LIVE_PROVIDERS.map((name) => LIVE_PROBES[name].host);
16699
+ var liveProvider = (name) => LIVE_PROVIDERS.find((provider) => provider === name);
16700
+ var LIVE_TIMEOUT_MS = 15e3;
16701
+ var liveRequest = (provider, key, cwd) => {
16702
+ const probe = LIVE_PROBES[provider];
16703
+ return {
16704
+ command: "curl",
16705
+ args: [
16706
+ "--silent",
16707
+ "--show-error",
16708
+ "--proto",
16709
+ "=https",
16710
+ "--max-time",
16711
+ "10",
16712
+ "--header",
16713
+ "@-",
16714
+ "--write-out",
16715
+ "\n%{http_code}",
16716
+ `https://${probe.host}${probe.path}`
16717
+ ],
16718
+ cwd,
16719
+ timeoutMs: LIVE_TIMEOUT_MS,
16720
+ input: `${probe.headers(key).join("\n")}
16721
+ `
16722
+ };
16723
+ };
16724
+ var liveOutcome = (result) => {
16725
+ if (!result.found || result.exitCode !== 0) return { verdict: "unreachable" };
16726
+ const status = Number.parseInt(result.stdout.trimEnd().split("\n").at(-1) ?? "", 10);
16727
+ if (!Number.isInteger(status) || status === 0) return { verdict: "unreachable" };
16728
+ if (status >= 200 && status < 300) return { verdict: "live", status };
16729
+ if (status === 401 || status === 403) return { verdict: "rejected", status };
16730
+ return { verdict: "unknown", status };
16731
+ };
16732
+
16733
+ // src/commands/secrets-live.ts
16734
+ var SECRETS_LIVE_SCHEMA = "finitemonkey/secrets-live@1";
16735
+ var USAGE2 = `finitemonkey secrets live <${LIVE_PROVIDERS.join("|")}> --env <VARIABLE> [--send]`;
16736
+ var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
16737
+ var target = (provider) => {
16738
+ const probe = LIVE_PROBES[provider];
16739
+ return { host: probe.host, url: `https://${probe.host}${probe.path}` };
16740
+ };
16741
+ var dryRun = (ctx, provider, variable) => {
16742
+ const { host, url: url2 } = target(provider);
16743
+ const data = { schema: SECRETS_LIVE_SCHEMA, provider, host, url: url2, variable, sent: false };
16744
+ return success(data, [ctx.t("secrets.live.dry", { host, url: url2, variable, provider })]);
16745
+ };
16746
+ var send = (ctx, provider, variable, key) => {
16747
+ const { host, url: url2 } = target(provider);
16748
+ ctx.io.stderr.write(`${ctx.t("secrets.live.contacting", { host, url: url2 })}
16749
+ `);
16750
+ const outcome2 = liveOutcome(ctx.runtime.exec(liveRequest(provider, key, ctx.cwd)));
16751
+ const data = { schema: SECRETS_LIVE_SCHEMA, provider, host, url: url2, variable, sent: true, ...outcome2 };
16752
+ const line2 = ctx.t(`secrets.live.${outcome2.verdict}`, { host, status: outcome2.status ?? 0, variable });
16753
+ if (outcome2.verdict !== "unreachable") return success(data, [line2]);
16754
+ return failureFrom(ctx.t, {
16755
+ code: "secrets_live_unreachable",
16756
+ messageKey: "secrets.live.unreachable",
16757
+ params: { host, status: 0, variable },
16758
+ fix: "install curl or check the network, then run the command again",
16759
+ exitCode: ExitCode.CheckFailed,
16760
+ data
16761
+ });
16762
+ };
16763
+ var secretsLiveCommand = {
16764
+ path: ["secrets", "live"],
16765
+ summaryKey: "secrets.command.live",
16766
+ usage: USAGE2,
16767
+ status: "ready",
16768
+ maxPositionals: 1,
16769
+ options: { env: { type: "string" }, send: { type: "boolean" } },
16770
+ run: (ctx, args2) => {
16771
+ const provider = liveProvider(args2.positionals[0]);
16772
+ const variable = stringOption(args2.values, "env") ?? "";
16773
+ if (provider === void 0 || !ENV_NAME.test(variable))
16774
+ return Promise.resolve(
16775
+ failureFrom(
16776
+ ctx.t,
16777
+ usageProblem("invalid_arguments", "error.invalid_arguments", USAGE2, {
16778
+ detail: provider === void 0 ? args2.positionals[0] ?? "<provider>" : "--env <VARIABLE>"
16779
+ })
16780
+ )
16781
+ );
16782
+ if (args2.values["send"] !== true) return Promise.resolve(dryRun(ctx, provider, variable));
16783
+ if (!isPerson(ctx))
16784
+ return Promise.resolve(
16785
+ failureFrom(
16786
+ ctx.t,
16787
+ needsPersonProblem("secrets.error.person_only", `${USAGE2.replace(" [--send]", "")} --send`, {
16788
+ action: ctx.t("secrets.action.live")
16789
+ })
16790
+ )
16791
+ );
16792
+ const key = ctx.env[variable] ?? "";
16793
+ if (key === "")
16794
+ return Promise.resolve(
16795
+ failureFrom(ctx.t, usageProblem("invalid_arguments", "secrets.live.no_value", USAGE2, { variable }))
16796
+ );
16797
+ return Promise.resolve(send(ctx, provider, variable, key));
16798
+ }
16799
+ };
16800
+
16458
16801
  // src/commands/secrets-scan.ts
16459
16802
  var SECRETS_SCAN_SCHEMA = "finitemonkey/secrets-scan@1";
16460
16803
  var view = (finding3, status) => ({
@@ -16475,15 +16818,15 @@ var modeOf3 = (values) => {
16475
16818
  if (chosen[0] === true) return "staged";
16476
16819
  return chosen[1] === true ? "history" : "path";
16477
16820
  };
16478
- var normalizedTarget = (target) => target.replace(/\\/g, "/").replace(/^\.\/?/, "").replace(/\/$/, "");
16479
- var scopeOf = (target) => {
16480
- const prefix = normalizedTarget(target);
16821
+ var normalizedTarget = (target2) => target2.replace(/\\/g, "/").replace(/^\.\/?/, "").replace(/\/$/, "");
16822
+ var scopeOf = (target2) => {
16823
+ const prefix = normalizedTarget(target2);
16481
16824
  return (file) => prefix === "" || file === prefix || file.startsWith(`${prefix}/`);
16482
16825
  };
16483
- var runScan = async (ctx, root, mode, target) => {
16826
+ var runScan = async (ctx, root, mode, target2) => {
16484
16827
  if (mode === "staged") return scanStaged(ctx.runtime, root);
16485
16828
  if (mode === "history") return scanHistory(ctx.runtime, root);
16486
- return scanPath(ctx.runtime, root, normalizedTarget(target) || ".");
16829
+ return scanPath(ctx.runtime, root, normalizedTarget(target2) || ".");
16487
16830
  };
16488
16831
  var historyIncidents = (ctx, root, fresh3) => fresh3.flatMap((finding3) => {
16489
16832
  if (ruleById(finding3.ruleId)?.kind !== "credential") return [];
@@ -16564,8 +16907,8 @@ var secretsScanCommand = {
16564
16907
  const fix = "finitemonkey help secrets scan";
16565
16908
  if (mode === void 0) return failureFrom(ctx.t, usageProblem("invalid_arguments", "secrets.error.one_mode", fix));
16566
16909
  const root = projectRoot(ctx);
16567
- const target = stringOption(args2.values, "path") ?? ".";
16568
- const scanned = await runScan(ctx, root, mode, target);
16910
+ const target2 = stringOption(args2.values, "path") ?? ".";
16911
+ const scanned = await runScan(ctx, root, mode, target2);
16569
16912
  if (!scanned.ok) return failureFrom(ctx.t, gitUnavailable(scanned.error));
16570
16913
  const baseline = loadBaseline(ctx.runtime, root);
16571
16914
  if (!baseline.ok) return failureFrom(ctx.t, baselineInvalid(baseline.error));
@@ -16573,7 +16916,7 @@ var secretsScanCommand = {
16573
16916
  scanned.value,
16574
16917
  baseline.value,
16575
16918
  ctx.runtime.now(),
16576
- mode === "path" ? scopeOf(target) : void 0
16919
+ mode === "path" ? scopeOf(target2) : void 0
16577
16920
  );
16578
16921
  if (mode === "path") prune(ctx, root, baseline.value, outcome2);
16579
16922
  const incidents = mode === "history" ? historyIncidents(ctx, root, outcome2.fresh) : [];
@@ -16582,7 +16925,7 @@ var secretsScanCommand = {
16582
16925
  };
16583
16926
 
16584
16927
  // src/task/create.ts
16585
- import { join as join39 } from "node:path";
16928
+ import { join as join40 } from "node:path";
16586
16929
 
16587
16930
  // src/core/yaml/write.ts
16588
16931
  var PLAIN_SAFE = /^[A-Za-z_./][A-Za-z0-9_./ ()-]*$/;
@@ -16671,10 +17014,10 @@ var explicitChecks = (draft) => [
16671
17014
  var draftChecks = (runtime, paths, draft) => {
16672
17015
  const disk = {
16673
17016
  read: (relative9) => {
16674
- const text6 = runtime.readText(toAbsolutePath(join39(paths.root, relative9)));
17017
+ const text6 = runtime.readText(toAbsolutePath(join40(paths.root, relative9)));
16675
17018
  return text6.ok ? text6.value : void 0;
16676
17019
  },
16677
- exists: (relative9) => runtime.stat(toAbsolutePath(join39(paths.root, relative9))) !== void 0
17020
+ exists: (relative9) => runtime.stat(toAbsolutePath(join40(paths.root, relative9))) !== void 0
16678
17021
  };
16679
17022
  const detected = detectChecks(disk);
16680
17023
  const base = draft.runs.length > 0 || draft.tests !== void 0 ? explicitChecks(draft) : detected;
@@ -16804,8 +17147,8 @@ var showTask = (ctx, id) => {
16804
17147
  const paths = projectOf(ctx);
16805
17148
  if (paths === void 0) return noRepo(ctx);
16806
17149
  const active = readState(ctx.runtime, paths).active;
16807
- const target = id ?? active;
16808
- const report2 = currentReport(ctx.runtime, paths, void 0, target);
17150
+ const target2 = id ?? active;
17151
+ const report2 = currentReport(ctx.runtime, paths, void 0, target2);
16809
17152
  if (report2 === void 0) {
16810
17153
  return failureFrom(
16811
17154
  ctx.t,
@@ -16813,7 +17156,7 @@ var showTask = (ctx, id) => {
16813
17156
  "task_not_found",
16814
17157
  "error.task_not_found",
16815
17158
  "finitemonkey task new <id> --goal <text>",
16816
- { task: target ?? "-" },
17159
+ { task: target2 ?? "-" },
16817
17160
  ExitCode.NotConfigured
16818
17161
  )
16819
17162
  );
@@ -16823,7 +17166,7 @@ var showTask = (ctx, id) => {
16823
17166
  var doneTask = (ctx, id) => {
16824
17167
  const paths = projectOf(ctx);
16825
17168
  if (paths === void 0) return noRepo(ctx);
16826
- const target = id ?? readState(ctx.runtime, paths).active;
17169
+ const target2 = id ?? readState(ctx.runtime, paths).active;
16827
17170
  const run4 = runGate({
16828
17171
  runtime: ctx.runtime,
16829
17172
  cwd: ctx.cwd,
@@ -16832,7 +17175,7 @@ var doneTask = (ctx, id) => {
16832
17175
  budgetMs: DEFAULT_GATE_BUDGET_MS,
16833
17176
  fast: false,
16834
17177
  noSkip: true,
16835
- ...target === void 0 ? {} : { taskId: target }
17178
+ ...target2 === void 0 ? {} : { taskId: target2 }
16836
17179
  });
16837
17180
  const result = runResult(ctx, run4, true);
16838
17181
  if (!result.ok || run4.kind !== "done" || run4.report.implicit) return result;
@@ -16924,9 +17267,9 @@ var isEmptyDir = (project, relative9) => {
16924
17267
  return listed4.ok && listed4.value.length === 0;
16925
17268
  };
16926
17269
  var finish3 = (project, plan) => {
16927
- const target = plan.purge ? STATE_DIR : LOCK_FILE;
16928
- const removed = project.runtime.remove(project.abs(target));
16929
- if (!removed.ok) return reject({ path: target, detail: removed.error });
17270
+ const target2 = plan.purge ? STATE_DIR : LOCK_FILE;
17271
+ const removed = project.runtime.remove(project.abs(target2));
17272
+ if (!removed.ok) return reject({ path: target2, detail: removed.error });
16930
17273
  for (const dir of plan.removeDirs) if (isEmptyDir(project, dir)) project.runtime.remove(project.abs(dir));
16931
17274
  return accept(void 0);
16932
17275
  };
@@ -17069,7 +17412,10 @@ var REGISTRY = [
17069
17412
  secretsScanCommand,
17070
17413
  secretsAcceptCommand,
17071
17414
  policyFalsePositiveCommand,
17415
+ offCommand,
17416
+ onCommand,
17072
17417
  secretsIncidentCommand,
17418
+ secretsLiveCommand,
17073
17419
  proseCheckCommand,
17074
17420
  snapshotCommand,
17075
17421
  undoCommand,
@@ -17081,7 +17427,7 @@ var REGISTRY = [
17081
17427
  ];
17082
17428
 
17083
17429
  // src/i18n/lang-settings.ts
17084
- import { join as join40 } from "node:path";
17430
+ import { join as join41 } from "node:path";
17085
17431
  var read3 = (runtime, path) => {
17086
17432
  const text6 = runtime.readText(toAbsolutePath(path));
17087
17433
  return text6.ok ? text6.value : void 0;
@@ -17096,14 +17442,14 @@ var languageIn = (text6) => {
17096
17442
  const value = parsed.ok && isRecord(parsed.value) ? parsed.value["language"] : void 0;
17097
17443
  return typeof value === "string" && value.trim() !== "" ? value : void 0;
17098
17444
  };
17099
- var claudeDir = (runtime, env) => env["CLAUDE_CONFIG_DIR"] ?? join40(runtime.homeDir(), ".claude");
17445
+ var claudeDir = (runtime, env) => env["CLAUDE_CONFIG_DIR"] ?? join41(runtime.homeDir(), ".claude");
17100
17446
  var langSettings = (runtime, cwd, env, platform) => {
17101
17447
  const root = findGitRoot(cwd, runtime) ?? cwd;
17102
- const config = parseConfigYaml(read3(runtime, join40(root, CONFIG_FILE)) ?? "");
17448
+ const config = parseConfigYaml(read3(runtime, join41(root, CONFIG_FILE)) ?? "");
17103
17449
  const claudeFiles = [
17104
- join40(root, ".claude", "settings.local.json"),
17105
- join40(root, ".claude", "settings.json"),
17106
- join40(claudeDir(runtime, env), "settings.json")
17450
+ join41(root, ".claude", "settings.local.json"),
17451
+ join41(root, ".claude", "settings.json"),
17452
+ join41(claudeDir(runtime, env), "settings.json")
17107
17453
  ];
17108
17454
  return {
17109
17455
  config: configText(config, "lang"),