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 +1 -0
- package/README.md +1 -0
- package/README.pt-BR.md +1 -0
- package/dist/finitemonkey.mjs +370 -190
- package/package.json +1 -1
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. |
|
package/dist/finitemonkey.mjs
CHANGED
|
@@ -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
|
|
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 = (
|
|
5168
|
-
const top =
|
|
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 = (
|
|
5173
|
-
var currentBranch = (
|
|
5174
|
-
const name =
|
|
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 = (
|
|
5178
|
-
var resolveBase = (
|
|
5179
|
-
if (declared !== 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(
|
|
5183
|
-
const merged2 = remote === void 0 ? void 0 :
|
|
5184
|
-
return nonEmpty(merged2) ?? nonEmpty(headSha(
|
|
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 = (
|
|
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 = (
|
|
5281
|
+
var changedFiles = (git2, base) => {
|
|
5208
5282
|
const tracked = parseNameStatus(
|
|
5209
|
-
|
|
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(
|
|
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 = (
|
|
5216
|
-
var treeHash = (
|
|
5217
|
-
const head = headSha(
|
|
5218
|
-
const diff =
|
|
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(
|
|
5221
|
-
const hashes = files.length === 0 ? "" :
|
|
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 = (
|
|
5241
|
-
var numstat = (
|
|
5242
|
-
const rows = (
|
|
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 = (
|
|
5250
|
-
var aheadOfUpstream = (
|
|
5251
|
-
const count4 =
|
|
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,
|
|
5334
|
+
var diffShape = (runtime, git2, base, changes) => ({
|
|
5261
5335
|
files: changes.map((change) => change.path),
|
|
5262
|
-
lines: numstat(
|
|
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,
|
|
5323
|
-
const auth = ghAuthenticated(runtime,
|
|
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,
|
|
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,
|
|
5336
|
-
const branch = currentBranch(
|
|
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(
|
|
5340
|
-
const status = fetchView(runtime,
|
|
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(
|
|
5417
|
+
return { kind: "found", branch, pr: status.pr, localHead: headSha(git2) };
|
|
5344
5418
|
};
|
|
5345
|
-
var conflictFiles = (
|
|
5419
|
+
var conflictFiles = (git2, base) => {
|
|
5346
5420
|
for (const target2 of [`origin/${base}`, base]) {
|
|
5347
|
-
if (
|
|
5348
|
-
const result =
|
|
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 = (
|
|
5469
|
-
read: (relative11) => base === EMPTY_TREE ? disk.read(relative11) : fileAt(
|
|
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,
|
|
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(
|
|
5476
|
-
return loadTask(options.runtime, paths, { disk: disk.read, base: (relative11) => fileAt(
|
|
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,
|
|
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
|
|
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(
|
|
5509
|
-
const loaded = loadContract(options, paths,
|
|
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(
|
|
5512
|
-
const changes = changedFiles(
|
|
5513
|
-
const tree = treeHash(
|
|
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,
|
|
5518
|
-
const size = measureSize(options.runtime,
|
|
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
|
|
9266
|
-
if (sessionId === void 0 || headSha(
|
|
9267
|
-
const tree = treeHash(
|
|
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,
|
|
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,
|
|
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,
|
|
9483
|
-
const redefined = redefinedNames(runtime,
|
|
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(
|
|
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 = (
|
|
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
|
-
|
|
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,
|
|
9513
|
-
const hits = grep(
|
|
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,
|
|
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
|
|
10357
|
+
let section2 = 0;
|
|
10269
10358
|
return blocks.flatMap((block4, paragraph) => {
|
|
10270
|
-
if (block4.kind === "heading")
|
|
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
|
|
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 ===
|
|
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
|
|
11021
|
-
if (
|
|
11109
|
+
const git2 = runtime.git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", target2], root);
|
|
11110
|
+
if (git2.ok)
|
|
11022
11111
|
return accept(
|
|
11023
|
-
|
|
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
|
|
11048
|
-
if (!
|
|
11049
|
-
return accept(diffFindings(
|
|
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
|
|
11055
|
-
if (!
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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"] ??
|
|
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,
|
|
18981
|
+
const config = parseConfigYaml(read3(runtime, join48(root, CONFIG_FILE)) ?? "");
|
|
18802
18982
|
const claudeFiles = [
|
|
18803
|
-
|
|
18804
|
-
|
|
18805
|
-
|
|
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.
|
|
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",
|