@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.
Files changed (126) hide show
  1. package/README.md +1 -1
  2. package/bin/diff-review.mjs +72 -53
  3. package/lib/acp-policy.mjs +99 -12
  4. package/lib/acp-policy.test.mjs +56 -3
  5. package/lib/acp-shell.mjs +38 -8
  6. package/lib/acp-shell.test.mjs +32 -1
  7. package/lib/agent-login.mjs +105 -105
  8. package/lib/check-report.mjs +1 -0
  9. package/lib/check-report.test.mjs +1 -0
  10. package/lib/extract-cache.test.mjs +162 -162
  11. package/lib/model-label.mjs +263 -263
  12. package/lib/model-label.test.mjs +77 -77
  13. package/lib/models-cache.mjs +75 -75
  14. package/lib/models-cache.test.mjs +50 -50
  15. package/lib/prompt.mjs +8 -6
  16. package/lib/review-prep.mjs +63 -63
  17. package/package.json +1 -1
  18. package/web/build/client/_app/immutable/assets/{0.CLQiq3Ed.css → 0.BKRG1f36.css} +1 -1
  19. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.br +0 -0
  20. package/web/build/client/_app/immutable/assets/{0.CLQiq3Ed.css.gz → 0.BKRG1f36.css.gz} +0 -0
  21. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css +1 -0
  22. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.br +0 -0
  23. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.gz +0 -0
  24. package/web/build/client/_app/immutable/assets/3.BT54aVKb.css +1 -0
  25. package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.br +0 -0
  26. package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.gz +0 -0
  27. package/web/build/client/_app/immutable/assets/storage.C8XGESwn.css +1 -0
  28. package/web/build/client/_app/immutable/assets/storage.C8XGESwn.css.br +0 -0
  29. package/web/build/client/_app/immutable/assets/storage.C8XGESwn.css.gz +0 -0
  30. package/web/build/client/_app/immutable/chunks/C0hrdiUt.js +2 -0
  31. package/web/build/client/_app/immutable/chunks/C0hrdiUt.js.br +0 -0
  32. package/web/build/client/_app/immutable/chunks/C0hrdiUt.js.gz +0 -0
  33. package/web/build/client/_app/immutable/chunks/CpeIO6nz.js +210 -0
  34. package/web/build/client/_app/immutable/chunks/CpeIO6nz.js.br +0 -0
  35. package/web/build/client/_app/immutable/chunks/CpeIO6nz.js.gz +0 -0
  36. package/web/build/client/_app/immutable/chunks/{CPyWH418.js → FOc6rgIW.js} +1 -1
  37. package/web/build/client/_app/immutable/chunks/FOc6rgIW.js.br +2 -0
  38. package/web/build/client/_app/immutable/chunks/FOc6rgIW.js.gz +0 -0
  39. package/web/build/client/_app/immutable/entry/{app.BiBohpyO.js → app.DcAtSJ3n.js} +2 -2
  40. package/web/build/client/_app/immutable/entry/app.DcAtSJ3n.js.br +0 -0
  41. package/web/build/client/_app/immutable/entry/app.DcAtSJ3n.js.gz +0 -0
  42. package/web/build/client/_app/immutable/entry/start.CO5l0Zbx.js +1 -0
  43. package/web/build/client/_app/immutable/entry/start.CO5l0Zbx.js.br +0 -0
  44. package/web/build/client/_app/immutable/entry/start.CO5l0Zbx.js.gz +0 -0
  45. package/web/build/client/_app/immutable/nodes/{1.BzPXaJJj.js → 1.E-Hs7ura.js} +1 -1
  46. package/web/build/client/_app/immutable/nodes/1.E-Hs7ura.js.br +4 -0
  47. package/web/build/client/_app/immutable/nodes/1.E-Hs7ura.js.gz +0 -0
  48. package/web/build/client/_app/immutable/nodes/2.s1_irV1Q.js +10 -0
  49. package/web/build/client/_app/immutable/nodes/2.s1_irV1Q.js.br +0 -0
  50. package/web/build/client/_app/immutable/nodes/2.s1_irV1Q.js.gz +0 -0
  51. package/web/build/client/_app/immutable/nodes/3.pgwe-_5b.js +171 -0
  52. package/web/build/client/_app/immutable/nodes/3.pgwe-_5b.js.br +0 -0
  53. package/web/build/client/_app/immutable/nodes/3.pgwe-_5b.js.gz +0 -0
  54. package/web/build/client/_app/version.json +1 -1
  55. package/web/build/client/_app/version.json.br +0 -0
  56. package/web/build/client/_app/version.json.gz +0 -0
  57. package/web/build/handler.js +3 -3
  58. package/web/build/index.js +3 -3
  59. package/web/build/server/chunks/{0-BYok4D0n.js → 0-DUStDOsr.js} +3 -3
  60. package/web/build/server/chunks/{0-BYok4D0n.js.map → 0-DUStDOsr.js.map} +1 -1
  61. package/web/build/server/chunks/1-BFhhV5JB.js +7 -0
  62. package/web/build/server/chunks/{1-C7zcB0q0.js.map → 1-BFhhV5JB.js.map} +1 -1
  63. package/web/build/server/chunks/2-BZ_MVqj8.js +7 -0
  64. package/web/build/server/chunks/2-BZ_MVqj8.js.map +1 -0
  65. package/web/build/server/chunks/3-BV-TIUp1.js +7 -0
  66. package/web/build/server/chunks/{3-DAAmG6VX.js.map → 3-BV-TIUp1.js.map} +1 -1
  67. package/web/build/server/chunks/{_server.ts-qYVrMYPB.js → _server.ts-CdFoXw36.js} +4 -4
  68. package/web/build/server/chunks/{_server.ts-qYVrMYPB.js.map → _server.ts-CdFoXw36.js.map} +1 -1
  69. package/web/build/server/chunks/{agent-sessions-DSqGavSn.js → agent-sessions-D0WwtWgR.js} +223 -51
  70. package/web/build/server/chunks/agent-sessions-D0WwtWgR.js.map +1 -0
  71. package/web/build/server/chunks/{git-review-CpO_G8z8.js → git-review-CgcBIbvM.js} +7 -7
  72. package/web/build/server/chunks/{git-review-CpO_G8z8.js.map → git-review-CgcBIbvM.js.map} +1 -1
  73. package/web/build/server/chunks/{handler-Di1wJQuU.js → handler-DbxjtK9M.js} +3 -3
  74. package/web/build/server/chunks/{handler-Di1wJQuU.js.map → handler-DbxjtK9M.js.map} +1 -1
  75. package/web/build/server/chunks/{hooks.server-BLOHobEK.js → hooks.server-Dg6gYUqs.js} +3 -3
  76. package/web/build/server/chunks/{hooks.server-BLOHobEK.js.map → hooks.server-Dg6gYUqs.js.map} +1 -1
  77. package/web/build/server/chunks/{index.js-yLYmrAWy.js → index.js-NNne-njN.js} +876 -879
  78. package/web/build/server/chunks/index.js-NNne-njN.js.map +1 -0
  79. package/web/build/server/chunks/{manifest.js-CXj8IIo3.js → manifest.js-V8ViI5bl.js} +9 -9
  80. package/web/build/server/chunks/{manifest.js-CXj8IIo3.js.map → manifest.js-V8ViI5bl.js.map} +1 -1
  81. package/web/build/server/chunks/{remote-1g4ayag-BiJeU0Pk.js → remote-1g4ayag-CCsG2QXw.js} +4 -4
  82. package/web/build/server/chunks/{remote-1g4ayag-BiJeU0Pk.js.map → remote-1g4ayag-CCsG2QXw.js.map} +1 -1
  83. package/web/build/server/chunks/{remote-wh6t6j-CHanXurm.js → remote-wh6t6j-BCvmX37z.js} +5 -5
  84. package/web/build/server/chunks/{remote-wh6t6j-CHanXurm.js.map → remote-wh6t6j-BCvmX37z.js.map} +1 -1
  85. package/web/build/server/chunks/{validate-BsTKyhsq.js → validate-CAQ31C7k.js} +2 -2
  86. package/web/build/server/chunks/{validate-BsTKyhsq.js.map → validate-CAQ31C7k.js.map} +1 -1
  87. package/web/build/client/_app/immutable/assets/0.CLQiq3Ed.css.br +0 -0
  88. package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css +0 -1
  89. package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.br +0 -0
  90. package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.gz +0 -0
  91. package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css +0 -1
  92. package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.br +0 -0
  93. package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.gz +0 -0
  94. package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css +0 -1
  95. package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.br +0 -0
  96. package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.gz +0 -0
  97. package/web/build/client/_app/immutable/chunks/B5ByWcPE.js +0 -2
  98. package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.br +0 -0
  99. package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.gz +0 -0
  100. package/web/build/client/_app/immutable/chunks/CPyWH418.js.br +0 -0
  101. package/web/build/client/_app/immutable/chunks/CPyWH418.js.gz +0 -0
  102. package/web/build/client/_app/immutable/chunks/NwkMsx0h.js +0 -210
  103. package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.br +0 -0
  104. package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.gz +0 -0
  105. package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.br +0 -0
  106. package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.gz +0 -0
  107. package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js +0 -1
  108. package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.br +0 -0
  109. package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.gz +0 -0
  110. package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.br +0 -2
  111. package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.gz +0 -0
  112. package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js +0 -10
  113. package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.br +0 -0
  114. package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.gz +0 -0
  115. package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js +0 -171
  116. package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js.br +0 -0
  117. package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js.gz +0 -0
  118. package/web/build/server/chunks/1-C7zcB0q0.js +0 -7
  119. package/web/build/server/chunks/2-v6ejI_0-.js +0 -7
  120. package/web/build/server/chunks/2-v6ejI_0-.js.map +0 -1
  121. package/web/build/server/chunks/3-DAAmG6VX.js +0 -7
  122. package/web/build/server/chunks/agent-sessions-DSqGavSn.js.map +0 -1
  123. package/web/build/server/chunks/index.js-yLYmrAWy.js.map +0 -1
  124. /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js → 0.Dj97JW46.js} +0 -0
  125. /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js.br → 0.Dj97JW46.js.br} +0 -0
  126. /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js.gz → 0.Dj97JW46.js.gz} +0 -0
