finitemonkey 0.1.0-next.22 → 0.1.0-next.23

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
@@ -145,6 +145,7 @@ Generado desde `src/core/footprint.ts`, la lista que los tests exigen al código
145
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
146
  | Escribe | `.finitemonkey/reports/` | Informes de error enmascarados que escribe `doctor --report`, solo cuando se pide. |
147
147
  | Escribe | `.finitemonkey/look/` | Capturas de pantalla, la hoja de contactos y el resumen JSON que escribe `look` por hash del árbol. |
148
+ | Escribe | `.finitemonkey/review/`, `.claude/agents/verifier.md` | El conteo de rondas de revisión que usa `review packet` y, solo cuando `review.playwright: true` está en la configuración, un agente verificador con el MCP de Playwright. |
148
149
  | 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. |
149
150
  | Envía | `127.0.0.1` | Una petición a tu propia app en el puerto que declara la comprobación `runtime`, después de arrancarla o encontrarla en marcha. Nada sale de esta máquina. |
150
151
  | Envía | `api.github.com`, `api.stripe.com`, `api.openai.com`, `api.anthropic.com`, `api.digitalocean.com`, `api.cloudflare.com`, `api.asaas.com` | A través de `curl`, solo cuando tú ejecutas `finitemonkey secrets live <proveedor> --env <VARIABLE> --send`: una petición con esa clave a la API del proveedor para ver si aún funciona. Apagado por defecto, nunca lo ejecuta un hook, y el host se muestra antes de enviar nada. |
package/README.md CHANGED
@@ -145,6 +145,7 @@ Generated from `src/core/footprint.ts`, the list the tests hold the code to. No
145
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
146
  | Writes | `.finitemonkey/reports/` | Masked bug reports that `doctor --report` writes, only when asked. |
147
147
  | Writes | `.finitemonkey/look/` | Screen captures, the contact sheet and the JSON summary that `look` writes per tree hash. |
148
+ | Writes | `.finitemonkey/review/`, `.claude/agents/verifier.md` | The review round count used by `review packet`, and, only when `review.playwright: true` is set in the config, a verifier agent with the Playwright MCP. |
148
149
  | 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. |
149
150
  | Sends | `127.0.0.1` | One request to your own app on the port a `runtime` check declares, after starting it or finding it running. Nothing goes past this machine. |
150
151
  | Sends | `api.github.com`, `api.stripe.com`, `api.openai.com`, `api.anthropic.com`, `api.digitalocean.com`, `api.cloudflare.com`, `api.asaas.com` | Through `curl`, only when you run `finitemonkey secrets live <provider> --env <VARIABLE> --send` yourself: one request with that key to the provider's API to see whether it still works. Off by default, never run by a hook, and the host is printed before anything is sent. |
package/README.pt-BR.md CHANGED
@@ -145,6 +145,7 @@ Gerado a partir de `src/core/footprint.ts`, a lista que os testes cobram do cód
145
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
146
  | Grava | `.finitemonkey/reports/` | Relatórios de bug mascarados que o `doctor --report` grava, só quando pedido. |
147
147
  | Grava | `.finitemonkey/look/` | Capturas de tela, a folha de contato e o resumo JSON que o `look` grava por hash da árvore. |
148
+ | Grava | `.finitemonkey/review/`, `.claude/agents/verifier.md` | A contagem de rodadas de revisão usada pelo `review packet` e, só quando `review.playwright: true` está na configuração, um agente verificador com o MCP do Playwright. |
148
149
  | 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. |
149
150
  | Envia | `127.0.0.1` | Uma requisição ao seu próprio app na porta que a checagem `runtime` declara, depois de subir o app ou achá-lo rodando. Nada sai desta máquina. |
150
151
  | Envia | `api.github.com`, `api.stripe.com`, `api.openai.com`, `api.anthropic.com`, `api.digitalocean.com`, `api.cloudflare.com`, `api.asaas.com` | Pelo `curl`, só quando você roda `finitemonkey secrets live <provedor> --env <VARIAVEL> --send`: uma requisição com aquela chave para a API do provedor para ver se ela ainda funciona. Desligado por padrão, nunca roda por hook, e o host aparece antes de qualquer envio. |
