finitemonkey 0.1.0-next.11 → 0.1.0-next.13

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.
package/README.es.md CHANGED
@@ -123,6 +123,30 @@ La comparación completa, con fuentes, está en [docs/compare.es.md](docs/compar
123
123
  - **Métodos como superpowers, GSD, Spec Kit u OpenSpec** le dicen al agente cómo planificar y construir. Finite Monkey no los sustituye: comprueba el resultado al final, uses el método que uses.
124
124
  - **Guardas sueltos como tdd-guard, cc-safety-net o dcg** hacen un trabajo cada uno, y lo hacen bien. Finite Monkey cubre cuatro daños con un control, una instalación y un `undo`. Si solo necesitas uno de esos trabajos, un guarda suelto puede ser lo más sencillo.
125
125
 
126
+ <!-- generated:footprint:start -->
127
+ ## Qué lee, escribe y envía
128
+
129
+ Generado desde `src/core/footprint.ts`, la lista que los tests exigen al código. Sin telemetría. Las rutas son relativas al proyecto salvo las que empiezan con `~`.
130
+
131
+ | | Qué | Para qué |
132
+ |---|---|---|
133
+ | Lee | `stdin` | El evento que Claude Code pasa a cada hook: la llamada a la herramienta, el prompt o la petición de parada. |
134
+ | Lee | `.claude/settings.json`, `.claude/settings.local.json`, `~/.claude/settings.json` | La configuración de Claude Code, para comprobar que los hooks están instalados y que nada los desactiva. `$CLAUDE_CONFIG_DIR` reemplaza `~/.claude` si está definido. |
135
+ | Lee | `CLAUDE.md`, `.claude/CLAUDE.md`, `CLAUDE.local.md`, `AGENTS.md`, `.claude/rules/`, `.claudeignore` | Archivos de instrucciones, que `doctor` lee para señalar reglas que solo existen como texto. |
136
+ | Lee | `~/.claude/projects/` | Los logs locales de sesión de Claude Code de este proyecto, leídos una vez y solo con `init --look-back`. |
137
+ | Lee | `.git/` | El repositorio a través del comando `git`: estado, diffs, refs y el reflog. |
138
+ | Lee | `git diff` | Archivos modificados, revisados en busca de secretos, tests saltados, placeholders y patrones de prosa. |
139
+ | Lee | `.finitemonkey/config.yml`, `.finitemonkey/briefing.md` | La configuración del proyecto y el briefing opcional para subagentes. |
140
+ | Escribe | `.claude/settings.json`, `.claude/skills/verify/SKILL.md`, `AGENTS.md`, `CLAUDE.md`, `.gitignore`, `.finitemonkey/config.yml`, `.finitemonkey/lock.json`, `.finitemonkey/locks/`, `.finitemonkey/runtime/`, `.finitemonkey/backups/` | `init` y `update`: la configuración de los hooks, la skill verify, un bloque gestionado corto en `AGENTS.md` que `CLAUDE.md` importa, una línea en `.gitignore`, el runtime fijado y una copia de cada archivo que cambian. |
141
+ | Escribe | `.finitemonkey/tasks/`, `.finitemonkey/state.json` | Contratos de tarea y cuál tarea está activa. |
142
+ | Escribe | `.finitemonkey/evidence/`, `.finitemonkey/log.jsonl`, `.finitemonkey/prod-evidence.jsonl` | La evidencia de cada comprobación, ligada al hash del árbol, y el log de la puerta. |
143
+ | Escribe | `.finitemonkey/trust.json`, `.finitemonkey/allow/`, `.finitemonkey/cache/` | Aprobaciones y permisos escritos por la persona, y una caché breve de los pull requests abiertos. |
144
+ | Escribe | `.finitemonkey/findings/baseline.json`, `.finitemonkey/incidents/` | La línea base de hallazgos aceptados y los incidentes de exposición. Ambos guardan huellas y máscaras, nunca el valor de un secreto. |
145
+ | Escribe | `.finitemonkey/git/`, `.finitemonkey/git-journal.jsonl`, `.finitemonkey/merge-dropped.json`, `refs/finitemonkey/backup/` | Snapshots antes de comandos git destructivos, el diario que lee `undo` y las decisiones sobre líneas descartadas en merges. |
146
+ | Escribe | `.finitemonkey/reports/` | Informes de error enmascarados que escribe `doctor --report`, solo cuando se pide. |
147
+ | Envía | `api.github.com` | A través de `gh`, solo si está instalado y con sesión iniciada: el estado del pull request indicado en una tarea y tus pull requests abiertos para la línea de la sesión. El código nunca se sube. |
148
+ <!-- generated:footprint:end -->
149
+
126
150
  ## Preguntas frecuentes
127
151
 
128
152
  Más preguntas, una por cada búsqueda real, en [docs/faq.es.md](docs/faq.es.md).
@@ -131,7 +155,7 @@ Más preguntas, una por cada búsqueda real, en [docs/faq.es.md](docs/faq.es.md)
131
155
  No. Son hooks y comprobaciones que se ejecutan aunque el modelo no se acuerde.
132
156
 
133
157
  **¿Envía mi código a algún sitio?**
134
- No. Por defecto nada sale de tu máquina. Sin telemetría.
158
+ No. Tu código nunca sale de tu máquina y no hay telemetría. La única llamada de red es `gh` preguntando a GitHub por pull requests, y solo si `gh` tiene sesión iniciada. La lista completa está en [Qué lee, escribe y envía](#qué-lee-escribe-y-envía).
135
159
 
136
160
  **¿Me va a ralentizar?**
137
161
  Las tareas pequeñas no tienen ceremonia, solo el control al final. Los hooks rápidos siguen siendo rápidos; la comprobación costosa va en `Stop`. El CI rechaza un pull request que lleve un guard de `PreToolUse` por encima de 50 ms en el p95 o que agregue contexto al inicio de la sesión; los límites están en [budgets.json](budgets.json). Publicaremos los tiempos de punta a punta junto con el benchmark.
package/README.md CHANGED
@@ -123,6 +123,30 @@ The full comparison, with sources, is in [docs/compare.md](docs/compare.md).
123
123
  - **Methods like superpowers, GSD, Spec Kit or OpenSpec** tell the agent how to plan and build. Finite Monkey doesn't replace them. It checks the result at the end, whatever method you used.
124
124
  - **Single guards like tdd-guard, cc-safety-net or dcg** each do one job well. Finite Monkey covers four damages with one gate, one install and one `undo`. If you only need one of those jobs, a single guard may be the simpler choice.
125
125
 
126
+ <!-- generated:footprint:start -->
127
+ ## What it reads, writes and sends
128
+
129
+ Generated from `src/core/footprint.ts`, the list the tests hold the code to. No telemetry. Paths are relative to the project unless they start with `~`.
130
+
131
+ | | What | Why |
132
+ |---|---|---|
133
+ | Reads | `stdin` | The event Claude Code passes to each hook: the tool call, the prompt or the stop request. |
134
+ | Reads | `.claude/settings.json`, `.claude/settings.local.json`, `~/.claude/settings.json` | Claude Code settings, to check that the hooks are installed and nothing switches them off. `$CLAUDE_CONFIG_DIR` replaces `~/.claude` when set. |
135
+ | Reads | `CLAUDE.md`, `.claude/CLAUDE.md`, `CLAUDE.local.md`, `AGENTS.md`, `.claude/rules/`, `.claudeignore` | Instruction files, read by `doctor` to report rules that exist only as text. |
136
+ | Reads | `~/.claude/projects/` | This project's local Claude Code session logs, read once and only with `init --look-back`. |
137
+ | Reads | `.git/` | The repository through the `git` command: status, diffs, refs and the reflog. |
138
+ | Reads | `git diff` | Changed files, scanned for secrets, test skips, placeholders and prose patterns. |
139
+ | Reads | `.finitemonkey/config.yml`, `.finitemonkey/briefing.md` | Project configuration and the optional briefing for subagents. |
140
+ | Writes | `.claude/settings.json`, `.claude/skills/verify/SKILL.md`, `AGENTS.md`, `CLAUDE.md`, `.gitignore`, `.finitemonkey/config.yml`, `.finitemonkey/lock.json`, `.finitemonkey/locks/`, `.finitemonkey/runtime/`, `.finitemonkey/backups/` | `init` and `update`: hook settings, the verify skill, a short managed block in `AGENTS.md` that `CLAUDE.md` imports, one `.gitignore` line, the pinned runtime and a backup of every file it changes. |
141
+ | Writes | `.finitemonkey/tasks/`, `.finitemonkey/state.json` | Task contracts and which task is active. |
142
+ | Writes | `.finitemonkey/evidence/`, `.finitemonkey/log.jsonl`, `.finitemonkey/prod-evidence.jsonl` | Evidence of each check, tied to the tree hash, and the gate log. |
143
+ | Writes | `.finitemonkey/trust.json`, `.finitemonkey/allow/`, `.finitemonkey/cache/` | Approvals and grants typed by the person, and a short-lived cache of open pull requests. |
144
+ | Writes | `.finitemonkey/findings/baseline.json`, `.finitemonkey/incidents/` | The accepted-findings baseline and exposure incidents. Both hold fingerprints and masks, never a secret value. |
145
+ | Writes | `.finitemonkey/git/`, `.finitemonkey/git-journal.jsonl`, `.finitemonkey/merge-dropped.json`, `refs/finitemonkey/backup/` | Snapshots before destructive git commands, the journal that `undo` reads and dropped-line decisions from merges. |
146
+ | Writes | `.finitemonkey/reports/` | Masked bug reports that `doctor --report` writes, only when asked. |
147
+ | Sends | `api.github.com` | Through `gh`, only when it is installed and logged in: the state of the pull request named in a task, and your open pull requests for the session line. Code is never uploaded. |
148
+ <!-- generated:footprint:end -->
149
+
126
150
  ## FAQ
127
151
 
128
152
  More questions, one per real search, in [docs/faq.md](docs/faq.md).
@@ -131,7 +155,7 @@ More questions, one per real search, in [docs/faq.md](docs/faq.md).
131
155
  No. It is hooks and checks that run whether or not the model remembers them.
132
156
 
133
157
  **Does it send my code anywhere?**
134
- No. Nothing leaves your machine by default. No telemetry.
158
+ No. Your code never leaves your machine, and there is no telemetry. The one network call is `gh` asking GitHub about pull requests, and only when `gh` is logged in. The full list is in [What it reads, writes and sends](#what-it-reads-writes-and-sends).
135
159
 
136
160
  **Will it slow me down?**
137
161
  Small tasks get no ceremony, only the gate at the end. Fast hooks stay fast; the expensive check runs at `Stop`. CI fails a pull request that pushes a `PreToolUse` guard past 50 ms at p95 or adds context at session start; the ceilings are in [budgets.json](budgets.json). We'll publish end-to-end timings with the benchmark.
package/README.pt-BR.md CHANGED
@@ -123,6 +123,30 @@ A comparação completa, com fontes, está em [docs/compare.pt-BR.md](docs/compa
123
123
  - **Métodos como superpowers, GSD, Spec Kit ou OpenSpec** dizem ao agente como planejar e construir. O Finite Monkey não substitui nenhum deles: confere o resultado no fim, seja qual for o método.
124
124
  - **Guardas avulsos como tdd-guard, cc-safety-net ou dcg** fazem um trabalho cada um, e bem. O Finite Monkey cobre quatro danos com um portão, uma instalação e um `undo`. Se você só precisa de um desses trabalhos, um guarda avulso pode ser a escolha mais simples.
125
125
 
126
+ <!-- generated:footprint:start -->
127
+ ## O que ele lê, grava e envia
128
+
129
+ Gerado a partir de `src/core/footprint.ts`, a lista que os testes cobram do código. Sem telemetria. Caminhos são relativos ao projeto, exceto os que começam com `~`.
130
+
131
+ | | O quê | Para quê |
132
+ |---|---|---|
133
+ | Lê | `stdin` | O evento que o Claude Code passa a cada hook: a chamada de ferramenta, o prompt ou o pedido de parada. |
134
+ | Lê | `.claude/settings.json`, `.claude/settings.local.json`, `~/.claude/settings.json` | As configurações do Claude Code, para conferir que os hooks estão instalados e que nada os desliga. `$CLAUDE_CONFIG_DIR` substitui `~/.claude` quando definido. |
135
+ | Lê | `CLAUDE.md`, `.claude/CLAUDE.md`, `CLAUDE.local.md`, `AGENTS.md`, `.claude/rules/`, `.claudeignore` | Arquivos de instrução, lidos pelo `doctor` para apontar regras que só existem como texto. |
136
+ | Lê | `~/.claude/projects/` | Os logs locais de sessão do Claude Code deste projeto, lidos uma vez e só com `init --look-back`. |
137
+ | Lê | `.git/` | O repositório pelo comando `git`: status, diffs, refs e o reflog. |
138
+ | Lê | `git diff` | Arquivos alterados, varridos atrás de segredos, testes pulados, placeholders e padrões de prosa. |
139
+ | Lê | `.finitemonkey/config.yml`, `.finitemonkey/briefing.md` | A configuração do projeto e o briefing opcional para subagentes. |
140
+ | Grava | `.claude/settings.json`, `.claude/skills/verify/SKILL.md`, `AGENTS.md`, `CLAUDE.md`, `.gitignore`, `.finitemonkey/config.yml`, `.finitemonkey/lock.json`, `.finitemonkey/locks/`, `.finitemonkey/runtime/`, `.finitemonkey/backups/` | `init` e `update`: configuração dos hooks, a skill verify, um bloco gerenciado curto no `AGENTS.md` que o `CLAUDE.md` importa, uma linha no `.gitignore`, o runtime fixado e um backup de cada arquivo que alteram. |
141
+ | Grava | `.finitemonkey/tasks/`, `.finitemonkey/state.json` | Contratos de tarefa e qual tarefa está ativa. |
142
+ | Grava | `.finitemonkey/evidence/`, `.finitemonkey/log.jsonl`, `.finitemonkey/prod-evidence.jsonl` | A evidência de cada checagem, ligada ao hash da árvore, e o log do portão. |
143
+ | Grava | `.finitemonkey/trust.json`, `.finitemonkey/allow/`, `.finitemonkey/cache/` | Aprovações e liberações digitadas pela pessoa, e um cache curto dos pull requests abertos. |
144
+ | Grava | `.finitemonkey/findings/baseline.json`, `.finitemonkey/incidents/` | A baseline de achados aceitos e os incidentes de exposição. Os dois guardam impressões e máscaras, nunca o valor de um segredo. |
145
+ | Grava | `.finitemonkey/git/`, `.finitemonkey/git-journal.jsonl`, `.finitemonkey/merge-dropped.json`, `refs/finitemonkey/backup/` | Snapshots antes de comandos git destrutivos, o diário que o `undo` lê e as decisões sobre linhas descartadas em merges. |
146
+ | Grava | `.finitemonkey/reports/` | Relatórios de bug mascarados que o `doctor --report` grava, só quando pedido. |
147
+ | Envia | `api.github.com` | Pelo `gh`, só quando ele está instalado e logado: o estado do pull request citado numa tarefa e os seus pull requests abertos para a linha da sessão. Código nunca é enviado. |
148
+ <!-- generated:footprint:end -->
149
+
126
150
  ## Perguntas frequentes
127
151
 
128
152
  Mais perguntas, uma para cada busca real, em [docs/faq.pt-BR.md](docs/faq.pt-BR.md).
@@ -131,7 +155,7 @@ Mais perguntas, uma para cada busca real, em [docs/faq.pt-BR.md](docs/faq.pt-BR.
131
155
  Não. São hooks e checagens que rodam lembrando o modelo ou não.
132
156
 
133
157
  **Ele manda meu código para algum lugar?**
134
- Não. Por padrão nada sai da sua máquina. Sem telemetria.
158
+ Não. Seu código nunca sai da sua máquina, e não há telemetria. A única chamada de rede é o `gh` perguntando ao GitHub sobre pull requests, e só quando o `gh` está logado. A lista completa está em [O que ele lê, grava e envia](#o-que-ele-lê-grava-e-envia).
135
159
 
136
160
  **Vai me deixar mais lento?**
137
161
  Tarefa pequena não tem cerimônia, só o portão no fim. Hook rápido continua rápido; a checagem cara roda no `Stop`. O CI reprova um pull request que leve um guard de `PreToolUse` acima de 50 ms no p95 ou que acrescente contexto no início da sessão; os limites estão em [budgets.json](budgets.json). Os tempos de ponta a ponta saem junto com o benchmark.
@@ -21,7 +21,7 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
21
21
 
22
22
  // src/core/node-exec.ts
23
23
  import { spawn, spawnSync } from "node:child_process";
24
- import { appendFileSync, mkdirSync } from "node:fs";
24
+ import { appendFileSync, existsSync, mkdirSync } from "node:fs";
25
25
  import { dirname } from "node:path";
26
26
 
27
27
  // src/core/parsed.ts
@@ -45,6 +45,32 @@ var optionalString = (record2, key) => {
45
45
  return typeof value === "string" ? value : void 0;
46
46
  };
47
47
 
48
+ // src/core/windows-command.ts
49
+ import { win32 } from "node:path";
50
+ var DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
51
+ var NEEDS_SHELL = /\.(cmd|bat)$/i;
52
+ var NEEDS_QUOTES = /[\s"&|<>^()%!]/;
53
+ var envValue = (env, name) => {
54
+ const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === name);
55
+ return key === void 0 ? void 0 : env[key];
56
+ };
57
+ var quoteForCmd = (arg) => arg !== "" && !NEEDS_QUOTES.test(arg) ? arg : `"${arg.replace(/"/g, '""')}"`;
58
+ var resolveWindowsCommand = (command, env, exists) => {
59
+ if (/[\\/]/.test(command) || win32.extname(command) !== "") {
60
+ return { command, shell: NEEDS_SHELL.test(command) };
61
+ }
62
+ const dirs = (envValue(env, "PATH") ?? "").split(";").filter((dir) => dir !== "");
63
+ const extensions = (envValue(env, "PATHEXT") ?? DEFAULT_PATHEXT).split(";").filter((ext) => ext !== "");
64
+ for (const dir of dirs) {
65
+ for (const ext of extensions) {
66
+ const candidate = win32.join(dir, `${command}${ext.toLowerCase()}`);
67
+ if (exists(candidate)) return { command: candidate, shell: NEEDS_SHELL.test(candidate) };
68
+ }
69
+ }
70
+ return { command, shell: false };
71
+ };
72
+ var windowsCommandLine = (resolved, args) => [quoteForCmd(resolved.command), ...args.map(quoteForCmd)].join(" ");
73
+
48
74
  // src/core/node-exec.ts
49
75
  var SHELL_NOT_FOUND = /* @__PURE__ */ new Set([127, 9009]);
50
76
  var CMD_NOT_FOUND = /is not recognized as an internal or external command|não é reconhecido como um comando interno|no se reconoce como un comando interno/i;
@@ -67,7 +93,8 @@ var nodeExec = (request) => {
67
93
  Object.entries({ ...process.env, ...request.env }).filter(([name]) => !(request.unset ?? []).includes(name))
68
94
  )
69
95
  };
70
- const result = request.shell === true ? spawnSync([request.command, ...request.args].join(" "), { ...options, shell: true }) : spawnSync(request.command, [...request.args], options);
96
+ const resolved = process.platform === "win32" && request.shell !== true ? resolveWindowsCommand(request.command, options.env, existsSync) : void 0;
97
+ const result = request.shell === true ? spawnSync([request.command, ...request.args].join(" "), { ...options, shell: true }) : resolved?.shell === true ? spawnSync(windowsCommandLine(resolved, request.args), { ...options, shell: true }) : spawnSync(resolved?.command ?? request.command, [...request.args], options);
71
98
  const code = errnoCode(result.error);
72
99
  const exitCode = result.status;
73
100
  const shellMissing = request.shell === true && exitCode !== null && (SHELL_NOT_FOUND.has(exitCode) || exitCode === 1 && CMD_NOT_FOUND.test(text(result.stderr)));
@@ -1132,6 +1159,12 @@ var isBundlePath = (path) => path.endsWith(".mjs");
1132
1159
 
1133
1160
  // src/install/plan.ts
1134
1161
  var CHANGING = ["create", "merge", "update"];
1162
+ var WSL_MOUNT = /^\/mnt\/[a-z](\/|$)/i;
1163
+ var SYNCED = /(OneDrive|Dropbox|iCloud ?Drive|Mobile Documents|Google ?Drive)/i;
1164
+ var locationOf = (root, env) => ({
1165
+ wslMount: WSL_MOUNT.test(root) && (env["WSL_DISTRO_NAME"] !== void 0 || env["WSL_INTEROP"] !== void 0),
1166
+ synced: SYNCED.test(root)
1167
+ });
1135
1168
  var detect = (project, gitRoot2, input) => ({
1136
1169
  root: project.root,
1137
1170
  gitRoot: gitRoot2,
@@ -1140,7 +1173,8 @@ var detect = (project, gitRoot2, input) => ({
1140
1173
  settings: settingsInfo(project.read(SETTINGS_FILE)),
1141
1174
  authorities: detectAuthorities(project, input.env),
1142
1175
  runner: chooseRunner(project, input.bin),
1143
- claude: detectClaude(project.runtime.probe)
1176
+ claude: detectClaude(project.runtime.probe),
1177
+ location: locationOf(project.root, input.env)
1144
1178
  });
1145
1179
  var readPreviousLock = (project) => {
1146
1180
  const text6 = project.read(LOCK_FILE);
@@ -1996,20 +2030,20 @@ var detectOs = ({ platform, env, runtime }) => {
1996
2030
  const version = runtime.readText(toAbsolutePath("/proc/version"));
1997
2031
  return version.ok && /microsoft/i.test(version.value) ? "wsl" : "linux";
1998
2032
  };
1999
- var WSL_MOUNT = /^\/mnt\/[a-z](\/|$)/i;
2000
- var SYNCED = /(OneDrive|Dropbox|iCloud ?Drive|Mobile Documents|Google ?Drive)/i;
2033
+ var WSL_MOUNT2 = /^\/mnt\/[a-z](\/|$)/i;
2034
+ var SYNCED2 = /(OneDrive|Dropbox|iCloud ?Drive|Mobile Documents|Google ?Drive)/i;
2001
2035
  var NON_ASCII = /[^\x20-\x7e]/;
2002
2036
  var pathTraits = (path) => ({
2003
2037
  spaces: path.includes(" "),
2004
2038
  nonAscii: NON_ASCII.test(path),
2005
- synced: SYNCED.test(path)
2039
+ synced: SYNCED2.test(path)
2006
2040
  });
2007
2041
  var checkOs = (env) => {
2008
2042
  const os = detectOs(env);
2009
2043
  const findings = [
2010
2044
  { check: "os", status: "ok", messageKey: `doctor.os.${os}`, params: { platform: env.platform } }
2011
2045
  ];
2012
- if (os === "wsl" && WSL_MOUNT.test(env.cwd)) {
2046
+ if (os === "wsl" && WSL_MOUNT2.test(env.cwd)) {
2013
2047
  findings.push({
2014
2048
  check: "path.wsl_mount",
2015
2049
  status: "warn",
@@ -5088,6 +5122,8 @@ var en_default = {
5088
5122
  "install.plan.title": "Finite Monkey plan for {root} (preset {preset}):",
5089
5123
  "install.warning": "warning: {message}",
5090
5124
  "install.warn.not_git": "this folder is not inside a git repository; installing here anyway",
5125
+ "install.warn.wsl_mount": "{path} is on a Windows drive mounted in WSL: git and hooks run several times slower and file watching misses changes; move the project to the Linux disk (for example ~/projects) and run init there",
5126
+ "install.warn.synced_folder": "{path} is inside a synced folder (OneDrive, Dropbox, iCloud or Google Drive): sync can lock or rewrite files while git and hooks use them; move the project out of the synced folder",
5091
5127
  "install.warn.fail_open": "Claude Code {version} cannot block an action when a hook crashes (needs 2.1.295 or newer); guards fail open until you update Claude Code",
5092
5128
  "install.warn.authority": "{name} ({source}) also decides when a session may stop; Finite Monkey works alongside it and never turns it off",
5093
5129
  "install.warn.plugin": "the finitemonkey plugin is enabled ({source}); its hooks step aside while this project has its own Finite Monkey hooks",
@@ -5375,7 +5411,16 @@ var en_default = {
5375
5411
  "gate.merge.clean": "{count, plural, one {# merge checked} other {# merges checked}}, nothing lost",
5376
5412
  "doctor.memory.lines": "{file}: {lines, plural, one {# line} other {# lines}} of {max}. Edits made during a session apply only after /clear, /compact or a restart.",
5377
5413
  "doctor.memory.too_long": "{file}: {lines, plural, one {# line} other {# lines}}, above the ceiling of {max}; long instruction files get followed less",
5378
- "doctor.memory.fix_too_long": "Move details out of {file} into docs or skills until it has at most {max} lines, or raise memory.maxLines in .finitemonkey/config.yml"
5414
+ "doctor.memory.fix_too_long": "Move details out of {file} into docs or skills until it has at most {max} lines, or raise memory.maxLines in .finitemonkey/config.yml",
5415
+ "doctor.git.autocrlf": 'git converts line endings to CRLF on checkout (core.autocrlf=true) and {file} does not pin LF; shell hooks and scripts checked out with CRLF fail with "bad interpreter"',
5416
+ "doctor.git.fix_autocrlf": "add `* text=auto eol=lf` to {file}, commit it, then run `git add --renormalize .`",
5417
+ "history.title": "Looking back at this project's local Claude Code sessions: {sessions, plural, one {# session} other {# sessions}}, {turns, plural, one {# turn} other {# turns}}.",
5418
+ "history.pattern.done_after_failing_tests": '{count, plural, one {# turn} other {# turns}} ended with "done" right after a failing test run',
5419
+ "history.pattern.env_read": "{count, plural, one {# read} other {# reads}} of a .env file",
5420
+ "history.pattern.no_verify": "{count, plural, one {# git command} other {# git commands}} with --no-verify",
5421
+ "history.pattern.force_push": "{count, plural, one {# force push} other {# force pushes}}",
5422
+ "history.pattern.test_skip": "{count, plural, one {# test edit} other {# test edits}} that added .skip, .only or a skip marker",
5423
+ "history.estimate": "These are estimates from pattern matching, not evidence; nothing was saved and nothing left this machine."
5379
5424
  };
5380
5425
 
5381
5426
  // src/i18n/es.json
@@ -5588,6 +5633,8 @@ var es_default = {
5588
5633
  "install.plan.title": "Plan de Finite Monkey para {root} (preset {preset}):",
5589
5634
  "install.warning": "aviso: {message}",
5590
5635
  "install.warn.not_git": "esta carpeta no está dentro de un repositorio git; se instala aquí de todos modos",
5636
+ "install.warn.wsl_mount": "{path} está en un disco de Windows montado en WSL: git y los hooks van varias veces más lentos y la vigilancia de archivos pierde cambios; mueve el proyecto al disco de Linux (por ejemplo ~/proyectos) y ejecuta init allí",
5637
+ "install.warn.synced_folder": "{path} está dentro de una carpeta sincronizada (OneDrive, Dropbox, iCloud o Google Drive): la sincronización puede bloquear o reescribir archivos mientras git y los hooks los usan; saca el proyecto de la carpeta sincronizada",
5591
5638
  "install.warn.fail_open": "Claude Code {version} no puede bloquear la acción cuando un hook falla (necesita 2.1.295 o más nueva); los guardas fallan abiertos hasta que actualices Claude Code",
5592
5639
  "install.warn.authority": "{name} ({source}) también decide cuándo puede terminar la sesión; Finite Monkey convive con él y nunca lo desactiva",
5593
5640
  "install.warn.plugin": "el plugin finitemonkey está habilitado ({source}); sus hooks ceden el paso mientras este proyecto tenga sus propios hooks de Finite Monkey",
@@ -5875,7 +5922,16 @@ var es_default = {
5875
5922
  "gate.merge.clean": "{count, plural, one {# fusión revisada} other {# fusiones revisadas}}, no se perdió nada",
5876
5923
  "doctor.memory.lines": "{file}: {lines, plural, one {# línea} other {# líneas}} de {max}. Los cambios hechos durante una sesión solo se aplican tras /clear, /compact o reiniciar.",
5877
5924
  "doctor.memory.too_long": "{file}: {lines, plural, one {# línea} other {# líneas}}, por encima del techo de {max}; un archivo de instrucciones largo se sigue menos",
5878
- "doctor.memory.fix_too_long": "Lleva detalles de {file} a docs o skills hasta que tenga como máximo {max} líneas, o sube memory.maxLines en .finitemonkey/config.yml"
5925
+ "doctor.memory.fix_too_long": "Lleva detalles de {file} a docs o skills hasta que tenga como máximo {max} líneas, o sube memory.maxLines en .finitemonkey/config.yml",
5926
+ "doctor.git.autocrlf": 'git convierte los saltos de línea a CRLF al hacer checkout (core.autocrlf=true) y {file} no fija LF; los hooks y scripts de shell con CRLF fallan con "bad interpreter"',
5927
+ "doctor.git.fix_autocrlf": "añade `* text=auto eol=lf` a {file}, haz commit y ejecuta `git add --renormalize .`",
5928
+ "history.title": "Revisando las sesiones locales de Claude Code en este proyecto: {sessions, plural, one {# sesión} other {# sesiones}}, {turns, plural, one {# turno} other {# turnos}}.",
5929
+ "history.pattern.done_after_failing_tests": '{count, plural, one {# turno terminó} other {# turnos terminaron}} con "terminado" justo después de pruebas fallidas',
5930
+ "history.pattern.env_read": "{count, plural, one {# lectura} other {# lecturas}} de un archivo .env",
5931
+ "history.pattern.no_verify": "{count, plural, one {# comando git} other {# comandos git}} con --no-verify",
5932
+ "history.pattern.force_push": "{count, plural, one {# push forzado} other {# push forzados}}",
5933
+ "history.pattern.test_skip": "{count, plural, one {# edición} other {# ediciones}} de pruebas que añadieron .skip, .only o una marca para saltar",
5934
+ "history.estimate": "Son estimaciones por patrones de texto, no evidencia; no se guardó nada y nada salió de esta máquina."
5879
5935
  };
5880
5936
 
5881
5937
  // src/i18n/pt-BR.json
@@ -6088,6 +6144,8 @@ var pt_BR_default = {
6088
6144
  "install.plan.title": "Plano do Finite Monkey para {root} (preset {preset}):",
6089
6145
  "install.warning": "aviso: {message}",
6090
6146
  "install.warn.not_git": "esta pasta não está dentro de um repositório git; instalando aqui mesmo assim",
6147
+ "install.warn.wsl_mount": "{path} está num disco do Windows montado no WSL: git e hooks ficam várias vezes mais lentos e a observação de arquivos perde mudanças; mova o projeto para o disco do Linux (por exemplo ~/projetos) e rode o init lá",
6148
+ "install.warn.synced_folder": "{path} está dentro de uma pasta sincronizada (OneDrive, Dropbox, iCloud ou Google Drive): a sincronização pode travar ou reescrever arquivos enquanto o git e os hooks os usam; tire o projeto da pasta sincronizada",
6091
6149
  "install.warn.fail_open": "o Claude Code {version} não consegue barrar a ação quando um hook quebra (precisa da 2.1.295 ou mais nova); as travas falham abertas até você atualizar o Claude Code",
6092
6150
  "install.warn.authority": "{name} ({source}) também decide quando a sessão pode parar; o Finite Monkey convive com ele e nunca o desliga",
6093
6151
  "install.warn.plugin": "o plugin finitemonkey está habilitado ({source}); os hooks dele cedem a vez enquanto este projeto tiver os próprios hooks do Finite Monkey",
@@ -6375,7 +6433,16 @@ var pt_BR_default = {
6375
6433
  "gate.merge.clean": "{count, plural, one {# mescla conferida} other {# mesclas conferidas}}, nada se perdeu",
6376
6434
  "doctor.memory.lines": "{file}: {lines, plural, one {# linha} other {# linhas}} de {max}. Edições feitas durante a sessão só valem depois de /clear, /compact ou reiniciar.",
6377
6435
  "doctor.memory.too_long": "{file}: {lines, plural, one {# linha} other {# linhas}}, acima do teto de {max}; arquivo de instruções longo é menos seguido",
6378
- "doctor.memory.fix_too_long": "Leve detalhes de {file} para docs ou skills até ficar com no máximo {max} linhas, ou aumente memory.maxLines em .finitemonkey/config.yml"
6436
+ "doctor.memory.fix_too_long": "Leve detalhes de {file} para docs ou skills até ficar com no máximo {max} linhas, ou aumente memory.maxLines em .finitemonkey/config.yml",
6437
+ "doctor.git.autocrlf": 'o git converte quebras de linha para CRLF no checkout (core.autocrlf=true) e o {file} não fixa LF; hooks e scripts de shell com CRLF falham com "bad interpreter"',
6438
+ "doctor.git.fix_autocrlf": "acrescente `* text=auto eol=lf` ao {file}, faça commit e rode `git add --renormalize .`",
6439
+ "history.title": "Olhando as sessões locais do Claude Code neste projeto: {sessions, plural, one {# sessão} other {# sessões}}, {turns, plural, one {# turno} other {# turnos}}.",
6440
+ "history.pattern.done_after_failing_tests": '{count, plural, one {# turno terminou} other {# turnos terminaram}} com "pronto" logo depois de testes falhando',
6441
+ "history.pattern.env_read": "{count, plural, one {# leitura} other {# leituras}} de arquivo .env",
6442
+ "history.pattern.no_verify": "{count, plural, one {# comando git} other {# comandos git}} com --no-verify",
6443
+ "history.pattern.force_push": "{count, plural, one {# push forçado} other {# pushes forçados}}",
6444
+ "history.pattern.test_skip": "{count, plural, one {# edição} other {# edições}} de teste que acrescentaram .skip, .only ou marcador de pular",
6445
+ "history.estimate": "São estimativas por padrão de texto, não evidência; nada foi salvo e nada saiu desta máquina."
6379
6446
  };
6380
6447
 
6381
6448
  // src/i18n/review.json
@@ -6493,6 +6560,43 @@ var review_default = {
6493
6560
  "doctor.project.fix_lockfiles": "machine",
6494
6561
  "doctor.project.configs": "machine",
6495
6562
  "doctor.project.fix_configs": "machine",
6563
+ "doctor.hooks.ok": "machine",
6564
+ "doctor.hooks.no_command": "machine",
6565
+ "doctor.hooks.not_found": "machine",
6566
+ "doctor.hooks.timeout": "machine",
6567
+ "doctor.hooks.exit": "machine",
6568
+ "doctor.hooks.output": "machine",
6569
+ "doctor.hooks.fix_broken": "machine",
6570
+ "doctor.hooks.fix_broken_plugin": "machine",
6571
+ "doctor.hooks.slow": "machine",
6572
+ "doctor.hooks.fix_slow": "machine",
6573
+ "doctor.hooks.fail_open": "machine",
6574
+ "doctor.hooks.fix_fail_open": "machine",
6575
+ "doctor.hooks.matcher": "machine",
6576
+ "doctor.hooks.fix_matcher": "machine",
6577
+ "doctor.hooks.not_fired": "machine",
6578
+ "doctor.hooks.fix_not_fired": "machine",
6579
+ "doctor.hooks.schema": "machine",
6580
+ "doctor.hooks.fix_schema": "machine",
6581
+ "doctor.hooks.plugin_disabled": "machine",
6582
+ "doctor.hooks.fix_plugin_disabled": "machine",
6583
+ "doctor.hooks.none": "machine",
6584
+ "doctor.hooks.fix_none": "machine",
6585
+ "doctor.hooks.absent": "machine",
6586
+ "doctor.hooks.fix_absent": "machine",
6587
+ "doctor.hooks.version_ok": "machine",
6588
+ "doctor.hooks.version_plugin": "machine",
6589
+ "doctor.hooks.fix_version_plugin": "machine",
6590
+ "doctor.hooks.version_cli": "machine",
6591
+ "doctor.hooks.fix_version_cli": "machine",
6592
+ "doctor.hooks.fix_version_cli_plugin": "machine",
6593
+ "doctor.fix.not_installed": "machine",
6594
+ "doctor.fix.nothing": "machine",
6595
+ "doctor.fix.would": "machine",
6596
+ "doctor.fix.applied": "machine",
6597
+ "doctor.fix.backup": "machine",
6598
+ "doctor.fix.manual": "machine",
6599
+ "doctor.report.written": "machine",
6496
6600
  "hook.failed_closed": "machine",
6497
6601
  "hook.failed_open": "machine",
6498
6602
  "exit.ok": "machine",
@@ -6552,6 +6656,8 @@ var review_default = {
6552
6656
  "install.plan.title": "machine",
6553
6657
  "install.warning": "machine",
6554
6658
  "install.warn.not_git": "machine",
6659
+ "install.warn.wsl_mount": "machine",
6660
+ "install.warn.synced_folder": "machine",
6555
6661
  "install.warn.fail_open": "machine",
6556
6662
  "install.warn.authority": "machine",
6557
6663
  "install.warn.plugin": "machine",
@@ -6840,43 +6946,15 @@ var review_default = {
6840
6946
  "doctor.memory.lines": "machine",
6841
6947
  "doctor.memory.too_long": "machine",
6842
6948
  "doctor.memory.fix_too_long": "machine",
6843
- "doctor.hooks.ok": "machine",
6844
- "doctor.hooks.no_command": "machine",
6845
- "doctor.hooks.not_found": "machine",
6846
- "doctor.hooks.timeout": "machine",
6847
- "doctor.hooks.exit": "machine",
6848
- "doctor.hooks.output": "machine",
6849
- "doctor.hooks.fix_broken": "machine",
6850
- "doctor.hooks.fix_broken_plugin": "machine",
6851
- "doctor.hooks.slow": "machine",
6852
- "doctor.hooks.fix_slow": "machine",
6853
- "doctor.hooks.fail_open": "machine",
6854
- "doctor.hooks.fix_fail_open": "machine",
6855
- "doctor.hooks.matcher": "machine",
6856
- "doctor.hooks.fix_matcher": "machine",
6857
- "doctor.hooks.not_fired": "machine",
6858
- "doctor.hooks.fix_not_fired": "machine",
6859
- "doctor.hooks.schema": "machine",
6860
- "doctor.hooks.fix_schema": "machine",
6861
- "doctor.hooks.plugin_disabled": "machine",
6862
- "doctor.hooks.fix_plugin_disabled": "machine",
6863
- "doctor.hooks.none": "machine",
6864
- "doctor.hooks.fix_none": "machine",
6865
- "doctor.hooks.absent": "machine",
6866
- "doctor.hooks.fix_absent": "machine",
6867
- "doctor.hooks.version_ok": "machine",
6868
- "doctor.hooks.version_plugin": "machine",
6869
- "doctor.hooks.fix_version_plugin": "machine",
6870
- "doctor.hooks.version_cli": "machine",
6871
- "doctor.hooks.fix_version_cli": "machine",
6872
- "doctor.hooks.fix_version_cli_plugin": "machine",
6873
- "doctor.fix.not_installed": "machine",
6874
- "doctor.fix.nothing": "machine",
6875
- "doctor.fix.would": "machine",
6876
- "doctor.fix.applied": "machine",
6877
- "doctor.fix.backup": "machine",
6878
- "doctor.fix.manual": "machine",
6879
- "doctor.report.written": "machine"
6949
+ "doctor.git.autocrlf": "machine",
6950
+ "doctor.git.fix_autocrlf": "machine",
6951
+ "history.title": "machine",
6952
+ "history.pattern.done_after_failing_tests": "machine",
6953
+ "history.pattern.env_read": "machine",
6954
+ "history.pattern.no_verify": "machine",
6955
+ "history.pattern.force_push": "machine",
6956
+ "history.pattern.test_skip": "machine",
6957
+ "history.estimate": "machine"
6880
6958
  },
6881
6959
  es: {
6882
6960
  "command.init.summary": "machine",
@@ -6991,6 +7069,43 @@ var review_default = {
6991
7069
  "doctor.project.fix_lockfiles": "machine",
6992
7070
  "doctor.project.configs": "machine",
6993
7071
  "doctor.project.fix_configs": "machine",
7072
+ "doctor.hooks.ok": "machine",
7073
+ "doctor.hooks.no_command": "machine",
7074
+ "doctor.hooks.not_found": "machine",
7075
+ "doctor.hooks.timeout": "machine",
7076
+ "doctor.hooks.exit": "machine",
7077
+ "doctor.hooks.output": "machine",
7078
+ "doctor.hooks.fix_broken": "machine",
7079
+ "doctor.hooks.fix_broken_plugin": "machine",
7080
+ "doctor.hooks.slow": "machine",
7081
+ "doctor.hooks.fix_slow": "machine",
7082
+ "doctor.hooks.fail_open": "machine",
7083
+ "doctor.hooks.fix_fail_open": "machine",
7084
+ "doctor.hooks.matcher": "machine",
7085
+ "doctor.hooks.fix_matcher": "machine",
7086
+ "doctor.hooks.not_fired": "machine",
7087
+ "doctor.hooks.fix_not_fired": "machine",
7088
+ "doctor.hooks.schema": "machine",
7089
+ "doctor.hooks.fix_schema": "machine",
7090
+ "doctor.hooks.plugin_disabled": "machine",
7091
+ "doctor.hooks.fix_plugin_disabled": "machine",
7092
+ "doctor.hooks.none": "machine",
7093
+ "doctor.hooks.fix_none": "machine",
7094
+ "doctor.hooks.absent": "machine",
7095
+ "doctor.hooks.fix_absent": "machine",
7096
+ "doctor.hooks.version_ok": "machine",
7097
+ "doctor.hooks.version_plugin": "machine",
7098
+ "doctor.hooks.fix_version_plugin": "machine",
7099
+ "doctor.hooks.version_cli": "machine",
7100
+ "doctor.hooks.fix_version_cli": "machine",
7101
+ "doctor.hooks.fix_version_cli_plugin": "machine",
7102
+ "doctor.fix.not_installed": "machine",
7103
+ "doctor.fix.nothing": "machine",
7104
+ "doctor.fix.would": "machine",
7105
+ "doctor.fix.applied": "machine",
7106
+ "doctor.fix.backup": "machine",
7107
+ "doctor.fix.manual": "machine",
7108
+ "doctor.report.written": "machine",
6994
7109
  "hook.failed_closed": "machine",
6995
7110
  "hook.failed_open": "machine",
6996
7111
  "exit.ok": "machine",
@@ -7050,6 +7165,8 @@ var review_default = {
7050
7165
  "install.plan.title": "machine",
7051
7166
  "install.warning": "machine",
7052
7167
  "install.warn.not_git": "machine",
7168
+ "install.warn.wsl_mount": "machine",
7169
+ "install.warn.synced_folder": "machine",
7053
7170
  "install.warn.fail_open": "machine",
7054
7171
  "install.warn.authority": "machine",
7055
7172
  "install.warn.plugin": "machine",
@@ -7338,43 +7455,15 @@ var review_default = {
7338
7455
  "doctor.memory.lines": "machine",
7339
7456
  "doctor.memory.too_long": "machine",
7340
7457
  "doctor.memory.fix_too_long": "machine",
7341
- "doctor.hooks.ok": "machine",
7342
- "doctor.hooks.no_command": "machine",
7343
- "doctor.hooks.not_found": "machine",
7344
- "doctor.hooks.timeout": "machine",
7345
- "doctor.hooks.exit": "machine",
7346
- "doctor.hooks.output": "machine",
7347
- "doctor.hooks.fix_broken": "machine",
7348
- "doctor.hooks.fix_broken_plugin": "machine",
7349
- "doctor.hooks.slow": "machine",
7350
- "doctor.hooks.fix_slow": "machine",
7351
- "doctor.hooks.fail_open": "machine",
7352
- "doctor.hooks.fix_fail_open": "machine",
7353
- "doctor.hooks.matcher": "machine",
7354
- "doctor.hooks.fix_matcher": "machine",
7355
- "doctor.hooks.not_fired": "machine",
7356
- "doctor.hooks.fix_not_fired": "machine",
7357
- "doctor.hooks.schema": "machine",
7358
- "doctor.hooks.fix_schema": "machine",
7359
- "doctor.hooks.plugin_disabled": "machine",
7360
- "doctor.hooks.fix_plugin_disabled": "machine",
7361
- "doctor.hooks.none": "machine",
7362
- "doctor.hooks.fix_none": "machine",
7363
- "doctor.hooks.absent": "machine",
7364
- "doctor.hooks.fix_absent": "machine",
7365
- "doctor.hooks.version_ok": "machine",
7366
- "doctor.hooks.version_plugin": "machine",
7367
- "doctor.hooks.fix_version_plugin": "machine",
7368
- "doctor.hooks.version_cli": "machine",
7369
- "doctor.hooks.fix_version_cli": "machine",
7370
- "doctor.hooks.fix_version_cli_plugin": "machine",
7371
- "doctor.fix.not_installed": "machine",
7372
- "doctor.fix.nothing": "machine",
7373
- "doctor.fix.would": "machine",
7374
- "doctor.fix.applied": "machine",
7375
- "doctor.fix.backup": "machine",
7376
- "doctor.fix.manual": "machine",
7377
- "doctor.report.written": "machine"
7458
+ "doctor.git.autocrlf": "machine",
7459
+ "doctor.git.fix_autocrlf": "machine",
7460
+ "history.title": "machine",
7461
+ "history.pattern.done_after_failing_tests": "machine",
7462
+ "history.pattern.env_read": "machine",
7463
+ "history.pattern.no_verify": "machine",
7464
+ "history.pattern.force_push": "machine",
7465
+ "history.pattern.test_skip": "machine",
7466
+ "history.estimate": "machine"
7378
7467
  }
7379
7468
  };
7380
7469
 
@@ -10952,11 +11041,11 @@ var withAlias = (invocation, expansion) => ({
10952
11041
 
10953
11042
  // src/git/path-key.ts
10954
11043
  import { posix as posix2 } from "node:path";
10955
- var WSL_MOUNT2 = /^\/mnt\/([a-zA-Z])(\/|$)/;
11044
+ var WSL_MOUNT3 = /^\/mnt\/([a-zA-Z])(\/|$)/;
10956
11045
  var DRIVE = /^([a-zA-Z]):(\/|$)/;
10957
11046
  var pathKey = (path) => {
10958
11047
  const posixPath = toPosixPath(path).replace(
10959
- WSL_MOUNT2,
11048
+ WSL_MOUNT3,
10960
11049
  (_match, letter, slash) => `${letter}:${slash || "/"}`
10961
11050
  );
10962
11051
  const drive = DRIVE.exec(posixPath);
@@ -13185,8 +13274,21 @@ var checkInstructions = (env) => {
13185
13274
  ];
13186
13275
  };
13187
13276
 
13188
- // src/doctor/memory.ts
13277
+ // src/doctor/line-endings.ts
13189
13278
  import { join as join30 } from "node:path";
13279
+ var LF_RULE = /^\*\s.*\beol=lf\b/m;
13280
+ var checkLineEndings = ({ root, runtime }) => {
13281
+ const autocrlf = runtime.git(["config", "--get", "core.autocrlf"], root);
13282
+ if (!autocrlf.ok || autocrlf.stdout.trim().toLowerCase() !== "true") return [];
13283
+ const attributes = runtime.readText(toAbsolutePath(join30(root, ".gitattributes")));
13284
+ if (attributes.ok && LF_RULE.test(attributes.value)) return [];
13285
+ return [
13286
+ finding("git.autocrlf", "warn", "doctor.git.autocrlf", { file: ".gitattributes" }, "doctor.git.fix_autocrlf")
13287
+ ];
13288
+ };
13289
+
13290
+ // src/doctor/memory.ts
13291
+ import { join as join31 } from "node:path";
13190
13292
  var ceilingOf = (config) => {
13191
13293
  const value = configAt(parseConfigYaml(config ?? ""), "memory", "maxLines");
13192
13294
  const parsed = typeof value === "string" ? Number.parseInt(value, 10) : Number.NaN;
@@ -13194,9 +13296,9 @@ var ceilingOf = (config) => {
13194
13296
  };
13195
13297
  var lineCount2 = (text6) => text6.replace(/\r?\n$/, "").split(/\r?\n/).length;
13196
13298
  var checkAgentsFile = ({ root, runtime }) => {
13197
- const text6 = runtime.readText(toAbsolutePath(join30(root, AGENTS_FILE)));
13299
+ const text6 = runtime.readText(toAbsolutePath(join31(root, AGENTS_FILE)));
13198
13300
  if (!text6.ok) return [];
13199
- const config = runtime.readText(toAbsolutePath(join30(root, CONFIG_FILE)));
13301
+ const config = runtime.readText(toAbsolutePath(join31(root, CONFIG_FILE)));
13200
13302
  const max = ceilingOf(config.ok ? config.value : void 0);
13201
13303
  const lines3 = lineCount2(text6.value);
13202
13304
  const params2 = { file: AGENTS_FILE, lines: lines3, max };
@@ -13204,7 +13306,7 @@ var checkAgentsFile = ({ root, runtime }) => {
13204
13306
  };
13205
13307
 
13206
13308
  // src/doctor/project-files.ts
13207
- import { join as join31 } from "node:path";
13309
+ import { join as join32 } from "node:path";
13208
13310
  var variants = (base, extensions) => extensions.map((extension) => `${base}${extension}`);
13209
13311
  var SCRIPT = [".js", ".cjs", ".mjs", ".ts", ".cts", ".mts"];
13210
13312
  var DATA = ["", ".json", ".yml", ".yaml"];
@@ -13235,7 +13337,7 @@ var CONFIG_GROUPS = [
13235
13337
  files: [...variants("commitlint.config", SCRIPT), ...variants(".commitlintrc", [...DATA, ".js"])]
13236
13338
  }
13237
13339
  ];
13238
- var present = (env, files) => files.filter((file) => env.runtime.stat(toAbsolutePath(join31(env.root, file)))?.kind === "file");
13340
+ var present = (env, files) => files.filter((file) => env.runtime.stat(toAbsolutePath(join32(env.root, file)))?.kind === "file");
13239
13341
  var crowded = (env, groups, build) => groups.flatMap(({ name, files }) => {
13240
13342
  const found = present(env, files);
13241
13343
  return found.length > 1 ? [build(name, found.join(", "))] : [];
@@ -13273,6 +13375,7 @@ var CHECKS = [
13273
13375
  checkClaudeSettings,
13274
13376
  checkInstructions,
13275
13377
  checkProjectFiles,
13378
+ checkLineEndings,
13276
13379
  checkAgentsFile,
13277
13380
  checkHooks
13278
13381
  ];
@@ -13800,7 +13903,7 @@ var rescueCommand = {
13800
13903
  };
13801
13904
 
13802
13905
  // src/git/undo.ts
13803
- import { basename as basename3, dirname as dirname8, join as join32 } from "node:path";
13906
+ import { basename as basename3, dirname as dirname8, join as join33 } from "node:path";
13804
13907
  var INDEX_SUBJECT = "finitemonkey index:";
13805
13908
  var gitLine = (runtime, repo, args) => {
13806
13909
  const result = runtime.git(args, repo.root);
@@ -13843,7 +13946,7 @@ var findSnapshot = (views, wanted) => {
13843
13946
  };
13844
13947
  var stampOf2 = (iso) => iso.replace(/[:.]/g, "-");
13845
13948
  var worktreePath = (runtime, repo) => {
13846
- const base = join32(dirname8(repo.root), `${basename3(repo.root)}-undo-${stampOf2(runtime.now())}`);
13949
+ const base = join33(dirname8(repo.root), `${basename3(repo.root)}-undo-${stampOf2(runtime.now())}`);
13847
13950
  return toAbsolutePath(runtime.stat(toAbsolutePath(base)) === void 0 ? base : `${base}-2`);
13848
13951
  };
13849
13952
  var intoWorktree = (runtime, repo, view2) => {
@@ -14032,13 +14135,13 @@ var helpCommand = {
14032
14135
  };
14033
14136
 
14034
14137
  // src/install/plugin-yield.ts
14035
- import { join as join33 } from "node:path";
14138
+ import { join as join34 } from "node:path";
14036
14139
  var PLUGIN_VIA = "plugin";
14037
14140
  var YIELD_STDOUT = `${JSON.stringify({ continue: true })}
14038
14141
  `;
14039
14142
  var projectOwnsHooks = (ctx) => {
14040
14143
  const root = ctx.env["CLAUDE_PROJECT_DIR"] ?? ctx.cwd;
14041
- const text6 = ctx.runtime.readText(toAbsolutePath(join33(root, ...SETTINGS_FILE.split("/"))));
14144
+ const text6 = ctx.runtime.readText(toAbsolutePath(join34(root, ...SETTINGS_FILE.split("/"))));
14042
14145
  if (!text6.ok) return false;
14043
14146
  const parsed = parseJsonObject(text6.value);
14044
14147
  return parsed.ok && ownHandlersIn(parsed.value) > 0;
@@ -14091,7 +14194,7 @@ var hookCommand = {
14091
14194
  };
14092
14195
 
14093
14196
  // src/commands/i18n-check.ts
14094
- import { join as join34 } from "node:path";
14197
+ import { join as join35 } from "node:path";
14095
14198
 
14096
14199
  // src/i18n/message.ts
14097
14200
  var ARG = /^\s*([a-zA-Z][a-zA-Z0-9_]*)\s*(?:,\s*(plural|select)\s*,)?/;
@@ -14293,7 +14396,7 @@ var unreadable = (dir, file, detail) => ({
14293
14396
  exitCode: ExitCode.NotConfigured
14294
14397
  });
14295
14398
  var readCatalog = (ctx, dir, lang) => {
14296
- const file = toAbsolutePath(join34(dir, `${lang}.json`));
14399
+ const file = toAbsolutePath(join35(dir, `${lang}.json`));
14297
14400
  const text6 = ctx.runtime.readText(file);
14298
14401
  if (!text6.ok) return reject(unreadable(dir, file, text6.error));
14299
14402
  const parsed = parseJsonObject(text6.value);
@@ -14316,7 +14419,7 @@ var describeProblem = (ctx, problem4) => {
14316
14419
  return ` ${problem4.lang} ${problem4.key} ${kind}`;
14317
14420
  };
14318
14421
  var loadReview = (ctx, dir) => {
14319
- const text6 = ctx.runtime.readText(toAbsolutePath(join34(dir, "review.json")));
14422
+ const text6 = ctx.runtime.readText(toAbsolutePath(join35(dir, "review.json")));
14320
14423
  const parsed = text6.ok ? parseJsonObject(text6.value) : void 0;
14321
14424
  if (parsed?.ok !== true) return void 0;
14322
14425
  const statuses = (lang) => {
@@ -14356,6 +14459,178 @@ var i18nCheckCommand = {
14356
14459
  }
14357
14460
  };
14358
14461
 
14462
+ // src/history/look-back.ts
14463
+ import { join as join36 } from "node:path";
14464
+
14465
+ // src/history/patterns.ts
14466
+ import { posix as posix4 } from "node:path";
14467
+ var PATTERNS = ["done_after_failing_tests", "env_read", "no_verify", "force_push", "test_skip"];
14468
+ 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)/;
14469
+ 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;
14470
+ var ENV_FILE2 = /(^|[\s"'=/])\.env(\.[A-Za-z0-9_-]+)?(?=$|[\s"';|&)])/;
14471
+ var ENV_TEMPLATE = /\.env\.(example|sample|template|dist|defaults)\b/;
14472
+ var READ_COMMAND = /(^|[\s;&|(])(cat|less|more|head|tail|bat|type|Get-Content|source|\.)\s/;
14473
+ var PUSH = /(^|[\s;&|(])git\s+push\b([^;&|\n]*)/;
14474
+ var FORCE = /\s(--force(-with-lease)?(=\S*)?|-f|-[A-Za-z]*f[A-Za-z]*)(?=\s|$)|\s\+\S/;
14475
+ var NO_VERIFY = /(^|[\s;&|(])git\s+(commit|push|merge|rebase|am|cherry-pick)\b[^;&|\n]*\s--no-verify\b/;
14476
+ var TEST_FILE = /(\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)__tests__\/|_test\.(go|py)$|(^|\/)test_[^/]+\.py$)/;
14477
+ var SKIP = /\b(it|test|describe)\.skip\s*\(|\bx(it|describe|test)\s*\(|@pytest\.mark\.skip|\bt\.Skip\(|\.only\s*\(/;
14478
+ var commandOf = (tool) => {
14479
+ if (tool.name !== "Bash" && tool.name !== "PowerShell") return void 0;
14480
+ const command = tool.input["command"];
14481
+ return typeof command === "string" ? command : void 0;
14482
+ };
14483
+ var stringField = (tool, name) => {
14484
+ const value = tool.input[name];
14485
+ return typeof value === "string" ? value : "";
14486
+ };
14487
+ var isEnvPath = (path) => {
14488
+ const name = posix4.basename(path.replace(/\\/g, "/"));
14489
+ return /^\.env(\.[A-Za-z0-9_-]+)?$/.test(name) && !ENV_TEMPLATE.test(name);
14490
+ };
14491
+ var envRead = (tool) => {
14492
+ if (tool.name === "Read") return isEnvPath(stringField(tool, "file_path"));
14493
+ const command = commandOf(tool);
14494
+ if (command === void 0 || !READ_COMMAND.test(command)) return false;
14495
+ const match = ENV_FILE2.exec(command);
14496
+ return match !== null && !ENV_TEMPLATE.test(command.slice(match.index, match.index + match[0].length + 12));
14497
+ };
14498
+ var forcePush = (command) => {
14499
+ const match = PUSH.exec(command);
14500
+ return match !== null && FORCE.test(match[2] ?? "");
14501
+ };
14502
+ var addedSkip = (tool) => {
14503
+ const path = stringField(tool, "file_path");
14504
+ if (!TEST_FILE.test(path.replace(/\\/g, "/"))) return false;
14505
+ if (tool.name === "Write") return SKIP.test(stringField(tool, "content"));
14506
+ if (tool.name === "Edit")
14507
+ return SKIP.test(stringField(tool, "new_string")) && !SKIP.test(stringField(tool, "old_string"));
14508
+ if (tool.name !== "MultiEdit") return false;
14509
+ const edits = tool.input["edits"];
14510
+ return Array.isArray(edits) && edits.some((edit) => {
14511
+ if (typeof edit !== "object" || edit === null) return false;
14512
+ const next2 = "new_string" in edit && typeof edit.new_string === "string" ? edit.new_string : "";
14513
+ const before = "old_string" in edit && typeof edit.old_string === "string" ? edit.old_string : "";
14514
+ return SKIP.test(next2) && !SKIP.test(before);
14515
+ });
14516
+ };
14517
+ var doneAfterFailure = (turn) => {
14518
+ const runs = turn.tools.filter((tool) => TEST_COMMAND.test(commandOf(tool) ?? ""));
14519
+ const last = runs.at(-1);
14520
+ if (last === void 0 || turn.results.get(last.id)?.failed !== true) return void 0;
14521
+ return DONE_CLAIM.test(turn.finalText) ? last : void 0;
14522
+ };
14523
+ var sampleOf = (tool) => (commandOf(tool) ?? stringField(tool, "file_path")).split("\n")[0]?.slice(0, 120) ?? "";
14524
+ var hitsIn = (turn) => {
14525
+ const hits = [];
14526
+ const done = doneAfterFailure(turn);
14527
+ if (done !== void 0) hits.push({ pattern: "done_after_failing_tests", sample: sampleOf(done) });
14528
+ for (const tool of turn.tools) {
14529
+ const command = commandOf(tool);
14530
+ if (envRead(tool)) hits.push({ pattern: "env_read", sample: sampleOf(tool) });
14531
+ if (command !== void 0 && NO_VERIFY.test(command)) hits.push({ pattern: "no_verify", sample: sampleOf(tool) });
14532
+ if (command !== void 0 && forcePush(command)) hits.push({ pattern: "force_push", sample: sampleOf(tool) });
14533
+ if (addedSkip(tool)) hits.push({ pattern: "test_skip", sample: sampleOf(tool) });
14534
+ }
14535
+ return hits;
14536
+ };
14537
+
14538
+ // src/history/transcript.ts
14539
+ var blocksOf = (message) => {
14540
+ if (!isRecord(message)) return [];
14541
+ const content = message["content"];
14542
+ if (typeof content === "string") return [{ type: "text", text: content }];
14543
+ return Array.isArray(content) ? content : [];
14544
+ };
14545
+ var textOf3 = (value) => {
14546
+ if (typeof value === "string") return value;
14547
+ if (Array.isArray(value)) return value.map(textOf3).join("\n");
14548
+ return isRecord(value) && typeof value["text"] === "string" ? value["text"] : "";
14549
+ };
14550
+ var FAILED_EXIT = /^Exit code [1-9]\d*/m;
14551
+ var startTurn = (prompt) => ({ prompt, tools: [], results: /* @__PURE__ */ new Map(), finalText: "" });
14552
+ var addAssistant = (draft, blocks) => {
14553
+ for (const block4 of blocks) {
14554
+ if (!isRecord(block4)) continue;
14555
+ if (block4["type"] === "text" && typeof block4["text"] === "string" && block4["text"].trim() !== "") {
14556
+ draft.finalText = block4["text"];
14557
+ }
14558
+ const { id, name, input } = block4;
14559
+ if (block4["type"] === "tool_use" && typeof id === "string" && typeof name === "string" && isRecord(input)) {
14560
+ draft.tools.push({ id, name, input });
14561
+ }
14562
+ }
14563
+ };
14564
+ var addResults = (draft, blocks) => {
14565
+ let any = false;
14566
+ for (const block4 of blocks) {
14567
+ if (!isRecord(block4) || block4["type"] !== "tool_result" || typeof block4["tool_use_id"] !== "string") continue;
14568
+ const text6 = textOf3(block4["content"]);
14569
+ draft.results.set(block4["tool_use_id"], { failed: block4["is_error"] === true || FAILED_EXIT.test(text6), text: text6 });
14570
+ any = true;
14571
+ }
14572
+ return any;
14573
+ };
14574
+ var parseTranscript = (jsonl) => {
14575
+ const turns = [];
14576
+ let current;
14577
+ for (const line2 of jsonl.split(/\r?\n/)) {
14578
+ if (line2.trim() === "") continue;
14579
+ const parsed = parseJsonObject(line2);
14580
+ if (!parsed.ok) continue;
14581
+ const entry2 = parsed.value;
14582
+ const blocks = blocksOf(entry2["message"]);
14583
+ if (entry2["type"] === "user") {
14584
+ if (current !== void 0 && addResults(current, blocks)) continue;
14585
+ const prompt = blocks.map(textOf3).join("\n").trim();
14586
+ if (prompt === "") continue;
14587
+ current = startTurn(prompt);
14588
+ turns.push(current);
14589
+ } else if (entry2["type"] === "assistant" && current !== void 0) {
14590
+ addAssistant(current, blocks);
14591
+ }
14592
+ }
14593
+ return turns;
14594
+ };
14595
+
14596
+ // src/history/look-back.ts
14597
+ var SAMPLES = 3;
14598
+ var projectLogKey = (root) => root.replace(/[^A-Za-z0-9-]/g, "-");
14599
+ var projectLogDir = (runtime, root, env) => join36(env["CLAUDE_CONFIG_DIR"] ?? join36(runtime.homeDir(), ".claude"), "projects", projectLogKey(root));
14600
+ var lookBack = (runtime, root, env) => {
14601
+ const dir = projectLogDir(runtime, root, env);
14602
+ const listed4 = runtime.listDir(toAbsolutePath(dir));
14603
+ const files = listed4.ok ? listed4.value.filter((name) => name.endsWith(".jsonl")).sort() : [];
14604
+ const counts = {
14605
+ done_after_failing_tests: 0,
14606
+ env_read: 0,
14607
+ no_verify: 0,
14608
+ force_push: 0,
14609
+ test_skip: 0
14610
+ };
14611
+ const samples = {
14612
+ done_after_failing_tests: [],
14613
+ env_read: [],
14614
+ no_verify: [],
14615
+ force_push: [],
14616
+ test_skip: []
14617
+ };
14618
+ let turns = 0;
14619
+ for (const name of files) {
14620
+ const text6 = runtime.readText(toAbsolutePath(join36(dir, name)));
14621
+ if (!text6.ok) continue;
14622
+ for (const turn of parseTranscript(text6.value)) {
14623
+ turns += 1;
14624
+ for (const hit3 of hitsIn(turn)) {
14625
+ counts[hit3.pattern] += 1;
14626
+ const list4 = samples[hit3.pattern];
14627
+ if (list4.length < SAMPLES) list4.push(maskText(hit3.sample));
14628
+ }
14629
+ }
14630
+ }
14631
+ return { estimate: true, sessions: files.length, turns, counts, samples };
14632
+ };
14633
+
14359
14634
  // src/install/diff.ts
14360
14635
  var MAX_DIFF_LINES = 120;
14361
14636
  var table = (a, b) => {
@@ -14429,6 +14704,8 @@ var itemWarning = (t, item) => {
14429
14704
  var planWarnings = (t, plan) => [
14430
14705
  ...plan.detection.gitRoot ? [] : [{ code: "not_git", message: t("install.warn.not_git") }],
14431
14706
  ...plan.detection.claude.failClosed ? [] : [{ code: "fail_open", message: t("install.warn.fail_open", { version: plan.detection.claude.version ?? "?" }) }],
14707
+ ...plan.detection.location.wslMount ? [{ code: "wsl_mount", message: t("install.warn.wsl_mount", { path: plan.detection.root }) }] : [],
14708
+ ...plan.detection.location.synced ? [{ code: "synced_folder", message: t("install.warn.synced_folder", { path: plan.detection.root }) }] : [],
14432
14709
  ...plan.detection.authorities.map((authority) => authorityWarning(t, authority)),
14433
14710
  ...plan.items.flatMap((item) => itemWarning(t, item))
14434
14711
  ];
@@ -14508,17 +14785,32 @@ var initRequest = (args) => ({
14508
14785
  marketplace: args.values["marketplace"] === true,
14509
14786
  showDiff: false
14510
14787
  });
14788
+ var withLookBack = (ctx, result) => {
14789
+ if (!result.ok) return result;
14790
+ const root = openProject(ctx).project.root;
14791
+ const found = lookBack(ctx.runtime, root, ctx.env);
14792
+ const lines3 = [
14793
+ ctx.t("history.title", { sessions: found.sessions, turns: found.turns }),
14794
+ ...PATTERNS.map((pattern) => ` ${ctx.t(`history.pattern.${pattern}`, { count: found.counts[pattern] })}`),
14795
+ ctx.t("history.estimate")
14796
+ ];
14797
+ return { ...result, data: { ...result.data, lookBack: found }, human: [...result.human ?? [], ...lines3] };
14798
+ };
14511
14799
  var initCommand = {
14512
14800
  path: ["init"],
14513
14801
  summaryKey: "command.init.summary",
14514
- usage: "finitemonkey init [--preset neutral|gabriel] [--bin <path>] [--marketplace] [--dry-run] [--yes]",
14802
+ usage: "finitemonkey init [--preset neutral|gabriel] [--bin <path>] [--marketplace] [--look-back] [--dry-run] [--yes]",
14515
14803
  status: "ready",
14516
14804
  options: {
14517
14805
  preset: { type: "string" },
14518
14806
  bin: { type: "string" },
14519
- marketplace: { type: "boolean" }
14807
+ marketplace: { type: "boolean" },
14808
+ "look-back": { type: "boolean" }
14520
14809
  },
14521
- run: (ctx, args) => Promise.resolve(runInstall(ctx, initRequest(args), "neutral"))
14810
+ run: (ctx, args) => {
14811
+ const result = runInstall(ctx, initRequest(args), "neutral");
14812
+ return Promise.resolve(args.values["look-back"] === true ? withLookBack(ctx, result) : result);
14813
+ }
14522
14814
  };
14523
14815
 
14524
14816
  // src/commands/prose-check.ts
@@ -14996,7 +15288,7 @@ var secretsScanCommand = {
14996
15288
  };
14997
15289
 
14998
15290
  // src/task/create.ts
14999
- import { join as join35 } from "node:path";
15291
+ import { join as join37 } from "node:path";
15000
15292
 
15001
15293
  // src/core/yaml/write.ts
15002
15294
  var PLAIN_SAFE = /^[A-Za-z_./][A-Za-z0-9_./ ()-]*$/;
@@ -15080,10 +15372,10 @@ var explicitChecks = (draft) => [
15080
15372
  var draftChecks = (runtime, paths, draft) => {
15081
15373
  const disk = {
15082
15374
  read: (relative9) => {
15083
- const text6 = runtime.readText(toAbsolutePath(join35(paths.root, relative9)));
15375
+ const text6 = runtime.readText(toAbsolutePath(join37(paths.root, relative9)));
15084
15376
  return text6.ok ? text6.value : void 0;
15085
15377
  },
15086
- exists: (relative9) => runtime.stat(toAbsolutePath(join35(paths.root, relative9))) !== void 0
15378
+ exists: (relative9) => runtime.stat(toAbsolutePath(join37(paths.root, relative9))) !== void 0
15087
15379
  };
15088
15380
  const detected = detectChecks(disk);
15089
15381
  const base = draft.runs.length > 0 || draft.tests !== void 0 ? explicitChecks(draft) : detected;
@@ -15487,7 +15779,7 @@ var REGISTRY = [
15487
15779
  ];
15488
15780
 
15489
15781
  // src/i18n/lang-settings.ts
15490
- import { join as join36 } from "node:path";
15782
+ import { join as join38 } from "node:path";
15491
15783
  var read3 = (runtime, path) => {
15492
15784
  const text6 = runtime.readText(toAbsolutePath(path));
15493
15785
  return text6.ok ? text6.value : void 0;
@@ -15502,14 +15794,14 @@ var languageIn = (text6) => {
15502
15794
  const value = parsed.ok && isRecord(parsed.value) ? parsed.value["language"] : void 0;
15503
15795
  return typeof value === "string" && value.trim() !== "" ? value : void 0;
15504
15796
  };
15505
- var claudeDir = (runtime, env) => env["CLAUDE_CONFIG_DIR"] ?? join36(runtime.homeDir(), ".claude");
15797
+ var claudeDir = (runtime, env) => env["CLAUDE_CONFIG_DIR"] ?? join38(runtime.homeDir(), ".claude");
15506
15798
  var langSettings = (runtime, cwd, env, platform) => {
15507
15799
  const root = findGitRoot(cwd, runtime) ?? cwd;
15508
- const config = parseConfigYaml(read3(runtime, join36(root, CONFIG_FILE)) ?? "");
15800
+ const config = parseConfigYaml(read3(runtime, join38(root, CONFIG_FILE)) ?? "");
15509
15801
  const claudeFiles = [
15510
- join36(root, ".claude", "settings.local.json"),
15511
- join36(root, ".claude", "settings.json"),
15512
- join36(claudeDir(runtime, env), "settings.json")
15802
+ join38(root, ".claude", "settings.local.json"),
15803
+ join38(root, ".claude", "settings.json"),
15804
+ join38(claudeDir(runtime, env), "settings.json")
15513
15805
  ];
15514
15806
  return {
15515
15807
  config: configText(config, "lang"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "finitemonkey",
3
- "version": "0.1.0-next.11",
3
+ "version": "0.1.0-next.13",
4
4
  "description": "Claude Code hooks that stop a coding agent from saying done before the tests pass: a machine-checked definition of done, plus guardrails for secrets, git safety and production. Pre-release.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -55,7 +55,7 @@
55
55
  "i18n:check": "node dist/finitemonkey.mjs i18n check --dir src/i18n",
56
56
  "bench:startup": "node scripts/bench-startup.mjs",
57
57
  "smoke:install": "node scripts/smoke-install.mjs",
58
- "check": "npm run typecheck && npm run lint && npm run format:check && npm run check:dist && npm test && npm run i18n:check && npm run plugin:validate && npm pack --dry-run && npm run llms:check && npm run schema:check",
58
+ "check": "npm run typecheck && npm run lint && npm run format:check && npm run check:dist && npm test && npm run i18n:check && npm run plugin:validate && npm pack --dry-run && npm run llms:check && npm run schema:check && npm run docs:check",
59
59
  "prepublishOnly": "npm run typecheck && npm run check:dist && npm test",
60
60
  "prepare": "husky",
61
61
  "gif": "tsc -p assets/scripts && node --experimental-strip-types assets/scripts/render.ts",
@@ -66,6 +66,8 @@
66
66
  "schema": "tsx scripts/build-schemas.ts",
67
67
  "schema:check": "tsx scripts/build-schemas.ts --check",
68
68
  "budgets": "tsx scripts/budgets.ts",
69
+ "docs": "tsx scripts/build-docs.ts",
70
+ "docs:check": "tsc -p scripts && tsx scripts/build-docs.ts --check",
69
71
  "plugin:sync": "node scripts/plugin-sync.mjs",
70
72
  "plugin:validate": "node scripts/plugin-validate.mjs",
71
73
  "version": "node scripts/plugin-sync.mjs"