@@ -1,5 +1,5 @@
1
- import { g 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, h as companionPaths, d as decodePayload, t as tryResolveGitRoot, j as loadBranchDiff, y as validateAgainst, z as reviewIssues, R as REVIEW_SCHEMA, A as globMatch, C as writeDiffText } from './validate-BsTKyhsq.js';
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
- /** `/c/Users/...` (Git Bash) → `C:/Users/...` en Windows. */
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 = normalize(resolve(ctx.repo, ".diff-review")).replace(/[\\/]+$/, "");
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
- const abs = normalize(isAbsolute(native) ? native : resolve(ctx.repo, native)).replace(/[\\/]+$/, "");
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
- * dentro de `.diff-review/`) y todo lo demás se rechaza.
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 title = String(toolCall.title ?? "").trim();
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 reviewRoot = resolve(repo, OUTPUT_DIR);
906
- const outside = paths.find((p) => !isInside(p, reviewRoot) && !samePath(p, reviewRoot));
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 en ${OUTPUT_DIR}/ (no ${outside})`
1008
+ reason: `solo se puede escribir ${outputRelPath(outputFilename)} (no ${outside})`
910
1009
  } : {
911
1010
  allow: true,
912
- reason: `escritura en ${OUTPUT_DIR}/`
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. Read the three-dot diff from the path given in "Precomputed git context" below. Do not re-run \`git diff\` unless that file is missing.
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. Read the three-dot diff from the path given in "Precomputed git context" below. Do not re-run \`git diff\` unless that file is missing.
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}\` — read this file; do not re-run \`git diff\` unless it is missing.`);
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
- emit(entry, {
2063
- type: "permission",
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: decision.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
- requestId,
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
- requestId,
2095
- title,
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 (una vez).
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, retry) {
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 (retry) return checkOutput(entry, 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."), false);
2310
- if (entry.mode === "review") emit(entry, {
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: !retry
2483
+ final: !canRetry
2350
2484
  });
2351
- if (!retry) return;
2352
- await checkOutput(entry, await followUp(entry, `${formatCheckReport(path, validateCliCommand(entry.outputFilename), result)}\n\n${SERVER_VALIDATES}`, "Corregí los errores de validación del JSON."), false);
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
- /** Turno extra que pide el server (corregir el JSON, seguir después de un rechazo). Devuelve cuándo empezó. */
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 (mode !== "ask" && stopReason !== "cancelled") await checkOutput(entry, since, true);
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
- runTurn(entry, `Continue the code review. For \`${outputRelPath(entry.outputFilename).replace(/\\/g, "/")}\`: Read it first if it exists, then Edit/Write the full JSON (forward slashes only). Do not mkdir. Never use shell redirection.
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
- await entry.acp.cancel();
2828
+ try {
2829
+ await entry.acp.cancel();
2830
+ } catch {}
2659
2831
  return;
2660
2832
  }
2661
2833
  entry.busy = false;
2662
- setStatus(entry, "error", "Cancelado.");
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 { sendMessage as a, setPermissionTrust as b, cancelTurn as c, retryReview as d, openAgentLogin as e, listAcpAgents as f, agentModels as g, sessionInfo as h, subscribe as i, startPendingAgentRun as j, loginCommand as l, openChat as o, respondPermission as r, startReview as s };
2688
- //# sourceMappingURL=agent-sessions-DSqGavSn.js.map
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