@@ -870,6 +870,92 @@ var parseLock = (text7) => {
870
870
  var renderLock = (lock) => `${JSON.stringify(lock, null, 2)}
871
871
  `;
872
872
 
873
+ // src/git/project-config.ts
874
+ import { join as join2 } from "node:path";
875
+ var CONFIG_FILE2 = ".finitemonkey/config.yml";
876
+ var unquote = (value) => value.trim().replace(/^(["'])(.*)\1$/s, "$2");
877
+ var stripComment = (line2) => {
878
+ let quote;
879
+ for (let index = 0; index < line2.length; index += 1) {
880
+ const char = line2.charAt(index);
881
+ if (quote === void 0 && (char === '"' || char === "'")) quote = char;
882
+ else if (char === quote) quote = void 0;
883
+ else if (quote === void 0 && char === "#" && (index === 0 || /\s/.test(line2.charAt(index - 1)))) {
884
+ return line2.slice(0, index);
885
+ }
886
+ }
887
+ return line2;
888
+ };
889
+ var flowList = (value) => {
890
+ const trimmed = value.trim();
891
+ if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return void 0;
892
+ return trimmed.slice(1, -1).split(",").map(unquote).filter((item2) => item2 !== "");
893
+ };
894
+ var keyValue = (body2) => {
895
+ const match = /^("[^"]*"|'[^']*'|[^:]+?)\s*:(?:\s+(.*))?$/.exec(body2);
896
+ if (match === null) return void 0;
897
+ return { key: unquote(match[1] ?? ""), value: match[2] ?? "" };
898
+ };
899
+ var addItem = (frame, item2) => {
900
+ if (frame.key === void 0) return;
901
+ const parent = frame.tree[frame.key];
902
+ if (Array.isArray(parent)) parent.push(item2);
903
+ else frame.tree[frame.key] = [item2];
904
+ };
905
+ var placeLine = (stack, indent, body2) => {
906
+ while (stack.length > 1 && (stack[stack.length - 1]?.indent ?? 0) >= indent && !body2.startsWith("- ")) stack.pop();
907
+ const top = stack[stack.length - 1];
908
+ if (top === void 0) return;
909
+ if (body2.startsWith("- ")) {
910
+ addItem(top, unquote(body2.slice(2)));
911
+ return;
912
+ }
913
+ const pair = keyValue(body2);
914
+ if (pair === void 0) return;
915
+ const parentTree = top.key === void 0 ? top.tree : ensureTree(top.tree, top.key);
916
+ if (pair.value === "") {
917
+ stack.push({ indent, tree: parentTree, key: pair.key });
918
+ return;
919
+ }
920
+ parentTree[pair.key] = flowList(pair.value) ?? unquote(pair.value);
921
+ };
922
+ var ensureTree = (tree, key) => {
923
+ const existing = tree[key];
924
+ if (existing !== void 0 && typeof existing !== "string" && !Array.isArray(existing)) return existing;
925
+ const fresh3 = {};
926
+ tree[key] = fresh3;
927
+ return fresh3;
928
+ };
929
+ var parseConfigYaml = (text7) => {
930
+ const root = {};
931
+ const stack = [{ indent: -1, tree: root, key: void 0 }];
932
+ for (const raw of text7.split(/\r?\n/)) {
933
+ const line2 = stripComment(raw).trimEnd();
934
+ if (line2.trim() === "" || line2.trim() === "---") continue;
935
+ const indent = line2.length - line2.trimStart().length;
936
+ placeLine(stack, indent, line2.trim());
937
+ }
938
+ return root;
939
+ };
940
+ var isTree = (node) => typeof node === "object" && !Array.isArray(node);
941
+ var configAt = (tree, ...path) => path.reduce((node, key) => isTree(node) ? node[key] : void 0, tree);
942
+ var configList = (tree, ...path) => {
943
+ const node = configAt(tree, ...path);
944
+ if (typeof node === "string") return node === "" ? [] : [node];
945
+ return Array.isArray(node) ? node.filter((item2) => typeof item2 === "string") : [];
946
+ };
947
+ var configMap = (tree, ...path) => {
948
+ const node = configAt(tree, ...path);
949
+ if (!isTree(node)) return {};
950
+ return Object.fromEntries(
951
+ Object.entries(node).filter((entry2) => typeof entry2[1] === "string")
952
+ );
953
+ };
954
+ var loadProjectConfig = (runtime, root) => {
955
+ const text7 = runtime.readText(toAbsolutePath(join2(root, CONFIG_FILE2)));
956
+ return text7.ok ? parseConfigYaml(text7.value) : {};
957
+ };
958
+
873
959
  // src/install/managed-block.ts
874
960
  var lineBreak = (text7) => text7.includes("\r\n") ? "\r\n" : "\n";
875
961
  var blockRange = (lines3, markers) => {
@@ -1145,94 +1231,6 @@ var gitHookItems = (project, runner) => {
1145
1231
 
1146
1232
  // src/policy/policy.ts
1147
1233
  import { join as join3 } from "node:path";
1148
-
1149
- // src/git/project-config.ts
1150
- import { join as join2 } from "node:path";
1151
- var CONFIG_FILE2 = ".finitemonkey/config.yml";
1152
- var unquote = (value) => value.trim().replace(/^(["'])(.*)\1$/s, "$2");
1153
- var stripComment = (line2) => {
1154
- let quote;
1155
- for (let index = 0; index < line2.length; index += 1) {
1156
- const char = line2.charAt(index);
1157
- if (quote === void 0 && (char === '"' || char === "'")) quote = char;
1158
- else if (char === quote) quote = void 0;
1159
- else if (quote === void 0 && char === "#" && (index === 0 || /\s/.test(line2.charAt(index - 1)))) {
1160
- return line2.slice(0, index);
1161
- }
1162
- }
1163
- return line2;
1164
- };
1165
- var flowList = (value) => {
1166
- const trimmed = value.trim();
1167
- if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return void 0;
1168
- return trimmed.slice(1, -1).split(",").map(unquote).filter((item2) => item2 !== "");
1169
- };
1170
- var keyValue = (body2) => {
1171
- const match = /^("[^"]*"|'[^']*'|[^:]+?)\s*:(?:\s+(.*))?$/.exec(body2);
1172
- if (match === null) return void 0;
1173
- return { key: unquote(match[1] ?? ""), value: match[2] ?? "" };
1174
- };
1175
- var addItem = (frame, item2) => {
1176
- if (frame.key === void 0) return;
1177
- const parent = frame.tree[frame.key];
1178
- if (Array.isArray(parent)) parent.push(item2);
1179
- else frame.tree[frame.key] = [item2];
1180
- };
1181
- var placeLine = (stack, indent, body2) => {
1182
- while (stack.length > 1 && (stack[stack.length - 1]?.indent ?? 0) >= indent && !body2.startsWith("- ")) stack.pop();
1183
- const top = stack[stack.length - 1];
1184
- if (top === void 0) return;
1185
- if (body2.startsWith("- ")) {
1186
- addItem(top, unquote(body2.slice(2)));
1187
- return;
1188
- }
1189
- const pair = keyValue(body2);
1190
- if (pair === void 0) return;
1191
- const parentTree = top.key === void 0 ? top.tree : ensureTree(top.tree, top.key);
1192
- if (pair.value === "") {
1193
- stack.push({ indent, tree: parentTree, key: pair.key });
1194
- return;
1195
- }
1196
- parentTree[pair.key] = flowList(pair.value) ?? unquote(pair.value);
1197
- };
1198
- var ensureTree = (tree, key) => {
1199
- const existing = tree[key];
1200
- if (existing !== void 0 && typeof existing !== "string" && !Array.isArray(existing)) return existing;
1201
- const fresh3 = {};
1202
- tree[key] = fresh3;
1203
- return fresh3;
1204
- };
1205
- var parseConfigYaml = (text7) => {
1206
- const root = {};
1207
- const stack = [{ indent: -1, tree: root, key: void 0 }];
1208
- for (const raw of text7.split(/\r?\n/)) {
1209
- const line2 = stripComment(raw).trimEnd();
1210
- if (line2.trim() === "" || line2.trim() === "---") continue;
1211
- const indent = line2.length - line2.trimStart().length;
1212
- placeLine(stack, indent, line2.trim());
1213
- }
1214
- return root;
1215
- };
1216
- var isTree = (node) => typeof node === "object" && !Array.isArray(node);
1217
- var configAt = (tree, ...path) => path.reduce((node, key) => isTree(node) ? node[key] : void 0, tree);
1218
- var configList = (tree, ...path) => {
1219
- const node = configAt(tree, ...path);
1220
- if (typeof node === "string") return node === "" ? [] : [node];
1221
- return Array.isArray(node) ? node.filter((item2) => typeof item2 === "string") : [];
1222
- };
1223
- var configMap = (tree, ...path) => {
1224
- const node = configAt(tree, ...path);
1225
- if (!isTree(node)) return {};
1226
- return Object.fromEntries(
1227
- Object.entries(node).filter((entry2) => typeof entry2[1] === "string")
1228
- );
1229
- };
1230
- var loadProjectConfig = (runtime, root) => {
1231
- const text7 = runtime.readText(toAbsolutePath(join2(root, CONFIG_FILE2)));
1232
- return text7.ok ? parseConfigYaml(text7.value) : {};
1233
- };
1234
-
1235
- // src/policy/policy.ts
1236
1234
  var POLICY_FILE = ".finitemonkey/policy.yml";
1237
1235
  var GUARD_MODES = ["off", "shadow", "active"];
1238
1236
  var LEVELS = ["light", "standard", "strict"];
@@ -1271,6 +1269,65 @@ var loadPolicy = (runtime, root) => {
1271
1269
  return text7.ok ? parsePolicy(text7.value) : DEFAULT_POLICY;
1272
1270
  };
1273
1271
 
1272
+ // src/review/agents.ts
1273
+ var REVIEW_AGENTS = ["reviewer", "verifier"];
1274
+ var FORBIDDEN_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit", "Agent", "Task"];
1275
+ var reviewAgentOf = (agentType) => REVIEW_AGENTS.find((name) => agentType === name || agentType.endsWith(`:${name}`));
1276
+ var REVIEWER_BODY = [
1277
+ "# Reviewer",
1278
+ "",
1279
+ "You review a change you did not write. Your input is the review packet in your prompt: the diff, the task contract, the evidence ledger and, when present, the code owners' REVIEW.md. You never see the conversation that produced the change, and you must not ask for it.",
1280
+ "",
1281
+ "1. Read the diff first, then open the files it touches when you need context.",
1282
+ "2. Judge the change against the contract: does each acceptance check have evidence for the current tree?",
1283
+ "3. Write numbered findings in three groups: must fix, should fix, nice to have. For each: what it is, the problem, the fix, and what happens if it is not fixed.",
1284
+ "4. Every must-fix finding carries a reproduction command and the expected result. A finding without one goes under should fix.",
1285
+ "5. End with a verdict: approve, approve with changes, or request changes.",
1286
+ "",
1287
+ "You cannot edit files or start other agents. Report; do not repair."
1288
+ ].join("\n");
1289
+ var VERIFIER_BODY = [
1290
+ "# Verifier",
1291
+ "",
1292
+ "You check claims with commands, not with opinions. Your input is the review packet and, when there is one, the reviewer's must-fix findings with their reproduction commands. You never see the conversation that produced the change.",
1293
+ "",
1294
+ "1. Run each reproduction command in a throwaway worktree on the base commit and on the head commit (`git worktree add <tmp> <rev>`), never in the person's working tree.",
1295
+ "2. Record both exit codes. Fails only on head: the finding stands. Fails on both: it was already there. Fails on neither: not reproduced.",
1296
+ "3. Report one line per finding with the command and both results, then remove the worktrees.",
1297
+ "",
1298
+ "You cannot edit files or start other agents. Report; do not repair."
1299
+ ].join("\n");
1300
+ var VERIFIER = {
1301
+ name: "verifier",
1302
+ description: "Runs reproduction commands from a review on the base and head commits in throwaway worktrees and reports which findings are real. Read-only.",
1303
+ tools: ["Read", "Grep", "Glob", "Bash"],
1304
+ model: "sonnet",
1305
+ maxTurns: 60,
1306
+ body: VERIFIER_BODY
1307
+ };
1308
+ var PLAYWRIGHT_SERVER = "playwright";
1309
+ var withPlaywright = (spec2) => ({
1310
+ ...spec2,
1311
+ tools: [...spec2.tools, `mcp__${PLAYWRIGHT_SERVER}`],
1312
+ mcpServers: [PLAYWRIGHT_SERVER]
1313
+ });
1314
+ var agentMarkdown = (spec2, marker) => [
1315
+ "---",
1316
+ `name: ${spec2.name}`,
1317
+ `description: ${spec2.description}`,
1318
+ `tools: ${spec2.tools.join(", ")}`,
1319
+ `disallowedTools: ${FORBIDDEN_TOOLS.join(", ")}`,
1320
+ `model: ${spec2.model}`,
1321
+ "effort: high",
1322
+ `maxTurns: ${String(spec2.maxTurns)}`,
1323
+ ...spec2.mcpServers === void 0 ? [] : ["mcpServers:", ...spec2.mcpServers.map((name) => ` - ${name}`)],
1324
+ "---",
1325
+ "",
1326
+ ...marker === void 0 ? [] : [`<!-- ${marker} -->`, ""],
1327
+ spec2.body,
1328
+ ""
1329
+ ].join("\n");
1330
+
1274
1331
  // src/install/agents-block.ts
1275
1332
  var MARKDOWN_MARKERS = { begin: MD_BLOCK_BEGIN, end: MD_BLOCK_END };
1276
1333
  var IMPORT_LINE = /^@AGENTS\.md[ \t]*$/m;
@@ -1795,11 +1852,19 @@ var generatedItem = (project, file, previous) => {
1795
1852
  }
1796
1853
  return { ...base, action: actionFor(before, template, "update"), after: template };
1797
1854
  };
1798
- var generatedFiles = (input, runner, vendor) => {
1855
+ var PLAYWRIGHT_VERIFIER = ".claude/agents/verifier.md";
1856
+ var playwrightVerifier = (project) => {
1857
+ const wanted = configAt(parseConfigYaml(project.read(CONFIG_FILE) ?? ""), "review", "playwright") === "true";
1858
+ if (!wanted) return [];
1859
+ const template = agentMarkdown(withPlaywright(VERIFIER), GENERATED_MARKER);
1860
+ return [{ path: PLAYWRIGHT_VERIFIER, template, isOurs: (text7) => text7.includes(GENERATED_MARKER) }];
1861
+ };
1862
+ var generatedFiles = (project, input, runner, vendor) => {
1799
1863
  const own = [
1800
1864
  { path: CONFIG_FILE, template: configTemplate(input.preset, input.lang), isOurs: hasMarker },
1801
1865
  { path: VERIFY_SKILL, template: verifySkillTemplate(runner, input.lang), isOurs: isOwnSkill },
1802
- { path: POLICY_FILE, template: policyTemplate(input.lang), isOurs: hasMarker }
1866
+ { path: POLICY_FILE, template: policyTemplate(input.lang), isOurs: hasMarker },
1867
+ ...playwrightVerifier(project)
1803
1868
  ];
1804
1869
  if (!vendor) return accept(own);
1805
1870
  if (input.bundle === void 0) return reject({ kind: "bundle", detail: "the running finitemonkey is not a bundle" });
@@ -1810,7 +1875,7 @@ var buildPlan = (project, detection, input, previous, migrations = MIGRATIONS) =
1810
1875
  const settings = settingsItem(project, detection, input, entryFor(previous, SETTINGS_FILE));
1811
1876
  if (!settings.ok) return settings;
1812
1877
  const vendor = detection.runner.kind === "vendored" || entryFor(previous, VENDOR_BIN) !== void 0;
1813
- const files = generatedFiles(input, detection.runner, vendor);
1878
+ const files = generatedFiles(project, input, detection.runner, vendor);
1814
1879
  if (!files.ok) return files;
1815
1880
  const generated = files.value.map((file) => generatedItem(project, file, entryFor(previous, file.path)));
1816
1881
  return accept({
@@ -4080,6 +4145,15 @@ var bypassGuard = (input) => {
4080
4145
  );
4081
4146
  };
4082
4147
 
4148
+ // src/guards/review/hook.ts
4149
+ var FORBIDDEN = new Set(FORBIDDEN_TOOLS);
4150
+ var reviewAgentGuard = (input) => {
4151
+ const agent = reviewAgentOf(textField(input.payload.raw, "agent_type") ?? "");
4152
+ const tool = textField(input.payload.raw, "tool_name") ?? "";
4153
+ if (agent === void 0 || !FORBIDDEN.has(tool)) return { kind: "allow" };
4154
+ return { kind: "block", reason: input.t("review.guard.read_only", { agent, tool }) };
4155
+ };
4156
+
4083
4157
  // src/guards/self/protected.ts
4084
4158
  import { relative as relative2 } from "node:path";
4085
4159
  var normalize = (path) => toPosixPath(path).replace(/^\.\//, "");
@@ -5164,27 +5238,27 @@ var gitAt = (runtime, cwd) => {
5164
5238
  }
5165
5239
  };
5166
5240
  };
5167
- var gitRoot = (git) => {
5168
- const top = git.out(["rev-parse", "--show-toplevel"])?.trim();
5241
+ var gitRoot = (git2) => {
5242
+ const top = git2.out(["rev-parse", "--show-toplevel"])?.trim();
5169
5243
  return top === void 0 || top === "" ? void 0 : toAbsolutePath(top);
5170
5244
  };
5171
5245
  var nonEmpty = (value) => value === "" ? void 0 : value;
5172
- var headSha = (git) => git.out(["rev-parse", "--verify", "-q", "HEAD"])?.trim();
5173
- var currentBranch = (git) => {
5174
- const name = git.out(["rev-parse", "--abbrev-ref", "HEAD"])?.trim();
5246
+ var headSha = (git2) => git2.out(["rev-parse", "--verify", "-q", "HEAD"])?.trim();
5247
+ var currentBranch = (git2) => {
5248
+ const name = git2.out(["rev-parse", "--abbrev-ref", "HEAD"])?.trim();
5175
5249
  return name === void 0 || name === "HEAD" || name === "" ? void 0 : name;
5176
5250
  };
5177
- var defaultRemoteBranch = (git) => nonEmpty(git.out(["rev-parse", "--abbrev-ref", "origin/HEAD"])?.trim());
5178
- var resolveBase = (git, declared) => {
5179
- if (declared !== void 0 && git.out(["rev-parse", "--verify", "-q", `${declared}^{commit}`]) !== void 0) {
5251
+ var defaultRemoteBranch = (git2) => nonEmpty(git2.out(["rev-parse", "--abbrev-ref", "origin/HEAD"])?.trim());
5252
+ var resolveBase = (git2, declared) => {
5253
+ if (declared !== void 0 && git2.out(["rev-parse", "--verify", "-q", `${declared}^{commit}`]) !== void 0) {
5180
5254
  return declared;
5181
5255
  }
5182
- const remote = defaultRemoteBranch(git);
5183
- const merged2 = remote === void 0 ? void 0 : git.out(["merge-base", "HEAD", remote])?.trim();
5184
- return nonEmpty(merged2) ?? nonEmpty(headSha(git)) ?? EMPTY_TREE;
5256
+ const remote = defaultRemoteBranch(git2);
5257
+ const merged2 = remote === void 0 ? void 0 : git2.out(["merge-base", "HEAD", remote])?.trim();
5258
+ return nonEmpty(merged2) ?? nonEmpty(headSha(git2)) ?? EMPTY_TREE;
5185
5259
  };
5186
5260
  var nulFields = (text7) => text7.split("\0").filter((part2) => part2 !== "");
5187
- var untracked = (git) => nulFields(git.out(["ls-files", "--others", "--exclude-standard", "-z", "--", ".", EXCLUDE_FM]) ?? "");
5261
+ var untracked = (git2) => nulFields(git2.out(["ls-files", "--others", "--exclude-standard", "-z", "--", ".", EXCLUDE_FM]) ?? "");
5188
5262
  var parseNameStatus = (text7) => {
5189
5263
  const fields = nulFields(text7);
5190
5264
  const changes = [];
@@ -5204,21 +5278,21 @@ var parseNameStatus = (text7) => {
5204
5278
  }
5205
5279
  return changes;
5206
5280
  };
5207
- var changedFiles = (git, base) => {
5281
+ var changedFiles = (git2, base) => {
5208
5282
  const tracked = parseNameStatus(
5209
- git.out(["diff", "--name-status", "-M", "-z", "--no-color", base, "--", ".", EXCLUDE_FM]) ?? ""
5283
+ git2.out(["diff", "--name-status", "-M", "-z", "--no-color", base, "--", ".", EXCLUDE_FM]) ?? ""
5210
5284
  );
5211
5285
  const known2 = new Set(tracked.map((change) => change.path));
5212
- const fresh3 = untracked(git).filter((path) => !known2.has(path)).map((path) => ({ status: "A", path }));
5286
+ const fresh3 = untracked(git2).filter((path) => !known2.has(path)).map((path) => ({ status: "A", path }));
5213
5287
  return [...tracked, ...fresh3];
5214
5288
  };
5215
- var fileAt = (git, rev, path) => rev === EMPTY_TREE ? void 0 : git.out(["show", `${rev}:${path}`]);
5216
- var treeHash = (git) => {
5217
- const head = headSha(git) ?? EMPTY_TREE;
5218
- const diff = git.run(["diff", "HEAD", "--binary", "--no-ext-diff", "--no-color", "--", ".", EXCLUDE_FM]);
5289
+ var fileAt = (git2, rev, path) => rev === EMPTY_TREE ? void 0 : git2.out(["show", `${rev}:${path}`]);
5290
+ var treeHash = (git2) => {
5291
+ const head = headSha(git2) ?? EMPTY_TREE;
5292
+ const diff = git2.run(["diff", "HEAD", "--binary", "--no-ext-diff", "--no-color", "--", ".", EXCLUDE_FM]);
5219
5293
  if (!diff.found || diff.exitCode !== 0 && head !== EMPTY_TREE) return void 0;
5220
- const files = untracked(git);
5221
- const hashes = files.length === 0 ? "" : git.out(["hash-object", "--", ...files]) ?? "";
5294
+ const files = untracked(git2);
5295
+ const hashes = files.length === 0 ? "" : git2.out(["hash-object", "--", ...files]) ?? "";
5222
5296
  const digest = sha256Hex([head, diff.stdout, files.join("\0"), hashes].join("\n--fm--\n"));
5223
5297
  return `w${digest.slice(0, 16)}`;
5224
5298
  };
@@ -5237,18 +5311,18 @@ var parseAddedLines = (diff) => {
5237
5311
  }
5238
5312
  return added;
5239
5313
  };
5240
- var addedLines = (git, base) => parseAddedLines(git.out(["diff", "-U0", "--no-color", "--no-ext-diff", base, "--", ".", EXCLUDE_FM]) ?? "");
5241
- var numstat = (git, base) => {
5242
- const rows = (git.out(["diff", "--numstat", "--no-color", base, "--", ".", EXCLUDE_FM]) ?? "").split("\n").filter((row) => row.trim() !== "");
5314
+ var addedLines = (git2, base) => parseAddedLines(git2.out(["diff", "-U0", "--no-color", "--no-ext-diff", base, "--", ".", EXCLUDE_FM]) ?? "");
5315
+ var numstat = (git2, base) => {
5316
+ const rows = (git2.out(["diff", "--numstat", "--no-color", base, "--", ".", EXCLUDE_FM]) ?? "").split("\n").filter((row) => row.trim() !== "");
5243
5317
  const lines3 = rows.reduce((sum, row) => {
5244
5318
  const [plus = "0", minus = "0"] = row.split(" ");
5245
5319
  return sum + (Number(plus) || 0) + (Number(minus) || 0);
5246
5320
  }, 0);
5247
5321
  return { files: rows.length, lines: lines3 };
5248
5322
  };
5249
- var dirtyPaths = (git) => nulFields(git.out(["status", "--porcelain", "-z", "--", ".", EXCLUDE_FM]) ?? "").map((entry2) => entry2.slice(3));
5250
- var aheadOfUpstream = (git) => {
5251
- const count4 = git.out(["rev-list", "--count", "@{u}..HEAD"])?.trim();
5323
+ var dirtyPaths = (git2) => nulFields(git2.out(["status", "--porcelain", "-z", "--", ".", EXCLUDE_FM]) ?? "").map((entry2) => entry2.slice(3));
5324
+ var aheadOfUpstream = (git2) => {
5325
+ const count4 = git2.out(["rev-list", "--count", "@{u}..HEAD"])?.trim();
5252
5326
  return count4 === void 0 ? void 0 : Number(count4);
5253
5327
  };
5254
5328
 
@@ -5257,9 +5331,9 @@ var untrackedLines = (runtime, root, changes) => changes.filter((change) => chan
5257
5331
  const text7 = runtime.readText(toAbsolutePath(join14(root, change.path)));
5258
5332
  return sum + (text7.ok ? text7.value.split("\n").length : 0);
5259
5333
  }, 0);
5260
- var diffShape = (runtime, git, base, changes) => ({
5334
+ var diffShape = (runtime, git2, base, changes) => ({
5261
5335
  files: changes.map((change) => change.path),
5262
- lines: numstat(git, base).lines + untrackedLines(runtime, git.cwd, changes)
5336
+ lines: numstat(git2, base).lines + untrackedLines(runtime, git2.cwd, changes)
5263
5337
  });
5264
5338
 
5265
5339
  // src/gate/pr-parse.ts
@@ -5319,11 +5393,11 @@ var cached = (runtime, paths, branch, ttlMs) => {
5319
5393
  const view2 = raw["view"];
5320
5394
  return typeof view2 === "string" ? parsePrView(view2) : void 0;
5321
5395
  };
5322
- var fetchView = (runtime, git, branch) => {
5323
- const auth = ghAuthenticated(runtime, git.cwd);
5396
+ var fetchView = (runtime, git2, branch) => {
5397
+ const auth = ghAuthenticated(runtime, git2.cwd);
5324
5398
  if (auth === "timeout") return { kind: "network", detail: "gh auth status timed out" };
5325
5399
  if (auth === "no") return { kind: "no_gh" };
5326
- const view2 = gh(runtime, git.cwd, ["pr", "view", branch, "--json", PR_FIELDS]);
5400
+ const view2 = gh(runtime, git2.cwd, ["pr", "view", branch, "--json", PR_FIELDS]);
5327
5401
  if (view2.timedOut) return { kind: "network", detail: "gh pr view timed out" };
5328
5402
  if (view2.exitCode !== 0) {
5329
5403
  if (/no (open )?pull requests? found|no git remotes/i.test(view2.stderr)) return { kind: "none", branch };
@@ -5332,20 +5406,20 @@ var fetchView = (runtime, git, branch) => {
5332
5406
  const pr = parsePrView(view2.stdout);
5333
5407
  return pr === void 0 ? { kind: "network", detail: "gh returned unexpected JSON" } : { kind: "fetched", branch, pr, view: view2.stdout };
5334
5408
  };
5335
- var prStatus = (runtime, git, paths, ttlMs = PR_CACHE_TTL_MS) => {
5336
- const branch = currentBranch(git);
5409
+ var prStatus = (runtime, git2, paths, ttlMs = PR_CACHE_TTL_MS) => {
5410
+ const branch = currentBranch(git2);
5337
5411
  if (branch === void 0) return { kind: "no_branch" };
5338
5412
  const hit3 = cached(runtime, paths, branch, ttlMs);
5339
- if (hit3 !== void 0) return { kind: "found", branch, pr: hit3, localHead: headSha(git) };
5340
- const status = fetchView(runtime, git, branch);
5413
+ if (hit3 !== void 0) return { kind: "found", branch, pr: hit3, localHead: headSha(git2) };
5414
+ const status = fetchView(runtime, git2, branch);
5341
5415
  if (status.kind !== "fetched") return status;
5342
5416
  writeJsonFile(runtime, paths.cache("pr.json"), { fetchedAt: nowMs(runtime), branch, view: status.view });
5343
- return { kind: "found", branch, pr: status.pr, localHead: headSha(git) };
5417
+ return { kind: "found", branch, pr: status.pr, localHead: headSha(git2) };
5344
5418
  };
5345
- var conflictFiles = (git, base) => {
5419
+ var conflictFiles = (git2, base) => {
5346
5420
  for (const target2 of [`origin/${base}`, base]) {
5347
- if (git.out(["rev-parse", "--verify", "-q", `${target2}^{commit}`]) === void 0) continue;
5348
- const result = git.run(["merge-tree", "--write-tree", "--name-only", "--no-messages", target2, "HEAD"]);
5421
+ if (git2.out(["rev-parse", "--verify", "-q", `${target2}^{commit}`]) === void 0) continue;
5422
+ const result = git2.run(["merge-tree", "--write-tree", "--name-only", "--no-messages", target2, "HEAD"]);
5349
5423
  if (result.found && result.exitCode === 1) return parseMergeTreeConflicts(result.stdout);
5350
5424
  if (result.found && result.exitCode === 0) return [];
5351
5425
  }
@@ -5465,15 +5539,15 @@ var diskFiles = (runtime, root) => ({
5465
5539
  },
5466
5540
  exists: (relative11) => runtime.stat(toAbsolutePath(join15(root, relative11))) !== void 0
5467
5541
  });
5468
- var baseFiles = (git, base, disk) => ({
5469
- read: (relative11) => base === EMPTY_TREE ? disk.read(relative11) : fileAt(git, base, relative11),
5542
+ var baseFiles = (git2, base, disk) => ({
5543
+ read: (relative11) => base === EMPTY_TREE ? disk.read(relative11) : fileAt(git2, base, relative11),
5470
5544
  exists: disk.exists
5471
5545
  });
5472
- var loadContract = (options, paths, git, base) => {
5546
+ var loadContract = (options, paths, git2, base) => {
5473
5547
  const disk = diskFiles(options.runtime, paths.root);
5474
5548
  const id = activeTaskId(options.runtime, paths, options.taskId);
5475
- if (id === void 0) return accept(loadImplicit(options.runtime, paths, baseFiles(git, base, disk)));
5476
- return loadTask(options.runtime, paths, { disk: disk.read, base: (relative11) => fileAt(git, base, relative11) }, id);
5549
+ if (id === void 0) return accept(loadImplicit(options.runtime, paths, baseFiles(git2, base, disk)));
5550
+ return loadTask(options.runtime, paths, { disk: disk.read, base: (relative11) => fileAt(git2, base, relative11) }, id);
5477
5551
  };
5478
5552
  var memo = (make) => {
5479
5553
  let value;
@@ -5495,30 +5569,30 @@ var skipReason = (runtime, paths, loaded, changes, tree, sessionId) => {
5495
5569
  const session = readSession(runtime, paths);
5496
5570
  return session !== void 0 && session.sessionId === sessionId && session.startTree === tree ? "no_session_edits" : void 0;
5497
5571
  };
5498
- var measureSize = (runtime, git, base, changes, contract) => sizeVerdict(contract.size, diffShape(runtime, git, base, changes), sizeConfig(loadProjectConfig(runtime, git.cwd)));
5572
+ var measureSize = (runtime, git2, base, changes, contract) => sizeVerdict(contract.size, diffShape(runtime, git2, base, changes), sizeConfig(loadProjectConfig(runtime, git2.cwd)));
5499
5573
  var prepareGate = (options) => {
5500
5574
  const probe = gitAt(options.runtime, options.cwd);
5501
5575
  const root = gitRoot(probe);
5502
5576
  if (root === void 0) {
5503
5577
  return options.taskId === void 0 ? { kind: "skipped", reason: "no_repo" } : { kind: "failed", failure: { kind: "not_a_repo" } };
5504
5578
  }
5505
- const git = gitAt(options.runtime, root);
5579
+ const git2 = gitAt(options.runtime, root);
5506
5580
  const paths = fmPaths(root);
5507
5581
  const trust2 = readTrust(options.runtime, paths);
5508
- const firstBase = resolveBase(git, void 0);
5509
- const loaded = loadContract(options, paths, git, firstBase);
5582
+ const firstBase = resolveBase(git2, void 0);
5583
+ const loaded = loadContract(options, paths, git2, firstBase);
5510
5584
  if (!loaded.ok) return { kind: "failed", failure: { kind: "contract", problem: loaded.error } };
5511
- const base = resolveBase(git, loaded.value.contract.base);
5512
- const changes = changedFiles(git, base);
5513
- const tree = treeHash(git);
5585
+ const base = resolveBase(git2, loaded.value.contract.base);
5586
+ const changes = changedFiles(git2, base);
5587
+ const tree = treeHash(git2);
5514
5588
  if (tree === void 0) return { kind: "failed", failure: { kind: "no_tree_hash" } };
5515
5589
  const skip = options.noSkip === true ? void 0 : skipReason(options.runtime, paths, loaded.value, changes, tree, options.sessionId);
5516
5590
  if (skip !== void 0) return { kind: "skipped", reason: skip };
5517
- const pr = memo(() => prStatus(options.runtime, git, paths));
5518
- const size = measureSize(options.runtime, git, base, changes, loaded.value.contract);
5591
+ const pr = memo(() => prStatus(options.runtime, git2, paths));
5592
+ const size = measureSize(options.runtime, git2, base, changes, loaded.value.contract);
5519
5593
  const ctx = {
5520
5594
  runtime: options.runtime,
5521
- git,
5595
+ git: git2,
5522
5596
  paths,
5523
5597
  contract: applySize(loaded.value.contract, size),
5524
5598
  size,
@@ -6436,7 +6510,10 @@ var en_default = {
6436
6510
  "reason.label.blocked": "blocked",
6437
6511
  "reason.label.requires": "requires",
6438
6512
  "reason.label.not": "does not count",
6439
- "reason.human.generic": "Blocked a tool call; the reason went to Claude."
6513
+ "reason.human.generic": "Blocked a tool call; the reason went to Claude.",
6514
+ "review.command.packet": "Print the review packet for the reviewer and verifier agents: diff, task contract, evidence ledger and REVIEW.md, never the conversation",
6515
+ "review.guard.read_only": "blocked: the {agent} agent is read-only and cannot use {tool}. Report the finding; the main session makes the change.",
6516
+ "review.error.no_base": "the base {detail} is not a commit here"
6440
6517
  };
6441
6518
 
6442
6519
  // src/i18n/es.json
@@ -7085,7 +7162,10 @@ var es_default = {
7085
7162
  "reason.label.blocked": "bloqueado",
7086
7163
  "reason.label.requires": "exige",
7087
7164
  "reason.label.not": "no cuenta",
7088
- "reason.human.generic": "Bloqueé una llamada de herramienta; el motivo fue a Claude."
7165
+ "reason.human.generic": "Bloqueé una llamada de herramienta; el motivo fue a Claude.",
7166
+ "review.command.packet": "Muestra el paquete de revisión para los agentes revisor y verificador: diff, contrato de la tarea, registro de evidencias y REVIEW.md, nunca la conversación",
7167
+ "review.guard.read_only": "bloqueado: el agente {agent} es de solo lectura y no puede usar {tool}. Informa el hallazgo; la sesión principal hace el cambio.",
7168
+ "review.error.no_base": "la base {detail} no es un commit aquí"
7089
7169
  };
7090
7170
 
7091
7171
  // src/i18n/pt-BR.json
@@ -7734,7 +7814,10 @@ var pt_BR_default = {
7734
7814
  "reason.label.blocked": "bloqueado",
7735
7815
  "reason.label.requires": "exige",
7736
7816
  "reason.label.not": "não vale",
7737
- "reason.human.generic": "Bloqueei uma chamada de ferramenta; o motivo foi para o Claude."
7817
+ "reason.human.generic": "Bloqueei uma chamada de ferramenta; o motivo foi para o Claude.",
7818
+ "review.command.packet": "Mostra o pacote de revisão para os agentes revisor e verificador: diff, contrato da tarefa, registro de evidências e REVIEW.md, nunca a conversa",
7819
+ "review.guard.read_only": "bloqueado: o agente {agent} é só leitura e não pode usar {tool}. Relate o achado; a sessão principal faz a mudança.",
7820
+ "review.error.no_base": "a base {detail} não é um commit aqui"
7738
7821
  };
7739
7822
 
7740
7823
  // src/i18n/review.json
@@ -8384,7 +8467,10 @@ var review_default = {
8384
8467
  "reason.label.blocked": "machine",
8385
8468
  "reason.label.requires": "machine",
8386
8469
  "reason.label.not": "machine",
8387
- "reason.human.generic": "machine"
8470
+ "reason.human.generic": "machine",
8471
+ "review.command.packet": "machine",
8472
+ "review.guard.read_only": "machine",
8473
+ "review.error.no_base": "machine"
8388
8474
  },
8389
8475
  es: {
8390
8476
  "command.init.summary": "machine",
@@ -9031,7 +9117,10 @@ var review_default = {
9031
9117
  "reason.label.blocked": "machine",
9032
9118
  "reason.label.requires": "machine",
9033
9119
  "reason.label.not": "machine",
9034
- "reason.human.generic": "machine"
9120
+ "reason.human.generic": "machine",
9121
+ "review.command.packet": "machine",
9122
+ "review.guard.read_only": "machine",
9123
+ "review.error.no_base": "machine"
9035
9124
  }
9036
9125
  };
9037
9126
 
@@ -9262,9 +9351,9 @@ var notRunLine = (t, runtime, paths) => {
9262
9351
  var markSession = (input, paths) => {
9263
9352
  const { runtime } = input;
9264
9353
  const sessionId = input.payload.sessionId;
9265
- const git = gitAt(runtime, paths.root);
9266
- if (sessionId === void 0 || headSha(git) === void 0) return;
9267
- const tree = treeHash(git);
9354
+ const git2 = gitAt(runtime, paths.root);
9355
+ if (sessionId === void 0 || headSha(git2) === void 0) return;
9356
+ const tree = treeHash(git2);
9268
9357
  if (tree === void 0) return;
9269
9358
  writeSession(runtime, paths, { sessionId, startTree: tree, at: nowMs(runtime) });
9270
9359
  startBaseline(runtime, paths, tree);
@@ -9470,22 +9559,22 @@ var current = (runtime, root, path) => {
9470
9559
  return text7.ok ? text7.value : "";
9471
9560
  };
9472
9561
  var keyOf = (language, name) => `${language.id}\0${name}`;
9473
- var redefinedNames = (runtime, git, changes) => {
9562
+ var redefinedNames = (runtime, git2, changes) => {
9474
9563
  const keys = /* @__PURE__ */ new Set();
9475
9564
  for (const change of changes) {
9476
9565
  const language = languageOf(change.path);
9477
9566
  if (language === void 0 || change.status === "D") continue;
9478
- for (const name of definitionsIn(language, current(runtime, git.cwd, change.path))) keys.add(keyOf(language, name));
9567
+ for (const name of definitionsIn(language, current(runtime, git2.cwd, change.path))) keys.add(keyOf(language, name));
9479
9568
  }
9480
9569
  return keys;
9481
9570
  };
9482
- var removedSymbols = (runtime, git, base, changes) => {
9483
- const redefined = redefinedNames(runtime, git, changes);
9571
+ var removedSymbols = (runtime, git2, base, changes) => {
9572
+ const redefined = redefinedNames(runtime, git2, changes);
9484
9573
  const found = /* @__PURE__ */ new Map();
9485
9574
  for (const change of changes) {
9486
9575
  const language = languageOf(change.oldPath ?? change.path);
9487
9576
  if (language === void 0 || change.status === "A") continue;
9488
- const before = definitionsIn(language, fileAt(git, base, change.oldPath ?? change.path) ?? "");
9577
+ const before = definitionsIn(language, fileAt(git2, base, change.oldPath ?? change.path) ?? "");
9489
9578
  const origins = [change.path, ...change.oldPath === void 0 ? [] : [change.oldPath]];
9490
9579
  for (const name of before) {
9491
9580
  const key = keyOf(language, name);
@@ -9501,19 +9590,19 @@ var parseGrep = (output) => output.split("\n").filter((row) => row !== "").flatM
9501
9590
  const number2 = Number(line2);
9502
9591
  return file === "" || !Number.isInteger(number2) ? [] : [{ file: toPosixPath(file), line: number2, text: rest.join("\0") }];
9503
9592
  });
9504
- var grep = (git, names) => {
9593
+ var grep = (git2, names) => {
9505
9594
  if (names.length === 0) return [];
9506
9595
  const patterns = names.flatMap((name) => ["-e", name]);
9507
9596
  return parseGrep(
9508
- git.out(["grep", "-n", "-z", "-w", "-I", "--untracked", "--no-color", ...patterns, "--", ".", EXCLUDE_FM]) ?? ""
9597
+ git2.out(["grep", "-n", "-z", "-w", "-I", "--untracked", "--no-color", ...patterns, "--", ".", EXCLUDE_FM]) ?? ""
9509
9598
  );
9510
9599
  };
9511
9600
  var mentions = (text7, name) => new RegExp(`(?<![\\w$])${name.replace(/[$?!]/g, "\\$&")}(?![\\w$])`).test(text7);
9512
- var callSites = (runtime, git, symbols) => {
9513
- const hits = grep(git, [...new Set(symbols.map((symbol) => symbol.name))]);
9601
+ var callSites = (runtime, git2, symbols) => {
9602
+ const hits = grep(git2, [...new Set(symbols.map((symbol) => symbol.name))]);
9514
9603
  const texts2 = /* @__PURE__ */ new Map();
9515
9604
  const textOf4 = (file) => {
9516
- const cached2 = texts2.get(file) ?? current(runtime, git.cwd, file);
9605
+ const cached2 = texts2.get(file) ?? current(runtime, git2.cwd, file);
9517
9606
  texts2.set(file, cached2);
9518
9607
  return cached2;
9519
9608
  };
@@ -10265,9 +10354,9 @@ var splitBlock = (block4) => {
10265
10354
  });
10266
10355
  };
10267
10356
  var unitsOf = (blocks) => {
10268
- let section = 0;
10357
+ let section2 = 0;
10269
10358
  return blocks.flatMap((block4, paragraph) => {
10270
- if (block4.kind === "heading") section += 1;
10359
+ if (block4.kind === "heading") section2 += 1;
10271
10360
  const pieces = splitBlock(block4);
10272
10361
  const next2 = blocks[paragraph + 1];
10273
10362
  const closesSection = block4.kind === "sentence" && (next2 === void 0 || next2.kind === "heading");
@@ -10275,7 +10364,7 @@ var unitsOf = (blocks) => {
10275
10364
  text: piece.text,
10276
10365
  line: piece.line,
10277
10366
  paragraph,
10278
- section,
10367
+ section: section2,
10279
10368
  kind: block4.kind,
10280
10369
  lastInParagraph: index === pieces.length - 1,
10281
10370
  closesSection
@@ -10503,9 +10592,9 @@ var phraseHits = (units, catalog) => units.flatMap(
10503
10592
  })
10504
10593
  );
10505
10594
  var windowEnd = (units, from, size) => {
10506
- const section = units[from]?.section;
10595
+ const section2 = units[from]?.section;
10507
10596
  let to = from;
10508
- while (to + 1 < Math.min(units.length, from + size) && units[to + 1]?.section === section) to += 1;
10597
+ while (to + 1 < Math.min(units.length, from + size) && units[to + 1]?.section === section2) to += 1;
10509
10598
  return to;
10510
10599
  };
10511
10600
  var windowScore = (hits, window) => hits.filter((item2) => item2.unit >= window.from && item2.unit <= window.to).reduce((sum, item2) => sum + item2.weight, 0);
@@ -11017,10 +11106,10 @@ var SKIPPED = /(?:^|\/)(?:\.git|node_modules|\.finitemonkey)\//;
11017
11106
  var DIFF_FLAGS = ["-U0", "--no-color", "--no-ext-diff", "--no-renames"];
11018
11107
  var binary = (text7) => text7.slice(0, 8e3).includes("\0");
11019
11108
  var listed2 = (runtime, root, target2) => {
11020
- const git = runtime.git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", target2], root);
11021
- if (git.ok)
11109
+ const git2 = runtime.git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", target2], root);
11110
+ if (git2.ok)
11022
11111
  return accept(
11023
- git.stdout.split("\0").filter((name) => name !== "").map(toPosixPath)
11112
+ git2.stdout.split("\0").filter((name) => name !== "").map(toPosixPath)
11024
11113
  );
11025
11114
  const base = toAbsolutePath(target2, root);
11026
11115
  const walked = runtime.walkFiles(base);
@@ -11044,15 +11133,15 @@ var diffFindings = (diff) => {
11044
11133
  };
11045
11134
  var scanStaged = (runtime, root, includeUnstaged = false) => {
11046
11135
  const args2 = includeUnstaged ? ["diff", "HEAD", ...DIFF_FLAGS] : ["diff", "--cached", ...DIFF_FLAGS];
11047
- const git = runtime.git(args2, root);
11048
- if (!git.ok) return reject(git.found ? "git diff failed" : "git not found");
11049
- return accept(diffFindings(git.stdout));
11136
+ const git2 = runtime.git(args2, root);
11137
+ if (!git2.ok) return reject(git2.found ? "git diff failed" : "git not found");
11138
+ return accept(diffFindings(git2.stdout));
11050
11139
  };
11051
11140
  var scanHistory = async (runtime, root) => {
11052
11141
  const scanner = diffScanner();
11053
11142
  const args2 = ["log", "-p", "--all", "--format=commit %H", ...DIFF_FLAGS];
11054
- const git = await runtime.gitLines(args2, root, scanner.push);
11055
- if (!git.ok) return reject(git.found ? "git log failed" : "git not found");
11143
+ const git2 = await runtime.gitLines(args2, root, scanner.push);
11144
+ if (!git2.ok) return reject(git2.found ? "git log failed" : "git not found");
11056
11145
  return accept(scanner.end().filter((finding4) => !SKIPPED.test(finding4.file)));
11057
11146
  };
11058
11147
 
@@ -12399,7 +12488,7 @@ var subagentStart = (input) => {
12399
12488
 
12400
12489
  // src/gate/hooks/index.ts
12401
12490
  var PORTAO_HANDLERS = {
12402
- PreToolUse: [selfGuard, bypassGuard, testsGuard],
12491
+ PreToolUse: [reviewAgentGuard, selfGuard, bypassGuard, testsGuard],
12403
12492
  UserPromptSubmit: [pausePrompt, promptRelease],
12404
12493
  Stop: [unlessPaused(stopGate)],
12405
12494
  SubagentStop: [unlessPaused(subagentStopGate)],
@@ -17759,6 +17848,96 @@ var proseCheckCommand = {
17759
17848
  }
17760
17849
  };
17761
17850
 
17851
+ // src/review/packet.ts
17852
+ import { join as join45 } from "node:path";
17853
+ var REVIEW_STATE_FILE = ".finitemonkey/review/state.json";
17854
+ var FULL_ROUNDS = 3;
17855
+ var REVIEW_FILE_MAX_LINES = 200;
17856
+ var LEDGER_MAX_LINES = 100;
17857
+ var statePath = (root) => toAbsolutePath(join45(root, REVIEW_STATE_FILE));
17858
+ var readReviewState = (runtime, root) => {
17859
+ const raw = runtime.readText(statePath(root));
17860
+ const parsed = raw.ok ? parseJsonObject(raw.value) : void 0;
17861
+ if (parsed === void 0 || !parsed.ok || !isRecord(parsed.value)) return void 0;
17862
+ const { base, rounds, lastHead } = parsed.value;
17863
+ return typeof base === "string" && typeof rounds === "number" && typeof lastHead === "string" ? { base, rounds, lastHead } : void 0;
17864
+ };
17865
+ var git = (runtime, root, args2) => {
17866
+ const result = runtime.git(args2, root);
17867
+ return result.ok ? result.stdout : void 0;
17868
+ };
17869
+ var lastLines = (text7, max) => text7.split("\n").slice(-max).join("\n");
17870
+ var section = (title, body2, empty) => `## ${title}
17871
+
17872
+ ${body2 === void 0 || body2.trim() === "" ? empty : body2.trimEnd()}
17873
+ `;
17874
+ var taskSections = (runtime, root) => {
17875
+ const paths = fmPaths(root);
17876
+ const active = readState(runtime, paths).active;
17877
+ if (active === void 0)
17878
+ return [section("Contract", void 0, "(no active task)"), section("Evidence ledger", void 0, "(none)")];
17879
+ const contract = runtime.readText(toAbsolutePath(join45(root, taskRelative(active))));
17880
+ const ledger = runtime.readText(paths.evidence(active));
17881
+ return [
17882
+ section("Contract", contract.ok ? contract.value : void 0, "(contract file missing)"),
17883
+ section("Evidence ledger", ledger.ok ? lastLines(ledger.value, LEDGER_MAX_LINES) : void 0, "(none)")
17884
+ ];
17885
+ };
17886
+ var reviewFile = (runtime, root, base) => {
17887
+ const text7 = git(runtime, root, ["show", `${base}:REVIEW.md`]);
17888
+ return text7 === void 0 ? void 0 : text7.split("\n").slice(0, REVIEW_FILE_MAX_LINES).join("\n");
17889
+ };
17890
+ var nextState = (previous, base, head) => {
17891
+ if (previous?.base !== base) return { base, rounds: 1, lastHead: head };
17892
+ return { base, rounds: previous.rounds + 1, lastHead: head };
17893
+ };
17894
+ var buildPacket = (runtime, root, base, head) => {
17895
+ const previous = readReviewState(runtime, root);
17896
+ const state = nextState(previous, base, head);
17897
+ const from = state.rounds > FULL_ROUNDS && previous !== void 0 ? previous.lastHead : base;
17898
+ const range = `${from}...${head}`;
17899
+ const diff = git(runtime, root, ["diff", "--no-color", "--no-ext-diff", range, "--", ".", ":(exclude).finitemonkey"]);
17900
+ const owners = reviewFile(runtime, root, base);
17901
+ const text7 = [
17902
+ `# Review packet (round ${String(state.rounds)}, ${range})`,
17903
+ "",
17904
+ ...state.rounds > FULL_ROUNDS ? ["Only the diff since the last review round is included.", ""] : [],
17905
+ section("Diff", diff, "(empty diff)"),
17906
+ ...taskSections(runtime, root),
17907
+ ...owners === void 0 ? [] : [section("REVIEW.md (base branch)", owners, "")]
17908
+ ].join("\n");
17909
+ runtime.mkdir(toAbsolutePath(join45(root, ".finitemonkey", "review")));
17910
+ runtime.writeAtomic(statePath(root), `${JSON.stringify(state, null, 2)}
17911
+ `);
17912
+ return { round: state.rounds, base, head, range, text: text7 };
17913
+ };
17914
+
17915
+ // src/commands/review.ts
17916
+ var REVIEW_PACKET_SCHEMA = "finitemonkey/review-packet@1";
17917
+ var reviewPacketCommand = {
17918
+ path: ["review", "packet"],
17919
+ summaryKey: "review.command.packet",
17920
+ usage: "finitemonkey review packet [--base <ref>] [--json]",
17921
+ status: "ready",
17922
+ options: { base: { type: "string" } },
17923
+ run: (ctx, args2) => {
17924
+ const repo = repoOf2(ctx);
17925
+ if (repo === void 0) return Promise.resolve(failureFrom(ctx.t, notRepository(ctx.cwd)));
17926
+ const base = stringOption(args2.values, "base") ?? repoFacts(ctx.runtime, repo).defaultBranch();
17927
+ const resolved = ctx.runtime.git(["rev-parse", "--verify", "--quiet", `${base}^{commit}`], repo.root);
17928
+ const head = ctx.runtime.git(["rev-parse", "HEAD"], repo.root).stdout.trim();
17929
+ if (!resolved.ok || head === "")
17930
+ return Promise.resolve(
17931
+ failureFrom(
17932
+ ctx.t,
17933
+ gitProblem("invalid_arguments", "review.error.no_base", base, "finitemonkey review packet --base <ref>")
17934
+ )
17935
+ );
17936
+ const packet = buildPacket(ctx.runtime, repo.root, base, head);
17937
+ return Promise.resolve(success({ schema: REVIEW_PACKET_SCHEMA, ...packet }, [packet.text]));
17938
+ }
17939
+ };
17940
+
17762
17941
  // src/commands/secrets-problems.ts
17763
17942
  var baselineInvalid = (detail) => ({
17764
17943
  code: "secrets_baseline_invalid",
@@ -17882,7 +18061,7 @@ var secretsAcceptCommand = {
17882
18061
  };
17883
18062
 
17884
18063
  // src/secrets/runbooks.ts
17885
- import { dirname as dirname9, join as join45 } from "node:path";
18064
+ import { dirname as dirname9, join as join46 } from "node:path";
17886
18065
  var PROVIDER_OF_RULE = {
17887
18066
  "aws-access-token": "aws",
17888
18067
  "aws-secret-access-key": "aws",
@@ -17904,7 +18083,7 @@ var RUNBOOKS_URL = "https://github.com/gabrielvieiraf/finitemonkey/blob/main/run
17904
18083
  var runbookProvider = (rule) => PROVIDER_OF_RULE[rule] ?? "generic";
17905
18084
  var runbookLink = (runtime, rule) => {
17906
18085
  const file = `${runbookProvider(rule)}.md`;
17907
- const local = toAbsolutePath(join45(dirname9(dirname9(runtime.selfPath)), "runbooks", file));
18086
+ const local = toAbsolutePath(join46(dirname9(dirname9(runtime.selfPath)), "runbooks", file));
17908
18087
  return runtime.stat(local)?.kind === "file" ? local : `${RUNBOOKS_URL}/${file}`;
17909
18088
  };
17910
18089
 
@@ -18266,7 +18445,7 @@ var secretsScanCommand = {
18266
18445
  };
18267
18446
 
18268
18447
  // src/task/create.ts
18269
- import { join as join46 } from "node:path";
18448
+ import { join as join47 } from "node:path";
18270
18449
 
18271
18450
  // src/core/yaml/write.ts
18272
18451
  var PLAIN_SAFE = /^[A-Za-z_./][A-Za-z0-9_./ ()-]*$/;
@@ -18355,10 +18534,10 @@ var explicitChecks = (draft) => [
18355
18534
  var draftChecks = (runtime, paths, draft) => {
18356
18535
  const disk = {
18357
18536
  read: (relative11) => {
18358
- const text7 = runtime.readText(toAbsolutePath(join46(paths.root, relative11)));
18537
+ const text7 = runtime.readText(toAbsolutePath(join47(paths.root, relative11)));
18359
18538
  return text7.ok ? text7.value : void 0;
18360
18539
  },
18361
- exists: (relative11) => runtime.stat(toAbsolutePath(join46(paths.root, relative11))) !== void 0
18540
+ exists: (relative11) => runtime.stat(toAbsolutePath(join47(paths.root, relative11))) !== void 0
18362
18541
  };
18363
18542
  const detected = detectChecks(disk);
18364
18543
  const base = draft.runs.length > 0 || draft.tests !== void 0 ? explicitChecks(draft) : detected;
@@ -18373,7 +18552,7 @@ var draftChecks = (runtime, paths, draft) => {
18373
18552
  { id: "merge", kind: "merge" },
18374
18553
  { id: "callers", kind: "callers" },
18375
18554
  ...draft.paths.length > 0 || draft.budget !== void 0 ? [{ id: "diff", kind: "diff" }] : [],
18376
- ...runtime.stat(toAbsolutePath(join46(paths.root, LOOK_CONFIG))) === void 0 ? [] : [{ id: "look", kind: "look" }],
18555
+ ...runtime.stat(toAbsolutePath(join47(paths.root, LOOK_CONFIG))) === void 0 ? [] : [{ id: "look", kind: "look" }],
18377
18556
  ...draft.git.length > 0 ? [{ id: "git", kind: "git", require: draft.git }] : [],
18378
18557
  { id: "pr", kind: "pr", requireOpen: draft.git.includes("pr_open") },
18379
18558
  ...draft.manual.map((description, index) => ({
@@ -18775,12 +18954,13 @@ var REGISTRY = [
18775
18954
  pushCheckCommand,
18776
18955
  ciGateCommand,
18777
18956
  lookCommand,
18957
+ reviewPacketCommand,
18778
18958
  versionCommand,
18779
18959
  helpCommand
18780
18960
  ];
18781
18961
 
18782
18962
  // src/i18n/lang-settings.ts
18783
- import { join as join47 } from "node:path";
18963
+ import { join as join48 } from "node:path";
18784
18964
  var read3 = (runtime, path) => {
18785
18965
  const text7 = runtime.readText(toAbsolutePath(path));
18786
18966
  return text7.ok ? text7.value : void 0;
@@ -18795,14 +18975,14 @@ var languageIn = (text7) => {
18795
18975
  const value = parsed.ok && isRecord(parsed.value) ? parsed.value["language"] : void 0;
18796
18976
  return typeof value === "string" && value.trim() !== "" ? value : void 0;
18797
18977
  };
18798
- var claudeDir = (runtime, env) => env["CLAUDE_CONFIG_DIR"] ?? join47(runtime.homeDir(), ".claude");
18978
+ var claudeDir = (runtime, env) => env["CLAUDE_CONFIG_DIR"] ?? join48(runtime.homeDir(), ".claude");
18799
18979
  var langSettings = (runtime, cwd, env, platform) => {
18800
18980
  const root = findGitRoot(cwd, runtime) ?? cwd;
18801
- const config = parseConfigYaml(read3(runtime, join47(root, CONFIG_FILE)) ?? "");
18981
+ const config = parseConfigYaml(read3(runtime, join48(root, CONFIG_FILE)) ?? "");
18802
18982
  const claudeFiles = [
18803
- join47(root, ".claude", "settings.local.json"),
18804
- join47(root, ".claude", "settings.json"),
18805
- join47(claudeDir(runtime, env), "settings.json")
18983
+ join48(root, ".claude", "settings.local.json"),
18984
+ join48(root, ".claude", "settings.json"),
18985
+ join48(claudeDir(runtime, env), "settings.json")
18806
18986
  ];
18807
18987
  return {
18808
18988
  config: configText(config, "lang"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "finitemonkey",
3
- "version": "0.1.0-next.22",
3
+ "version": "0.1.0-next.23",
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",