@jpkme/diff-review 0.1.12 → 0.1.14
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.md +1 -1
- package/bin/diff-review.mjs +72 -53
- package/lib/acp-policy.mjs +99 -12
- package/lib/acp-policy.test.mjs +56 -3
- package/lib/acp-shell.mjs +38 -8
- package/lib/acp-shell.test.mjs +32 -1
- package/lib/agent-login.mjs +105 -105
- package/lib/check-report.mjs +1 -0
- package/lib/check-report.test.mjs +1 -0
- package/lib/extract-cache.test.mjs +162 -162
- package/lib/model-label.mjs +263 -263
- package/lib/model-label.test.mjs +77 -77
- package/lib/models-cache.mjs +75 -75
- package/lib/models-cache.test.mjs +50 -50
- package/lib/prompt.mjs +8 -6
- package/lib/review-prep.mjs +63 -63
- package/package.json +1 -1
- package/web/build/client/_app/immutable/assets/{0.CLQiq3Ed.css → 0.BKRG1f36.css} +1 -1
- package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.br +0 -0
- package/web/build/client/_app/immutable/assets/{0.CLQiq3Ed.css.gz → 0.BKRG1f36.css.gz} +0 -0
- package/web/build/client/_app/immutable/assets/2.C4whE4xz.css +1 -0
- package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.BT54aVKb.css +1 -0
- package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/storage.C8XGESwn.css +1 -0
- package/web/build/client/_app/immutable/assets/storage.C8XGESwn.css.br +0 -0
- package/web/build/client/_app/immutable/assets/storage.C8XGESwn.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/C0hrdiUt.js +2 -0
- package/web/build/client/_app/immutable/chunks/C0hrdiUt.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/C0hrdiUt.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/CpeIO6nz.js +210 -0
- package/web/build/client/_app/immutable/chunks/CpeIO6nz.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/CpeIO6nz.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/{CPyWH418.js → FOc6rgIW.js} +1 -1
- package/web/build/client/_app/immutable/chunks/FOc6rgIW.js.br +2 -0
- package/web/build/client/_app/immutable/chunks/FOc6rgIW.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/{app.BiBohpyO.js → app.DcAtSJ3n.js} +2 -2
- package/web/build/client/_app/immutable/entry/app.DcAtSJ3n.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.DcAtSJ3n.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.CO5l0Zbx.js +1 -0
- package/web/build/client/_app/immutable/entry/start.CO5l0Zbx.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.CO5l0Zbx.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/{1.BzPXaJJj.js → 1.E-Hs7ura.js} +1 -1
- package/web/build/client/_app/immutable/nodes/1.E-Hs7ura.js.br +4 -0
- package/web/build/client/_app/immutable/nodes/1.E-Hs7ura.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.s1_irV1Q.js +10 -0
- package/web/build/client/_app/immutable/nodes/2.s1_irV1Q.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.s1_irV1Q.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.pgwe-_5b.js +171 -0
- package/web/build/client/_app/immutable/nodes/3.pgwe-_5b.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.pgwe-_5b.js.gz +0 -0
- package/web/build/client/_app/version.json +1 -1
- package/web/build/client/_app/version.json.br +0 -0
- package/web/build/client/_app/version.json.gz +0 -0
- package/web/build/handler.js +3 -3
- package/web/build/index.js +3 -3
- package/web/build/server/chunks/{0-BYok4D0n.js → 0-DUStDOsr.js} +3 -3
- package/web/build/server/chunks/{0-BYok4D0n.js.map → 0-DUStDOsr.js.map} +1 -1
- package/web/build/server/chunks/1-BFhhV5JB.js +7 -0
- package/web/build/server/chunks/{1-C7zcB0q0.js.map → 1-BFhhV5JB.js.map} +1 -1
- package/web/build/server/chunks/2-BZ_MVqj8.js +7 -0
- package/web/build/server/chunks/2-BZ_MVqj8.js.map +1 -0
- package/web/build/server/chunks/3-BV-TIUp1.js +7 -0
- package/web/build/server/chunks/{3-DAAmG6VX.js.map → 3-BV-TIUp1.js.map} +1 -1
- package/web/build/server/chunks/{_server.ts-qYVrMYPB.js → _server.ts-CdFoXw36.js} +4 -4
- package/web/build/server/chunks/{_server.ts-qYVrMYPB.js.map → _server.ts-CdFoXw36.js.map} +1 -1
- package/web/build/server/chunks/{agent-sessions-DSqGavSn.js → agent-sessions-D0WwtWgR.js} +223 -51
- package/web/build/server/chunks/agent-sessions-D0WwtWgR.js.map +1 -0
- package/web/build/server/chunks/{git-review-CpO_G8z8.js → git-review-CgcBIbvM.js} +7 -7
- package/web/build/server/chunks/{git-review-CpO_G8z8.js.map → git-review-CgcBIbvM.js.map} +1 -1
- package/web/build/server/chunks/{handler-Di1wJQuU.js → handler-DbxjtK9M.js} +3 -3
- package/web/build/server/chunks/{handler-Di1wJQuU.js.map → handler-DbxjtK9M.js.map} +1 -1
- package/web/build/server/chunks/{hooks.server-BLOHobEK.js → hooks.server-Dg6gYUqs.js} +3 -3
- package/web/build/server/chunks/{hooks.server-BLOHobEK.js.map → hooks.server-Dg6gYUqs.js.map} +1 -1
- package/web/build/server/chunks/{index.js-yLYmrAWy.js → index.js-NNne-njN.js} +876 -879
- package/web/build/server/chunks/index.js-NNne-njN.js.map +1 -0
- package/web/build/server/chunks/{manifest.js-CXj8IIo3.js → manifest.js-V8ViI5bl.js} +9 -9
- package/web/build/server/chunks/{manifest.js-CXj8IIo3.js.map → manifest.js-V8ViI5bl.js.map} +1 -1
- package/web/build/server/chunks/{remote-1g4ayag-BiJeU0Pk.js → remote-1g4ayag-CCsG2QXw.js} +4 -4
- package/web/build/server/chunks/{remote-1g4ayag-BiJeU0Pk.js.map → remote-1g4ayag-CCsG2QXw.js.map} +1 -1
- package/web/build/server/chunks/{remote-wh6t6j-CHanXurm.js → remote-wh6t6j-BCvmX37z.js} +5 -5
- package/web/build/server/chunks/{remote-wh6t6j-CHanXurm.js.map → remote-wh6t6j-BCvmX37z.js.map} +1 -1
- package/web/build/server/chunks/{validate-BsTKyhsq.js → validate-CAQ31C7k.js} +2 -2
- package/web/build/server/chunks/{validate-BsTKyhsq.js.map → validate-CAQ31C7k.js.map} +1 -1
- package/web/build/client/_app/immutable/assets/0.CLQiq3Ed.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css +0 -1
- package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.br +0 -0
- package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css +0 -1
- package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.br +0 -0
- package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.gz +0 -0
- package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css +0 -1
- package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.br +0 -0
- package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.gz +0 -0
- package/web/build/client/_app/immutable/chunks/B5ByWcPE.js +0 -2
- package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/CPyWH418.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/CPyWH418.js.gz +0 -0
- package/web/build/client/_app/immutable/chunks/NwkMsx0h.js +0 -210
- package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.br +0 -0
- package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.br +0 -0
- package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.gz +0 -0
- package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js +0 -1
- package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.br +0 -0
- package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.br +0 -2
- package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js +0 -10
- package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.gz +0 -0
- package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js +0 -171
- package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js.br +0 -0
- package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js.gz +0 -0
- package/web/build/server/chunks/1-C7zcB0q0.js +0 -7
- package/web/build/server/chunks/2-v6ejI_0-.js +0 -7
- package/web/build/server/chunks/2-v6ejI_0-.js.map +0 -1
- package/web/build/server/chunks/3-DAAmG6VX.js +0 -7
- package/web/build/server/chunks/agent-sessions-DSqGavSn.js.map +0 -1
- package/web/build/server/chunks/index.js-yLYmrAWy.js.map +0 -1
- /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js → 0.Dj97JW46.js} +0 -0
- /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js.br → 0.Dj97JW46.js.br} +0 -0
- /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js.gz → 0.Dj97JW46.js.gz} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, readFileSync, statSync, accessSync, constants, readdirSync, mkdirSync, writeFileSync, copyFileSync, rmSync } from 'node:fs';
|
|
1
|
+
import { o as getRemoteUrl, e as ensureReviewDir, w as writeDiffCache, p as readDiffCache, q as extractBranchDiff, u as loadReviewConfig, v as outputRelPath, m as makeOutputFilename, O as OUTPUT_DIR, x as writeSessionRef, c as companionPaths, d as decodePayload, t as tryResolveGitRoot, f as loadBranchDiff, y as validateAgainst, z as reviewIssues, R as REVIEW_SCHEMA, A as globMatch, C as writeDiffText } from './validate-CAQ31C7k.js';
|
|
2
|
+
import { existsSync, readFileSync, statSync, accessSync, constants, readdirSync, mkdirSync, writeFileSync, copyFileSync, rmSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { homedir, tmpdir } from 'node:os';
|
|
5
5
|
import { join, delimiter, isAbsolute, resolve, basename, dirname, normalize } from 'node:path';
|
|
@@ -492,17 +492,46 @@ var DISCARD = /* @__PURE__ */ new Set([
|
|
|
492
492
|
"nul",
|
|
493
493
|
"$null"
|
|
494
494
|
]);
|
|
495
|
-
/**
|
|
495
|
+
/**
|
|
496
|
+
* Paths de shell Unix → forma Windows cuando la política corre en win32:
|
|
497
|
+
* - Git Bash: `/c/Users/...` → `C:/Users/...`
|
|
498
|
+
* - WSL: `/mnt/c/Users/...` → `C:/Users/...`
|
|
499
|
+
*/
|
|
496
500
|
function toNativePath(p, platform) {
|
|
497
501
|
if (platform === "win32") {
|
|
502
|
+
const wsl = /^\/mnt\/([a-zA-Z])(\/|$)(.*)$/.exec(p);
|
|
503
|
+
if (wsl) return `${wsl[1].toUpperCase()}:/${wsl[3]}`;
|
|
498
504
|
const msys = /^\/([a-zA-Z])(\/|$)(.*)$/.exec(p);
|
|
499
505
|
if (msys) return `${msys[1].toUpperCase()}:/${msys[3]}`;
|
|
500
506
|
}
|
|
501
507
|
return p;
|
|
502
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* Resuelve symlinks cuando el path (o un ancestro) existe. Sin eso, un link
|
|
511
|
+
* dentro del repo puede apuntar fuera y pasar `isInside` con solo `normalize`.
|
|
512
|
+
* @param {string} path
|
|
513
|
+
*/
|
|
514
|
+
function canonicalPath$1(path) {
|
|
515
|
+
try {
|
|
516
|
+
return realpathSync(path);
|
|
517
|
+
} catch {
|
|
518
|
+
const parts = [];
|
|
519
|
+
let cur = normalize(path);
|
|
520
|
+
for (;;) {
|
|
521
|
+
const parent = dirname(cur);
|
|
522
|
+
if (!parent || parent === cur) return normalize(path);
|
|
523
|
+
parts.unshift(basename(cur));
|
|
524
|
+
try {
|
|
525
|
+
return resolve(realpathSync(parent), ...parts);
|
|
526
|
+
} catch {
|
|
527
|
+
cur = parent;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
503
532
|
function isInside$1(child, root, platform) {
|
|
504
|
-
const c = normalize(child).replace(/[\\/]+$/, "");
|
|
505
|
-
const r = normalize(root).replace(/[\\/]+$/, "");
|
|
533
|
+
const c = normalize(canonicalPath$1(child)).replace(/[\\/]+$/, "");
|
|
534
|
+
const r = normalize(canonicalPath$1(root)).replace(/[\\/]+$/, "");
|
|
506
535
|
const [cc, rr] = platform === "win32" ? [c.toLowerCase(), r.toLowerCase()] : [c, r];
|
|
507
536
|
return cc === rr || cc.startsWith(`${rr}\\`) || cc.startsWith(`${rr}/`);
|
|
508
537
|
}
|
|
@@ -727,13 +756,11 @@ function isValidate(argv) {
|
|
|
727
756
|
*/
|
|
728
757
|
function checkReviewDirMkdir(argv, ctx) {
|
|
729
758
|
const name = argv[0].toLowerCase();
|
|
730
|
-
const reviewRoot =
|
|
759
|
+
const reviewRoot = resolve(ctx.repo, ".diff-review");
|
|
731
760
|
/** @param {string} arg */
|
|
732
761
|
const underReviewDir = (arg) => {
|
|
733
762
|
const native = toNativePath(String(arg || "").replace(/^~(?=[\\/]|$)/, "/__home__"), ctx.platform);
|
|
734
|
-
|
|
735
|
-
const [a, r] = ctx.platform === "win32" ? [abs.toLowerCase(), reviewRoot.toLowerCase()] : [abs, reviewRoot];
|
|
736
|
-
return a === r || a.startsWith(`${r}\\`) || a.startsWith(`${r}/`);
|
|
763
|
+
return isInside$1(isAbsolute(native) ? native : resolve(ctx.repo, native), reviewRoot, ctx.platform);
|
|
737
764
|
};
|
|
738
765
|
if (name === "mkdir" || name === "md") {
|
|
739
766
|
const paths = argv.slice(1).filter((a) => !a.startsWith("-"));
|
|
@@ -802,7 +829,7 @@ function checkShell(command, { repo, platform = process.platform } = {}) {
|
|
|
802
829
|
/**
|
|
803
830
|
* Qué puede hacer el agente cuando corre por ACP. Cada `session/request_permission` pasa por acá: no hay nadie
|
|
804
831
|
* mirando la terminal para aprobar, así que la política es una allowlist (leer, comandos de solo lectura y escribir
|
|
805
|
-
*
|
|
832
|
+
* el JSON de salida en `.diff-review/`) y todo lo demás se rechaza.
|
|
806
833
|
*
|
|
807
834
|
* Ojo: el agente solo pide permiso para lo que *su* configuración no aprueba sola (p. ej. Claude Code aplica la
|
|
808
835
|
* allowlist de `~/.claude/settings.json` antes de preguntar). Esto acota lo que pide, no lo que el usuario ya
|
|
@@ -827,10 +854,33 @@ function samePath(a, b, platform = process.platform) {
|
|
|
827
854
|
const nb = normalize(b);
|
|
828
855
|
return platform === "win32" ? na.toLowerCase() === nb.toLowerCase() : na === nb;
|
|
829
856
|
}
|
|
857
|
+
/**
|
|
858
|
+
* Resuelve symlinks cuando el path (o un ancestro) existe. Sin eso, un link dentro del repo
|
|
859
|
+
* puede apuntar fuera y pasar `isInside` con solo `resolve`.
|
|
860
|
+
* @param {string} path
|
|
861
|
+
*/
|
|
862
|
+
function canonicalPath(path) {
|
|
863
|
+
try {
|
|
864
|
+
return realpathSync(path);
|
|
865
|
+
} catch {
|
|
866
|
+
const parts = [];
|
|
867
|
+
let cur = normalize(path);
|
|
868
|
+
for (;;) {
|
|
869
|
+
const parent = dirname(cur);
|
|
870
|
+
if (!parent || parent === cur) return normalize(path);
|
|
871
|
+
parts.unshift(basename(cur));
|
|
872
|
+
try {
|
|
873
|
+
return resolve(realpathSync(parent), ...parts);
|
|
874
|
+
} catch {
|
|
875
|
+
cur = parent;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
830
880
|
/** @param {string} child @param {string} root */
|
|
831
881
|
function isInside(child, root, platform = process.platform) {
|
|
832
|
-
const c = normalize(child);
|
|
833
|
-
const r = normalize(root).replace(/[\\/]+$/, "");
|
|
882
|
+
const c = normalize(canonicalPath(child));
|
|
883
|
+
const r = normalize(canonicalPath(root)).replace(/[\\/]+$/, "");
|
|
834
884
|
const [cc, rr] = platform === "win32" ? [c.toLowerCase(), r.toLowerCase()] : [c, r];
|
|
835
885
|
return cc === rr || cc.startsWith(`${rr}\\`) || cc.startsWith(`${rr}/`);
|
|
836
886
|
}
|
|
@@ -861,6 +911,52 @@ function commandOf(rawInput) {
|
|
|
861
911
|
return typeof cmd === "string" ? cmd : "";
|
|
862
912
|
}
|
|
863
913
|
/**
|
|
914
|
+
* Comando de shell que la política evalúa (rawInput, backticks o título entero).
|
|
915
|
+
* Cursor a veces envuelve el script con `@…@` en el título.
|
|
916
|
+
* @param {ToolCallLike} toolCall
|
|
917
|
+
*/
|
|
918
|
+
function toolCallCommand(toolCall) {
|
|
919
|
+
const fromRaw = commandOf(toolCall.rawInput).trim();
|
|
920
|
+
if (fromRaw) return fromRaw;
|
|
921
|
+
const title = String(toolCall.title ?? "").trim();
|
|
922
|
+
const tick = /^`([\s\S]+)`$/.exec(title);
|
|
923
|
+
if (tick) return tick[1].trim();
|
|
924
|
+
const at = /^@\s*([\s\S]*?)\s*@$/.exec(title);
|
|
925
|
+
if (at) return at[1].trim();
|
|
926
|
+
return title;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Etiqueta corta del agente si el título no es el comando mismo.
|
|
930
|
+
* @param {ToolCallLike} toolCall
|
|
931
|
+
* @param {string} [command]
|
|
932
|
+
*/
|
|
933
|
+
function toolCallLabel(toolCall, command = toolCallCommand(toolCall)) {
|
|
934
|
+
const title = String(toolCall.title ?? "").trim();
|
|
935
|
+
if (!title) return "";
|
|
936
|
+
if (/^`[\s\S]+`$/.test(title) || /^@\s*[\s\S]*?\s*@$/.test(title)) return "";
|
|
937
|
+
const norm = (s) => s.replace(/\s+/g, " ").trim();
|
|
938
|
+
if (command && norm(title) === norm(command)) return "";
|
|
939
|
+
if (command && command.length >= 24 && title.includes(command.slice(0, 24))) return "";
|
|
940
|
+
return title;
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Motivo de denegación legible en la UI de permisos.
|
|
944
|
+
* @param {string} reason
|
|
945
|
+
*/
|
|
946
|
+
function humanizePolicyReason(reason) {
|
|
947
|
+
const r = String(reason || "").trim();
|
|
948
|
+
if (!r) return "La política bloqueó este pedido.";
|
|
949
|
+
let m;
|
|
950
|
+
if (m = /^(.+?) no está permitido$/.exec(r)) return `La política bloqueó «${m[1]}» (no está en la allowlist de comandos).`;
|
|
951
|
+
if (m = /^variables de entorno en el comando:\s*(.*)$/s.exec(r)) return `La política interpretó parte del comando como variable de entorno (${String(m[1] || "?").trim().slice(0, 40)}) y lo bloqueó.`;
|
|
952
|
+
if (m = /^path fuera del repo:\s*(.*)$/s.exec(r)) return `El path queda fuera del repo: ${m[1]}`;
|
|
953
|
+
if (m = /^lectura fuera del repo:\s*(.*)$/s.exec(r)) return `Lectura fuera del repo: ${m[1]}`;
|
|
954
|
+
if (m = /^opción global de git no permitida:\s*(.*)$/s.exec(r)) return `Opción de git no permitida: ${m[1]}`;
|
|
955
|
+
if (r.startsWith("git ")) return `Comando git no permitido por la política: ${r}`;
|
|
956
|
+
if (r.startsWith("solo se puede escribir") || r.includes("sin path") || r.startsWith("modo pregunta")) return r;
|
|
957
|
+
return `La política bloqueó el pedido: ${r}`;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
864
960
|
* @param {ToolCallLike} toolCall
|
|
865
961
|
* @param {PolicyContext} ctx
|
|
866
962
|
* @returns {Decision}
|
|
@@ -869,6 +965,10 @@ function decidePermission(toolCall, { repo, outputFilename, mode }) {
|
|
|
869
965
|
const kind = toolCall.kind ?? "other";
|
|
870
966
|
const paths = toolCallPaths(toolCall, repo);
|
|
871
967
|
if (kind === "read" || kind === "search") {
|
|
968
|
+
if (!paths.length) return {
|
|
969
|
+
allow: false,
|
|
970
|
+
reason: `${kind === "search" ? "búsqueda" : "lectura"} sin path`
|
|
971
|
+
};
|
|
872
972
|
const outside = paths.find((p) => !isInside(p, repo));
|
|
873
973
|
return outside ? {
|
|
874
974
|
allow: false,
|
|
@@ -883,8 +983,7 @@ function decidePermission(toolCall, { repo, outputFilename, mode }) {
|
|
|
883
983
|
reason: "pensar"
|
|
884
984
|
};
|
|
885
985
|
if (kind === "execute") {
|
|
886
|
-
const
|
|
887
|
-
const check = checkShell(commandOf(toolCall.rawInput) || (/^`([\s\S]+)`$/.exec(title)?.[1] ?? title), { repo });
|
|
986
|
+
const check = checkShell(toolCallCommand(toolCall), { repo });
|
|
888
987
|
return check.ok ? {
|
|
889
988
|
allow: true,
|
|
890
989
|
reason: "comando de solo lectura"
|
|
@@ -902,14 +1001,14 @@ function decidePermission(toolCall, { repo, outputFilename, mode }) {
|
|
|
902
1001
|
allow: false,
|
|
903
1002
|
reason: `${kind === "delete" ? "borrado" : "edición"} sin path`
|
|
904
1003
|
};
|
|
905
|
-
const
|
|
906
|
-
const outside = paths.find((p) => !
|
|
1004
|
+
const target = resolve(repo, outputRelPath(outputFilename));
|
|
1005
|
+
const outside = paths.find((p) => !samePath(canonicalPath(p), canonicalPath(target)));
|
|
907
1006
|
return outside ? {
|
|
908
1007
|
allow: false,
|
|
909
|
-
reason: `solo se puede escribir
|
|
1008
|
+
reason: `solo se puede escribir ${outputRelPath(outputFilename)} (no ${outside})`
|
|
910
1009
|
} : {
|
|
911
1010
|
allow: true,
|
|
912
|
-
reason: `escritura
|
|
1011
|
+
reason: `escritura de ${outputRelPath(outputFilename)}`
|
|
913
1012
|
};
|
|
914
1013
|
}
|
|
915
1014
|
return {
|
|
@@ -1560,7 +1659,7 @@ function formatCheckReport(file, command, result) {
|
|
|
1560
1659
|
problems || "- (unknown)"
|
|
1561
1660
|
];
|
|
1562
1661
|
if (notes) lines.push("", notes);
|
|
1563
|
-
lines.push("", "How to continue:", "- Fix every problem above in place. Keep existing meta/intent/groups/blocks/findings; only change what is wrong or missing.", "- Every diff path must appear in \"blocks\" or \"skipped\" (a skipped glob counts).", "- meta.agent and meta.model are required (product name and the exact model id you are running).", "- intent ≥40 chars; enums exactly as in the schema.", "- Human-readable strings in Spanish.", `- Re-run exactly: \`${command}\``, "- Stop only when that command prints OK.");
|
|
1662
|
+
lines.push("", "How to continue:", "- Fix every problem above in place. Keep existing meta/intent/groups/blocks/findings; only change what is wrong or missing.", "- Every diff path must appear in \"blocks\" or \"skipped\" (a skipped glob counts).", "- If a problem lists missing paths: ADD a block or a skipped entry for each exact path. Do not only bump meta.generatedAt, notes, or claim the file is already complete.", "- meta.agent and meta.model are required (product name and the exact model id you are running).", "- intent ≥40 chars; enums exactly as in the schema.", "- Human-readable strings in Spanish.", `- Re-run exactly: \`${command}\``, "- Stop only when that command prints OK.");
|
|
1564
1663
|
return lines.join("\n");
|
|
1565
1664
|
}
|
|
1566
1665
|
//#endregion
|
|
@@ -1670,7 +1769,7 @@ var RULES = `- Ids: themes use \`t1\`, \`t2\`, …; findings use \`f1\`, \`f2\`,
|
|
|
1670
1769
|
/** @param {{ hasDiff?: boolean }} [ctx] */
|
|
1671
1770
|
function workflowLocal(base, branch, ctx = {}) {
|
|
1672
1771
|
if (ctx.hasDiff) return `Workflow:
|
|
1673
|
-
1.
|
|
1772
|
+
1. The host already computed the three-dot diff and wrote it to disk (see "Precomputed git context"). Read that file with your file tools. Do not run \`git diff\`, \`git merge-base\`, or rebuild the patch yourself unless that file is missing.
|
|
1674
1773
|
2. Read enough surrounding code (types, callers, tests) to judge behavior, not only the hunk lines.
|
|
1675
1774
|
3. Emit the JSON file. Do not modify the repo.
|
|
1676
1775
|
4. Validate it with the command in the output section. If it prints INVALID, follow "How to continue" and re-run until it prints OK.`;
|
|
@@ -1688,7 +1787,7 @@ function validateCliCommand(filename) {
|
|
|
1688
1787
|
/** Workflow sin paso de validate por CLI (sesiones ACP: valida el server). */
|
|
1689
1788
|
function workflowLocalAcp(ctx = {}) {
|
|
1690
1789
|
if (ctx.hasDiff) return `Workflow:
|
|
1691
|
-
1.
|
|
1790
|
+
1. The host already computed the three-dot diff and wrote it to disk (see "Precomputed git context"). Read that file with your file tools. Do not run \`git diff\`, \`git merge-base\`, or rebuild the patch yourself unless that file is missing.
|
|
1692
1791
|
2. Read enough surrounding code (types, callers, tests) to judge behavior, not only the hunk lines.
|
|
1693
1792
|
3. Write the JSON with your file Write/Edit tool (never shell redirection). Do not modify other repo files. The host validates when your turn ends — do not run the validate CLI.`;
|
|
1694
1793
|
return `Workflow:
|
|
@@ -1729,11 +1828,11 @@ function outputSchemaBlock(meta) {
|
|
|
1729
1828
|
* }} ctx
|
|
1730
1829
|
*/
|
|
1731
1830
|
function formatPrecomputedContext(ctx) {
|
|
1732
|
-
const lines = ["Precomputed git context (do not rediscover):"];
|
|
1831
|
+
const lines = ["Precomputed git context (provided by the host — do not rediscover or recompute):"];
|
|
1733
1832
|
if (ctx.mergeBase) lines.push(`- Merge-base SHA: \`${ctx.mergeBase}\``);
|
|
1734
1833
|
if (ctx.branchSha) lines.push(`- Branch tip SHA: \`${ctx.branchSha}\``);
|
|
1735
1834
|
if (ctx.baseSha) lines.push(`- Base tip SHA: \`${ctx.baseSha}\``);
|
|
1736
|
-
if (ctx.diffRelPath) lines.push(`- Diff file (relative to repo root): \`${ctx.diffRelPath}\` —
|
|
1835
|
+
if (ctx.diffRelPath) lines.push(`- Diff file (relative to repo root): \`${ctx.diffRelPath}\` — Read this file for the full patch. Do not run \`git diff\` / \`git merge-base\` to rebuild it unless the file is missing.`);
|
|
1737
1836
|
const files = Array.isArray(ctx.files) ? ctx.files : [];
|
|
1738
1837
|
if (files.length) {
|
|
1739
1838
|
lines.push(`- Files in the diff (${files.length}):`);
|
|
@@ -2057,14 +2156,20 @@ function askUserPermission(entry, toolCall, decision) {
|
|
|
2057
2156
|
always: false
|
|
2058
2157
|
});
|
|
2059
2158
|
const requestId = crypto.randomUUID();
|
|
2060
|
-
const title = toolCall.title || toolCall.kind || "herramienta";
|
|
2061
2159
|
const kind = toolCall.kind || "";
|
|
2062
|
-
|
|
2063
|
-
|
|
2160
|
+
const command = toolCallCommand(toolCall);
|
|
2161
|
+
const title = toolCallLabel(toolCall, command) || (command ? "" : toolCall.title || kind || "herramienta");
|
|
2162
|
+
const reason = humanizePolicyReason(decision.reason);
|
|
2163
|
+
const base = {
|
|
2064
2164
|
requestId,
|
|
2065
2165
|
title,
|
|
2066
2166
|
kind,
|
|
2067
|
-
reason
|
|
2167
|
+
reason,
|
|
2168
|
+
command: command || void 0
|
|
2169
|
+
};
|
|
2170
|
+
emit(entry, {
|
|
2171
|
+
type: "permission",
|
|
2172
|
+
...base,
|
|
2068
2173
|
allow: false,
|
|
2069
2174
|
pending: true
|
|
2070
2175
|
});
|
|
@@ -2073,9 +2178,7 @@ function askUserPermission(entry, toolCall, decision) {
|
|
|
2073
2178
|
entry.pendingPermissions.delete(requestId);
|
|
2074
2179
|
emit(entry, {
|
|
2075
2180
|
type: "permission",
|
|
2076
|
-
|
|
2077
|
-
title,
|
|
2078
|
-
kind,
|
|
2181
|
+
...base,
|
|
2079
2182
|
reason: "tiempo agotado",
|
|
2080
2183
|
allow: false,
|
|
2081
2184
|
pending: false
|
|
@@ -2091,10 +2194,8 @@ function askUserPermission(entry, toolCall, decision) {
|
|
|
2091
2194
|
if (allow && (scope === "session" || scope === "always")) entry.sessionAllowDenied = true;
|
|
2092
2195
|
emit(entry, {
|
|
2093
2196
|
type: "permission",
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
kind,
|
|
2097
|
-
reason: permissionReasonLabel(allow, scope, decision.reason),
|
|
2197
|
+
...base,
|
|
2198
|
+
reason: permissionReasonLabel(allow, scope, reason),
|
|
2098
2199
|
allow,
|
|
2099
2200
|
pending: false,
|
|
2100
2201
|
quiet: opts?.quiet
|
|
@@ -2284,16 +2385,42 @@ function mtime(path) {
|
|
|
2284
2385
|
return 0;
|
|
2285
2386
|
}
|
|
2286
2387
|
}
|
|
2388
|
+
/** Cuántas veces el server le pide solo al agente corregir/escribir el JSON antes de cortar con Reintentar. */
|
|
2389
|
+
var OUTPUT_AUTO_RETRIES = 2;
|
|
2287
2390
|
/**
|
|
2288
2391
|
* Después de un turno que podía escribir el JSON: validarlo, estamparle la sesión y, si quedó mal, pedirle al
|
|
2289
|
-
* agente que lo corrija (
|
|
2392
|
+
* agente que lo corrija (hasta `OUTPUT_AUTO_RETRIES` veces).
|
|
2290
2393
|
*/
|
|
2394
|
+
function checkEntryReport(entry) {
|
|
2395
|
+
const path = companionPaths(entry.repo, entry.outputFilename).json;
|
|
2396
|
+
if (!existsSync(path)) return null;
|
|
2397
|
+
return {
|
|
2398
|
+
path,
|
|
2399
|
+
result: checkReportFile(path, {
|
|
2400
|
+
repo: entry.repo,
|
|
2401
|
+
branch: entry.branch,
|
|
2402
|
+
base: entry.base,
|
|
2403
|
+
...entry.diffPaths?.length ? { paths: entry.diffPaths } : {}
|
|
2404
|
+
})
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
/** Prompt cuando el JSON existe pero no pasa validación (auto-fix o Reintentar). */
|
|
2408
|
+
function invalidJsonFixPrompt(entry, path, result) {
|
|
2409
|
+
return `${formatCheckReport(path, validateCliCommand(entry.outputFilename), result)}
|
|
2410
|
+
|
|
2411
|
+
Critical: do NOT only bump meta.generatedAt or rewrite notes. Edit \`${outputRelPath(entry.outputFilename).replace(/\\/g, "/")}\` so every problem above is fixed (add blocks or skipped for each missing path). Do not claim success until coverage is complete.
|
|
2412
|
+
|
|
2413
|
+
${TOOL_RULES}
|
|
2414
|
+
|
|
2415
|
+
${SERVER_VALIDATES}`;
|
|
2416
|
+
}
|
|
2291
2417
|
function noWriteFollowUpPrompt(entry) {
|
|
2292
2418
|
const rel = outputRelPath(entry.outputFilename);
|
|
2293
2419
|
const rejected = entry.rejections.length ? `These commands were rejected by diff-review's policy and did not run:\n${entry.rejections.slice(-5).map((r) => `- ${r}`).join("\n")}\n\n` : "";
|
|
2294
2420
|
const posixRel = rel.replace(/\\/g, "/");
|
|
2421
|
+
const existing = checkEntryReport(entry);
|
|
2295
2422
|
return `${rejected}Your turn ended without writing \`${posixRel}\` in this turn (mtime must change now). The user did not stop you.
|
|
2296
|
-
|
|
2423
|
+
${existing && !existing.result.ok ? `\nThe file on disk is also INVALID — fix these when you Write/Edit (do not only bump generatedAt):\n${(existing.result.errors || []).map((e) => `- ${e}`).join("\n")}\n` : ""}
|
|
2297
2424
|
Do this now:
|
|
2298
2425
|
1. Read \`${posixRel}\` if it exists (required before Write/Edit on an existing file).
|
|
2299
2426
|
2. Edit or Write the full review JSON to that same path (forward slashes only).
|
|
@@ -2303,11 +2430,16 @@ ${TOOL_RULES}
|
|
|
2303
2430
|
|
|
2304
2431
|
${SERVER_VALIDATES}`;
|
|
2305
2432
|
}
|
|
2306
|
-
async function checkOutput(entry, since,
|
|
2433
|
+
async function checkOutput(entry, since, retriesLeft) {
|
|
2434
|
+
if (entry.cancelled) return;
|
|
2307
2435
|
const path = companionPaths(entry.repo, entry.outputFilename).json;
|
|
2308
2436
|
if (!(existsSync(path) && mtime(path) >= since)) {
|
|
2309
|
-
if (
|
|
2310
|
-
|
|
2437
|
+
if (retriesLeft > 0) {
|
|
2438
|
+
const again = await followUp(entry, noWriteFollowUpPrompt(entry), entry.rejections.length ? "Seguí: hubo comandos rechazados; escribí el JSON con la tool de archivos." : "Seguí: el JSON no se escribió en este turno.");
|
|
2439
|
+
if (entry.cancelled || again == null) return;
|
|
2440
|
+
return checkOutput(entry, again, retriesLeft - 1);
|
|
2441
|
+
}
|
|
2442
|
+
if (entry.mode === "review" && !entry.cancelled) emit(entry, {
|
|
2311
2443
|
type: "review",
|
|
2312
2444
|
ok: false,
|
|
2313
2445
|
filename: entry.outputFilename,
|
|
@@ -2341,24 +2473,33 @@ async function checkOutput(entry, since, retry) {
|
|
|
2341
2473
|
});
|
|
2342
2474
|
return;
|
|
2343
2475
|
}
|
|
2476
|
+
if (entry.cancelled) return;
|
|
2477
|
+
const canRetry = retriesLeft > 0;
|
|
2344
2478
|
emit(entry, {
|
|
2345
2479
|
type: "review",
|
|
2346
2480
|
ok: false,
|
|
2347
2481
|
filename: entry.outputFilename,
|
|
2348
2482
|
errors: result.errors,
|
|
2349
|
-
final: !
|
|
2483
|
+
final: !canRetry
|
|
2350
2484
|
});
|
|
2351
|
-
if (!
|
|
2352
|
-
|
|
2485
|
+
if (!canRetry) return;
|
|
2486
|
+
const again = await followUp(entry, invalidJsonFixPrompt(entry, path, result), "Corregí los errores de validación del JSON.");
|
|
2487
|
+
if (entry.cancelled || again == null) return;
|
|
2488
|
+
await checkOutput(entry, again, retriesLeft - 1);
|
|
2353
2489
|
}
|
|
2354
|
-
/**
|
|
2490
|
+
/**
|
|
2491
|
+
* Turno extra que pide el server (corregir el JSON, seguir después de un rechazo).
|
|
2492
|
+
* Devuelve cuándo empezó, o `null` si el usuario canceló (no hay que seguir reintentando).
|
|
2493
|
+
*/
|
|
2355
2494
|
async function followUp(entry, prompt, shown) {
|
|
2495
|
+
if (entry.cancelled) return null;
|
|
2356
2496
|
emit(entry, {
|
|
2357
2497
|
type: "user",
|
|
2358
2498
|
text: shown,
|
|
2359
2499
|
mode: "fix"
|
|
2360
2500
|
});
|
|
2361
2501
|
const acp = await ensureAcp(entry);
|
|
2502
|
+
if (entry.cancelled) return null;
|
|
2362
2503
|
const started = Date.now();
|
|
2363
2504
|
entry.rejections = [];
|
|
2364
2505
|
const { stopReason } = await acp.prompt(prompt);
|
|
@@ -2366,10 +2507,15 @@ async function followUp(entry, prompt, shown) {
|
|
|
2366
2507
|
type: "turn",
|
|
2367
2508
|
stopReason
|
|
2368
2509
|
});
|
|
2510
|
+
if (entry.cancelled || stopReason === "cancelled") {
|
|
2511
|
+
entry.cancelled = true;
|
|
2512
|
+
return null;
|
|
2513
|
+
}
|
|
2369
2514
|
return started;
|
|
2370
2515
|
}
|
|
2371
2516
|
async function runTurn(entry, text, mode, opts) {
|
|
2372
2517
|
if (entry.busy) throw new Error("El agente todavía está trabajando en el pedido anterior.");
|
|
2518
|
+
entry.cancelled = false;
|
|
2373
2519
|
entry.busy = true;
|
|
2374
2520
|
entry.mode = mode;
|
|
2375
2521
|
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
@@ -2381,6 +2527,11 @@ async function runTurn(entry, text, mode, opts) {
|
|
|
2381
2527
|
try {
|
|
2382
2528
|
if (!entry.acp) setStatus(entry, "starting", `Lanzando ${entry.agentLabel}…`);
|
|
2383
2529
|
const acp = await ensureAcp(entry, opts.model);
|
|
2530
|
+
if (entry.cancelled) {
|
|
2531
|
+
entry.busy = false;
|
|
2532
|
+
setStatus(entry, "idle", "Cancelado.");
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2384
2535
|
setStatus(entry, "working", "El agente está revisando…");
|
|
2385
2536
|
const since = Date.now();
|
|
2386
2537
|
entry.rejections = [];
|
|
@@ -2389,11 +2540,22 @@ async function runTurn(entry, text, mode, opts) {
|
|
|
2389
2540
|
type: "turn",
|
|
2390
2541
|
stopReason
|
|
2391
2542
|
});
|
|
2392
|
-
if (
|
|
2543
|
+
if (entry.cancelled || stopReason === "cancelled") {
|
|
2544
|
+
entry.cancelled = true;
|
|
2545
|
+
entry.busy = false;
|
|
2546
|
+
setStatus(entry, "idle", "Cancelado.");
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
if (mode !== "ask") await checkOutput(entry, since, OUTPUT_AUTO_RETRIES);
|
|
2393
2550
|
entry.busy = false;
|
|
2394
|
-
setStatus(entry, "idle");
|
|
2551
|
+
if (entry.cancelled) setStatus(entry, "idle", "Cancelado.");
|
|
2552
|
+
else setStatus(entry, "idle");
|
|
2395
2553
|
} catch (err) {
|
|
2396
2554
|
entry.busy = false;
|
|
2555
|
+
if (entry.cancelled) {
|
|
2556
|
+
setStatus(entry, "idle", "Cancelado.");
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2397
2559
|
const raw = err instanceof Error ? err.message : String(err);
|
|
2398
2560
|
setStatus(entry, "error", authFailureMessage(entry.agentId, entry.agentLabel, raw) || raw);
|
|
2399
2561
|
} finally {
|
|
@@ -2559,17 +2721,20 @@ function sendMessage(id, text, mode, opts) {
|
|
|
2559
2721
|
/**
|
|
2560
2722
|
* Reintento manual tras un turno de review que no dejó el JSON válido (p. ej. redirect bloqueado o JSON viejo).
|
|
2561
2723
|
* Misma sesión ACP; no recalcula el diff.
|
|
2724
|
+
* Si el JSON ya existe e inválido, reenvía los errores concretos (cobertura faltante, etc.) — no un “escribí de nuevo” genérico.
|
|
2562
2725
|
*/
|
|
2563
2726
|
function retryReview(id) {
|
|
2564
2727
|
const entry = sessions.get(String(id || ""));
|
|
2565
2728
|
if (!entry) throw new Error("La sesión del agente ya no existe. Recargá la página.");
|
|
2566
2729
|
if (entry.busy) throw new Error("El agente todavía está trabajando en el pedido anterior.");
|
|
2567
2730
|
if (entry.status === "error") throw new Error(entry.statusMessage || "La sesión está en error; iniciá una nueva review.");
|
|
2568
|
-
|
|
2731
|
+
const posixRel = outputRelPath(entry.outputFilename).replace(/\\/g, "/");
|
|
2732
|
+
const existing = checkEntryReport(entry);
|
|
2733
|
+
runTurn(entry, existing && !existing.result.ok ? invalidJsonFixPrompt(entry, existing.path, existing.result) : `Continue the code review. For \`${posixRel}\`: Read it first if it exists, then Edit/Write the full JSON (forward slashes only). Do not mkdir. Never use shell redirection.
|
|
2569
2734
|
|
|
2570
2735
|
${TOOL_RULES}
|
|
2571
2736
|
|
|
2572
|
-
${SERVER_VALIDATES}`, "review", { shown: "Reintentar: escribir el JSON de la review" });
|
|
2737
|
+
${SERVER_VALIDATES}`, "review", { shown: existing && !existing.result.ok ? "Reintentar: corregir el JSON inválido" : "Reintentar: escribir el JSON de la review" });
|
|
2573
2738
|
return info(entry);
|
|
2574
2739
|
}
|
|
2575
2740
|
/**
|
|
@@ -2654,12 +2819,19 @@ async function cancelTurn(id) {
|
|
|
2654
2819
|
const entry = sessions.get(id);
|
|
2655
2820
|
if (!entry) return;
|
|
2656
2821
|
entry.cancelled = true;
|
|
2822
|
+
for (const [requestId, pending] of [...entry.pendingPermissions]) {
|
|
2823
|
+
clearTimeout(pending.timer);
|
|
2824
|
+
entry.pendingPermissions.delete(requestId);
|
|
2825
|
+
pending.resolve(false, { quiet: true });
|
|
2826
|
+
}
|
|
2657
2827
|
if (entry.acp && entry.busy) {
|
|
2658
|
-
|
|
2828
|
+
try {
|
|
2829
|
+
await entry.acp.cancel();
|
|
2830
|
+
} catch {}
|
|
2659
2831
|
return;
|
|
2660
2832
|
}
|
|
2661
2833
|
entry.busy = false;
|
|
2662
|
-
setStatus(entry, "
|
|
2834
|
+
setStatus(entry, "idle", "Cancelado.");
|
|
2663
2835
|
}
|
|
2664
2836
|
function subscribe(id, listener) {
|
|
2665
2837
|
const entry = sessions.get(id);
|
|
@@ -2684,5 +2856,5 @@ function startPendingAgentRun() {
|
|
|
2684
2856
|
}
|
|
2685
2857
|
}
|
|
2686
2858
|
|
|
2687
|
-
export {
|
|
2688
|
-
//# sourceMappingURL=agent-sessions-
|
|
2859
|
+
export { agentModels as a, startReview as b, sendMessage as c, setPermissionTrust as d, cancelTurn as e, retryReview as f, openAgentLogin as g, loginCommand as h, subscribe as i, startPendingAgentRun as j, listAcpAgents as l, openChat as o, respondPermission as r, sessionInfo as s };
|
|
2860
|
+
//# sourceMappingURL=agent-sessions-D0WwtWgR.js.map
|