dsh-auto-review 0.13.4 → 0.13.5

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 (192) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +0 -0
  3. package/README-es.md +1 -1
  4. package/README-hi.md +4 -2
  5. package/README-pt.md +1 -1
  6. package/README-zh.md +1 -1
  7. package/README.md +1 -1
  8. package/bin/dsh-auto-review-mcp.mjs +0 -0
  9. package/bin/dsh-eval.mjs +0 -0
  10. package/cordis.patch.yml +0 -0
  11. package/eval/cases/demo.yaml +0 -0
  12. package/eval/cordis.yml +0 -0
  13. package/lib/client.js +0 -0
  14. package/lib/client.js.map +0 -0
  15. package/lib/eval-cli.js +0 -0
  16. package/lib/eval.js +0 -0
  17. package/lib/index.js +190 -15
  18. package/lib/invariant.js +0 -0
  19. package/lib/mcp.js +0 -0
  20. package/lib/report-Df29db58.js +0 -0
  21. package/lib/session-message-emzsbx_9.js +0 -0
  22. package/lib/types/audit.d.ts +0 -0
  23. package/lib/types/audit.d.ts.map +0 -0
  24. package/lib/types/audit.js +0 -0
  25. package/lib/types/audit.js.map +0 -0
  26. package/lib/types/cache.d.ts +0 -0
  27. package/lib/types/cache.d.ts.map +0 -0
  28. package/lib/types/cache.js +0 -0
  29. package/lib/types/cache.js.map +0 -0
  30. package/lib/types/call-id.d.ts +0 -0
  31. package/lib/types/call-id.d.ts.map +0 -0
  32. package/lib/types/call-id.js +0 -0
  33. package/lib/types/call-id.js.map +0 -0
  34. package/lib/types/client/ReviewPanel.d.ts +0 -0
  35. package/lib/types/client/ReviewPanel.d.ts.map +0 -0
  36. package/lib/types/client/ReviewPanel.js +0 -0
  37. package/lib/types/client/ReviewPanel.js.map +0 -0
  38. package/lib/types/client/index.d.ts +0 -0
  39. package/lib/types/client/index.d.ts.map +0 -0
  40. package/lib/types/client/index.js +0 -0
  41. package/lib/types/client/index.js.map +0 -0
  42. package/lib/types/client/locales.d.ts +0 -0
  43. package/lib/types/client/locales.d.ts.map +0 -0
  44. package/lib/types/client/locales.js +0 -0
  45. package/lib/types/client/locales.js.map +0 -0
  46. package/lib/types/client/present.d.ts +0 -0
  47. package/lib/types/client/present.d.ts.map +0 -0
  48. package/lib/types/client/present.js +0 -0
  49. package/lib/types/client/present.js.map +0 -0
  50. package/lib/types/client/styles.d.ts +0 -0
  51. package/lib/types/client/styles.d.ts.map +0 -0
  52. package/lib/types/client/styles.js +0 -0
  53. package/lib/types/client/styles.js.map +0 -0
  54. package/lib/types/config.d.ts +0 -0
  55. package/lib/types/config.d.ts.map +0 -0
  56. package/lib/types/config.js +0 -0
  57. package/lib/types/config.js.map +0 -0
  58. package/lib/types/eval/assert.d.ts +0 -0
  59. package/lib/types/eval/assert.d.ts.map +0 -0
  60. package/lib/types/eval/assert.js +0 -0
  61. package/lib/types/eval/assert.js.map +0 -0
  62. package/lib/types/eval/cli.d.ts +0 -0
  63. package/lib/types/eval/cli.d.ts.map +0 -0
  64. package/lib/types/eval/cli.js +0 -0
  65. package/lib/types/eval/cli.js.map +0 -0
  66. package/lib/types/eval/diff.d.ts +0 -0
  67. package/lib/types/eval/diff.d.ts.map +0 -0
  68. package/lib/types/eval/diff.js +0 -0
  69. package/lib/types/eval/diff.js.map +0 -0
  70. package/lib/types/eval/dsl.d.ts +0 -0
  71. package/lib/types/eval/dsl.d.ts.map +0 -0
  72. package/lib/types/eval/dsl.js +0 -0
  73. package/lib/types/eval/dsl.js.map +0 -0
  74. package/lib/types/eval/index.d.ts +0 -0
  75. package/lib/types/eval/index.d.ts.map +0 -0
  76. package/lib/types/eval/index.js +0 -0
  77. package/lib/types/eval/index.js.map +0 -0
  78. package/lib/types/eval/report.d.ts +0 -0
  79. package/lib/types/eval/report.d.ts.map +0 -0
  80. package/lib/types/eval/report.js +0 -0
  81. package/lib/types/eval/report.js.map +0 -0
  82. package/lib/types/eval/review.d.ts +0 -0
  83. package/lib/types/eval/review.d.ts.map +0 -0
  84. package/lib/types/eval/review.js +0 -0
  85. package/lib/types/eval/review.js.map +0 -0
  86. package/lib/types/eval/runner.d.ts +0 -0
  87. package/lib/types/eval/runner.d.ts.map +0 -0
  88. package/lib/types/eval/runner.js +0 -0
  89. package/lib/types/eval/runner.js.map +0 -0
  90. package/lib/types/eval/stress.d.ts +0 -0
  91. package/lib/types/eval/stress.d.ts.map +0 -0
  92. package/lib/types/eval/stress.js +0 -0
  93. package/lib/types/eval/stress.js.map +0 -0
  94. package/lib/types/eval/trace.d.ts +0 -0
  95. package/lib/types/eval/trace.d.ts.map +0 -0
  96. package/lib/types/eval/trace.js +0 -0
  97. package/lib/types/eval/trace.js.map +0 -0
  98. package/lib/types/events.d.ts +0 -0
  99. package/lib/types/events.d.ts.map +0 -0
  100. package/lib/types/events.js +0 -0
  101. package/lib/types/events.js.map +0 -0
  102. package/lib/types/index.d.ts +0 -0
  103. package/lib/types/index.d.ts.map +0 -0
  104. package/lib/types/index.js +0 -0
  105. package/lib/types/index.js.map +0 -0
  106. package/lib/types/invariant.d.ts +0 -0
  107. package/lib/types/invariant.d.ts.map +0 -0
  108. package/lib/types/invariant.js +0 -0
  109. package/lib/types/invariant.js.map +0 -0
  110. package/lib/types/isolation.d.ts +0 -0
  111. package/lib/types/isolation.d.ts.map +0 -0
  112. package/lib/types/isolation.js +0 -0
  113. package/lib/types/isolation.js.map +0 -0
  114. package/lib/types/mcp/index.d.ts +0 -0
  115. package/lib/types/mcp/index.d.ts.map +0 -0
  116. package/lib/types/mcp/index.js +0 -0
  117. package/lib/types/mcp/index.js.map +0 -0
  118. package/lib/types/mcp/server.d.ts +0 -0
  119. package/lib/types/mcp/server.d.ts.map +0 -0
  120. package/lib/types/mcp/server.js +0 -0
  121. package/lib/types/mcp/server.js.map +0 -0
  122. package/lib/types/mcp/standalone.d.ts +0 -0
  123. package/lib/types/mcp/standalone.d.ts.map +0 -0
  124. package/lib/types/mcp/standalone.js +0 -0
  125. package/lib/types/mcp/standalone.js.map +0 -0
  126. package/lib/types/message-source.d.ts +0 -0
  127. package/lib/types/message-source.d.ts.map +0 -0
  128. package/lib/types/message-source.js +0 -0
  129. package/lib/types/message-source.js.map +0 -0
  130. package/lib/types/messages.d.ts +0 -0
  131. package/lib/types/messages.d.ts.map +0 -0
  132. package/lib/types/messages.js +0 -0
  133. package/lib/types/messages.js.map +0 -0
  134. package/lib/types/projection-types.d.ts +9 -1
  135. package/lib/types/projection-types.d.ts.map +1 -1
  136. package/lib/types/projection-types.js +0 -0
  137. package/lib/types/projection-types.js.map +0 -0
  138. package/lib/types/projection.d.ts +22 -2
  139. package/lib/types/projection.d.ts.map +1 -1
  140. package/lib/types/projection.js +27 -7
  141. package/lib/types/projection.js.map +1 -1
  142. package/lib/types/review.d.ts +12 -4
  143. package/lib/types/review.d.ts.map +1 -1
  144. package/lib/types/review.js +51 -6
  145. package/lib/types/review.js.map +1 -1
  146. package/lib/types/runtime.d.ts +40 -1
  147. package/lib/types/runtime.d.ts.map +1 -1
  148. package/lib/types/runtime.js +134 -4
  149. package/lib/types/runtime.js.map +1 -1
  150. package/lib/types/session-events.d.ts +0 -0
  151. package/lib/types/session-events.d.ts.map +0 -0
  152. package/lib/types/session-events.js +0 -0
  153. package/lib/types/session-events.js.map +0 -0
  154. package/lib/types/session-message.d.ts +0 -0
  155. package/lib/types/session-message.d.ts.map +0 -0
  156. package/lib/types/session-message.js +0 -0
  157. package/lib/types/session-message.js.map +0 -0
  158. package/package.json +1 -1
  159. package/src/audit.ts +0 -0
  160. package/src/cache.ts +0 -0
  161. package/src/call-id.ts +0 -0
  162. package/src/client/ReviewPanel.tsx +0 -0
  163. package/src/client/index.ts +0 -0
  164. package/src/client/locales.ts +0 -0
  165. package/src/client/present.ts +0 -0
  166. package/src/client/styles.ts +0 -0
  167. package/src/config.ts +0 -0
  168. package/src/eval/assert.ts +0 -0
  169. package/src/eval/cli.ts +0 -0
  170. package/src/eval/diff.ts +0 -0
  171. package/src/eval/dsl.ts +0 -0
  172. package/src/eval/index.ts +0 -0
  173. package/src/eval/report.ts +0 -0
  174. package/src/eval/review.ts +0 -0
  175. package/src/eval/runner.ts +0 -0
  176. package/src/eval/stress.ts +0 -0
  177. package/src/eval/trace.ts +0 -0
  178. package/src/events.ts +0 -0
  179. package/src/index.ts +0 -0
  180. package/src/invariant.ts +0 -0
  181. package/src/isolation.ts +0 -0
  182. package/src/mcp/index.ts +0 -0
  183. package/src/mcp/server.ts +0 -0
  184. package/src/mcp/standalone.ts +0 -0
  185. package/src/message-source.ts +0 -0
  186. package/src/messages.ts +0 -0
  187. package/src/projection-types.ts +9 -1
  188. package/src/projection.ts +30 -7
  189. package/src/review.ts +53 -6
  190. package/src/runtime.ts +154 -4
  191. package/src/session-events.ts +0 -0
  192. package/src/session-message.ts +0 -0
package/CHANGELOG.md CHANGED
@@ -46,6 +46,18 @@
46
46
 
47
47
  All notable changes to `dsh-auto-review` are documented here. The repo is pre-release; versions follow the DeepSeek Harness `0.1.0-rc.x` target runtime and bump on every behavior change.
48
48
 
49
+ ## [0.13.5] - 2026-10-18
50
+
51
+ ### Fixed
52
+
53
+ - **A secret spelled inside a free-form string argument reached the reviewer verbatim.** `sanitizeArguments` redacts by object KEY, so it sees `{"apiKey": "..."}` but not `deploy --token sk-live-123` or `curl -H "Authorization: Bearer …"` — which is how a secret usually appears in a shell call. Both rendering paths are covered now: the pending call's own preview (`sanitizeArguments`) and the compact transcript line (`contextLine`), the second being the one that reached the reviewer intact. Known limit, pinned by a test: a bare secret with no key adjacent to it (`echo sk-live-123`) stays visible — this is a heuristic, not a secret scanner.
54
+ - **Truncated evidence read as complete evidence.** The pending call's preview is cut at `reasonMaxChars` (default 2000) behind a bare `…`, so a reviewer shown only the benign head of a long call could allow it with the decisive arguments cut off. A truncated preview is now labelled `TRUNCATED`, states the exact counts (`only the first <n> of <m> characters are shown; the remainder is NOT visible to you`), and the verdict rules gained the matching `- truncated evidence counts as insufficient: when the arguments are marked TRUNCATED, DENY.`
55
+ - **The review panel showed all zeros on every host that declines the audit events.** The `autoReview` projection folds the plugin's own `autoReview/*` session events, and on a host whose `Session.append` cannot stamp the `ignorable` marker none of them is ever written — which is every published line — so the panel folded an empty log while the in-memory mirror held the real numbers. The persisted state now carries the owning `sessionId` (state version 4) and the runtime registers a live wire view that serves the switch state and the session's activity from the mirror; on a marker-aware host the pure fold stays authoritative and is returned untouched.
56
+
57
+ ### Added
58
+
59
+ - `test/hardening.spec.ts` — a regression case per fix above, including the documented bare-secret limit. `test/projection.spec.ts` covers the state-version-4 contract and the substituted panel view.
60
+
49
61
  ## [0.12.17] - 2026-10-10
50
62
 
51
63
  ### Fixed
package/LICENSE CHANGED
File without changes
package/README-es.md CHANGED
@@ -360,7 +360,7 @@ El servidor es de solo lectura y determinista: sin red, sin modelo, sin escritur
360
360
  - **Cierre en fallo.** Cada camino anómalo (proveedor ausente, carencias de capacidad, rechazo al iniciar, timeout, razón de parada no `completed`, veredicto ausente/malformado, fallo de correlación de auditoría) se resuelve con `fallbackPolicy`, por defecto `rejected` — y el rechazo devuelve una razón auditable al modelo. `allow-once` concede incondicionalmente; existe solo para despliegues desatendidos cuyo administrador acepta ese riesgo.
361
361
  - **Revisor de solo lectura.** La lista blanca `toolFilter` del revisor (`read`/`glob`/`grep`) no puede escribir, editar, ejecutar bash, acceder a la red ni delegar (`maxDepth` = su propia profundidad). Su registro de sesión se persiste y es auditable.
362
362
  - **Revisor aislado del contexto.** Los pasos del hijo revisor se filtran en la costura oficial `agent/pre-step`: solo entran su propio prompt y los resultados de sus propias herramientas de solo lectura. Los archivos de instrucciones del espacio de trabajo (`AGENTS.md` / `CLAUDE.md`) y los plugins que inyectan contexto se descartan en esa costura; **la instantánea de contexto de ejecución del propio harness se re-admite después de `agent/pre-step`, el firewall no la elimina y el revisor la ve.** Esto vale con CUALQUIERA de los proveedores de subagente — esos productores inyectan de nuevo en cada sesión de agente, así que lo que los cierra es el filtro y no la elección de proveedor. La lista blanca es por ORIGEN del mensaje, así que un plugin que declare un origen nuevo también se descarta.
363
- - **Los argumentos sensibles se redactan** (coincidencia por nombre de clave: `token`, `password`, `api_key`, `Authorization`, credenciales, claves privadas …) antes de entrar en el prompt del revisor; el plugin nunca ejecuta los argumentos revisados. La redacción se basa en claves, no en contenido — no revises con IA herramientas cuyos valores de argumento no te puedas permitir mostrar a un modelo.
363
+ - **Los argumentos sensibles se redactan** (coincidencia por nombre de clave: `token`, `password`, `api_key`, `Authorization`, credenciales, claves privadas …) antes de entrar en el prompt del revisor, y un argumento de texto libre pasa además por una heurística en línea que enmascara las formas `KEY=VALUE` / `KEY: VALUE` / `--flag VALUE` y los esquemas `Bearer`/`Basic` — que es como suele aparecer un secreto en una llamada de shell. El plugin nunca ejecuta los argumentos revisados. Ambas pasadas son de mejor esfuerzo, no un escáner de secretos: un secreto sin clave al lado sigue siendo visible, así que no revises con IA herramientas cuyos valores de argumento no te puedas permitir mostrar a un modelo.
364
364
  - **Las desactivaciones duras se explican a sí mismas.** Una herramienta o regla de riesgo `never` rechaza de forma determinista Y registra un evento solo-registro `autoReview/rejection`, y luego inyecta un marcador `[auto-review-never]` en el resultado de la herramienta denegada — el modelo aprende que la acción está duramente desactivada en lugar de reintentarla (verificado por invariant: marcador ⟺ evento).
365
365
  - **Disyuntor de rechazos.** Una racha de denegaciones en un turno dispara el disyuntor (`consecutiveDenies` / `windowDenies` dentro de `windowSize`), registrada como un evento solo-registro `autoReview/circuit`; las solicitudes posteriores siguen su `action` (`delegate` / `reject` / `abort-turn`).
366
366
  - **El contexto del revisor es transcripción ya presentada.** `contextBudget` alimenta al revisor con contenido de sesión ya presentado. Con el modelo revisor de la misma ruta por defecto, ese contenido permanece dentro de un proveedor; configura `reviewerModel` a un proveedor diferente solo si aceptas presentarle esa transcripción.
package/README-hi.md CHANGED
@@ -392,7 +392,9 @@ dsh-eval eval/cases --model deepseek-flash --timeout-ms 240000 --out .eval-repor
392
392
 
393
393
  </details>
394
394
 
395
- </details>## MCP सर्वर (स्टैंडअलोन)
395
+ </details>
396
+
397
+ ## MCP सर्वर (स्टैंडअलोन)
396
398
 
397
399
  `dsh-auto-review` एक stdio **MCP सर्वर** (`dsh-auto-review-mcp`) भी देता है ताकि बाहरी MCP क्लाइंट (Claude, Codex, …) बिना harness के एक निर्धारक समीक्षा पथ का उपयोग कर सकें। यह newline-delimited JSON (NDJSON) पर JSON-RPC 2.0 बोलता है — प्रति पंक्ति एक JSON ऑब्जेक्ट, कोई `Content-Length` फ़्रेमिंग नहीं।
398
400
 
@@ -447,7 +449,7 @@ Claude Desktop (`claude_desktop_config.json`) उदाहरण:
447
449
  - **विफल-बंद।** हर असामान्य मार्ग (प्रदाता अनुपस्थित, क्षमता अंतराल, प्रारंभ अस्वीकृति, टाइमआउट, गैर-`completed` स्टॉप कारण, अनुपस्थित/विकृत निर्णय, ऑडिट-सहसंबंध विफलता) `fallbackPolicy` से हल होता है, डिफ़ॉल्ट `rejected` — और अस्वीकृति मॉडल को एक ऑडिट-योग्य कारण लौटाती है। `allow-once` बिना शर्त अनुमति देता है; यह केवल उन अप्रबंधित परिनियोजनों के लिए है जिनका व्यवस्थापक उस जोखिम को स्वीकार करता है।
448
450
  - **केवल-पढ़ने वाला समीक्षक।** समीक्षक की `toolFilter` अनुमति-सूची (`read`/`glob`/`grep`) लिख, संपादित, bash चला, नेटवर्क उपयोग या प्रत्यायोजन नहीं कर सकती (`maxDepth` = उसकी अपनी गहराई)। उसका सत्र लॉग स्थायी और ऑडिट-योग्य है।
449
451
  - **संदर्भ-पृथक समीक्षक।** समीक्षक चाइल्ड के हर चरण को आधिकारिक `agent/pre-step` सीम पर छाना जाता है: केवल उसका अपना प्रॉम्प्ट और उसके अपने केवल-पढ़ने वाले टूल परिणाम ही भीतर आते हैं। वर्कस्पेस निर्देश फ़ाइलें (`AGENTS.md` / `CLAUDE.md`), हार्नेस का रनटाइम-संदर्भ स्नैपशॉट और संदर्भ इंजेक्ट करने वाला कोई भी प्लगइन लूप द्वारा जोड़े जाने से पहले हटा दिए जाते हैं, इसलिए रिपॉज़िटरी-नियंत्रित पाठ कभी उस घटक तक नहीं पहुँचता जो कॉल की अनुमति तय करता है। यह किसी भी subagent प्रदाता के साथ लागू होता है — ये उत्पादक हर नए एजेंट सत्र में नए सिरे से इंजेक्ट करते हैं, इसलिए इन्हें बंद करने वाला यही फ़िल्टर है, प्रदाता का चुनाव नहीं। अनुमति-सूची संदेश के स्रोत पर आधारित है, इसलिए नया स्रोत घोषित करने वाला प्लगइन भी हटा दिया जाता है।
450
- - **संवेदनशील तर्क संशोधित किए जाते हैं** (कुंजी-नाम मिलान: `token`, `password`, `api_key`, `Authorization`, क्रेडेंशियल, निजी कुंजियाँ …) समीक्षक प्रॉम्प्ट में जाने से पहले; प्लगइन समीक्षित तर्कों को कभी निष्पादित नहीं करता। संशोधन कुंजी-आधारित है, सामग्री-आधारित नहीं — उन टूल की AI समीक्षा न करें जिनके तर्क मान आप किसी मॉडल को दिखाने का जोखिम नहीं उठा सकते।
452
+ - **संवेदनशील तर्क संशोधित किए जाते हैं** (कुंजी-नाम मिलान: `token`, `password`, `api_key`, `Authorization`, क्रेडेंशियल, निजी कुंजियाँ …) समीक्षक प्रॉम्प्ट में जाने से पहले, और एक मुक्त-रूप स्ट्रिंग तर्क पर अतिरिक्त रूप से एक इनलाइन अनुमान लगता है जो `KEY=VALUE` / `KEY: VALUE` / `--flag VALUE` रूपों तथा `Bearer`/`Basic` स्कीमों को छिपा देता है — शेल कॉल में सीक्रेट प्रायः इसी तरह दिखता है। प्लगइन समीक्षित तर्कों को कभी निष्पादित नहीं करता। दोनों पास सर्वोत्तम-प्रयास हैं, कोई सीक्रेट स्कैनर नहीं: बिना कुंजी वाला नंगा सीक्रेट दिखता रहता है, इसलिए उन टूल की AI समीक्षा न करें जिनके तर्क मान आप किसी मॉडल को दिखाने का जोखिम नहीं उठा सकते।
451
453
  - **कठोर अक्षमताएँ स्वयं को समझाती हैं।** एक `never` टूल या जोखिम नियम निर्धारक रूप से अस्वीकार करता है और एक केवल-लॉग `autoReview/rejection` घटना दर्ज करता है, फिर अस्वीकृत टूल परिणाम में एक `[auto-review-never]` मार्कर इंजेक्ट करता है — मॉडल सीखता है कि क्रिया कठोर रूप से अक्षम है बजाय उसे पुनः प्रयास करने के (invariant-जाँचा गया: मार्कर ⟺ घटना)।
452
454
  - **अस्वीकृति सर्किट ब्रेकर।** एक टर्न में अस्वीकृतियों की एक शृंखला ब्रेकर को ट्रिप करती है (`windowSize` के भीतर `consecutiveDenies` / `windowDenies`), जो एक केवल-लॉग `autoReview/circuit` घटना के रूप में दर्ज होती है; बाद के अनुरोध उसकी `action` (`delegate` / `reject` / `abort-turn`) का पालन करते हैं।
453
455
  - **समीक्षक संदर्भ प्रस्तुत ट्रांसक्रिप्ट है।** `contextBudget` समीक्षक को पहले से प्रस्तुत सत्र सामग्री खिलाता है। डिफ़ॉल्ट समान-मार्ग समीक्षक मॉडल के साथ वह सामग्री एक ही प्रदाता के भीतर रहती है; `reviewerModel` को किसी भिन्न प्रदाता पर तभी कॉन्फ़िगर करें जब आप उस ट्रांसक्रिप्ट को उसे प्रस्तुत करना स्वीकार करते हों।
package/README-pt.md CHANGED
@@ -360,7 +360,7 @@ O servidor é somente-leitura e determinista: sem rede, sem modelo, sem gravaç
360
360
  - **Falha fechada.** Todo caminho anômalo (provedor ausente, lacunas de capacidade, rejeição ao iniciar, timeout, razão de parada não `completed`, veredito ausente/malformado, falha de correlação de auditoria) se resolve via `fallbackPolicy`, padrão `rejected` — e a rejeição devolve uma razão auditável ao modelo. `allow-once` concede incondicionalmente; existe apenas para implantações desatendidas cujo administrador aceita esse risco.
361
361
  - **Revisor somente leitura.** A lista de permissões `toolFilter` do revisor (`read`/`glob`/`grep`) não pode escrever, editar, executar bash, acessar a rede nem delegar (`maxDepth` = sua própria profundidade). Seu log de sessão é persistido e auditável.
362
362
  - **Revisor isolado do contexto.** Os passos do filho revisor são filtrados na costura oficial `agent/pre-step`: apenas o seu próprio prompt e os resultados das suas próprias ferramentas somente leitura entram. Arquivos de instruções do workspace (`AGENTS.md` / `CLAUDE.md`) e plugins que injetam contexto são descartados nessa costura; **o instantâneo de contexto de execução do próprio harness é readmitido após `agent/pre-step`, o firewall não o remove e o revisor o vê.** Isso vale sob QUALQUER provedor de subagente — esses produtores injetam de novo em cada sessão de agente, então o que os fecha é o filtro, não a escolha do provedor. A lista de permissões é por ORIGEM da mensagem, então um plugin que declare uma origem nova também é descartado.
363
- - **Argumentos sensíveis são redigidos** (correspondência por nome de chave: `token`, `password`, `api_key`, `Authorization`, credenciais, chaves privadas …) antes de entrar no prompt do revisor; o plugin nunca executa os argumentos revisados. A redação é baseada em chave, não em conteúdo — não revise com IA ferramentas cujos valores de argumento você não pode se dar ao luxo de mostrar a um modelo.
363
+ - **Argumentos sensíveis são redigidos** (correspondência por nome de chave: `token`, `password`, `api_key`, `Authorization`, credenciais, chaves privadas …) antes de entrar no prompt do revisor, e um argumento de texto livre passa ainda por uma heurística em linha que mascara as formas `KEY=VALUE` / `KEY: VALUE` / `--flag VALUE` e os esquemas `Bearer`/`Basic` — que é como um segredo costuma aparecer numa chamada de shell. O plugin nunca executa os argumentos revisados. Ambas as passadas são de melhor esforço, não um scanner de segredos: um segredo sem chave ao lado continua visível, então não revise com IA ferramentas cujos valores de argumento você não pode se dar ao luxo de mostrar a um modelo.
364
364
  - **Desativações duras se explicam.** Uma ferramenta ou regra de risco `never` rejeita de forma determinística E registra um evento somente-log `autoReview/rejection`, e então injeta um marcador `[auto-review-never]` no resultado da ferramenta negada — o modelo aprende que a ação está duramente desativada em vez de tentar novamente (verificado por invariant: marcador ⟺ evento).
365
365
  - **Disjuntor de rejeições.** Uma sequência de negações em um turno dispara o disjuntor (`consecutiveDenies` / `windowDenies` dentro de `windowSize`), registrada como um evento somente-log `autoReview/circuit`; solicitações posteriores seguem sua `action` (`delegate` / `reject` / `abort-turn`).
366
366
  - **O contexto do revisor é transcrição apresentada.** `contextBudget` alimenta o revisor com conteúdo de sessão já apresentado. Com o modelo revisor da mesma rota por padrão, esse conteúdo permanece dentro de um provedor; configure `reviewerModel` para um provedor diferente apenas se você aceitar apresentar essa transcrição a ele.
package/README-zh.md CHANGED
@@ -360,7 +360,7 @@ Claude Desktop(`claude_desktop_config.json`)配置示例:
360
360
  - **失败关闭。** 每条异常路径(provider 缺失、能力缺口、启动拒绝、超时、非 `completed` 停止原因、缺失/畸形裁决、审计关联失败)都经由 `fallbackPolicy` 处理,默认 `rejected` —— 且拒绝会向模型反馈一条可审计的理由。`allow-once` 是无条件放行;它只用于接受该风险的无值守部署。
361
361
  - **只读审查器。** 审查器的 `toolFilter` 白名单(`read`/`glob`/`grep`)无法写入、编辑、运行 bash、访问网络或委派(`maxDepth` = 自身深度)。其会话日志被持久化且可审计。
362
362
  - **上下文隔离的审查器。** 审查器子代理的每一步都在官方 `agent/pre-step` 接缝上被过滤:只有它自己的提示词与它自己的只读工具结果能够进入。工作区指令文件(`AGENTS.md` / `CLAUDE.md`)与注入上下文的插件会在该接缝处被丢弃;**宿主自己的运行时上下文快照会在 `agent/pre-step` 之后被循环重新接纳,防火墙无法移除它,因此审查器能看到该快照。**这在任一 subagent provider 下都成立——这些注入方会向任何新建的 agent 会话重新注入,因此关闭它们的是这道过滤,而不是 provider 的选择。白名单按消息来源判定,所以声明了新来源类型的插件同样会被丢弃。
363
- - **敏感参数会被脱敏**(按键名匹配:`token`、`password`、`api_key`、`Authorization`、凭据、私钥……)后才进入审查器提示词;该插件绝不会执行被审查的参数。脱敏是按键名而非内容 —— 不要把参数值经不起展示给模型的工具交给 AI 审查。
363
+ - **敏感参数会被脱敏**(按键名匹配:`token`、`password`、`api_key`、`Authorization`、凭据、私钥……)后才进入审查器提示词;自由字符串参数还会额外经过一层内联启发式,掩掉 `KEY=VALUE` / `KEY: VALUE` / `--flag VALUE` 这几种写法以及 `Bearer`/`Basic` 方案 —— 而命令行里的密钥通常正是这样出现的。该插件绝不会执行被审查的参数。两层都是尽力而为,不是密钥扫描器:旁边没有键名的裸密钥仍然可见,所以不要把参数值经不起展示给模型的工具交给 AI 审查。
364
364
  - **硬禁用会自我解释。** `never` 工具或风险规则确定性拒绝,并记录一条仅日志的 `autoReview/rejection` 事件,然后把 `[auto-review-never]` 标记注入被拒绝的工具结果 —— 模型学会该操作已被硬禁用,而不是重试(invariant 校验:标记 ⟺ 事件)。
365
365
  - **拒绝熔断器。** 一轮内连续拒绝会触发熔断(`consecutiveDenies` / 窗口内的 `windowDenies`),记录为仅日志的 `autoReview/circuit` 事件;后续请求按其 `action`(`delegate` / `reject` / `abort-turn`)处理。
366
366
  - **审查上下文是已呈现的 transcript。** `contextBudget` 把已呈现的会话内容喂给审查器。默认同路由审查模型时该内容停留在单一 provider 内;仅当你接受把该 transcript 呈现给另一 provider 时,才把 `reviewerModel` 配成别的 provider。
package/README.md CHANGED
@@ -363,7 +363,7 @@ The server is read-only and deterministic: no network, no model, no writes.
363
363
  - **Fail closed.** Every abnormal path (provider missing, capability gaps, start rejection, timeout, non-`completed` stop reason, missing/malformed verdict, audit-correlation failure) resolves through `fallbackPolicy`, default `rejected` — and the rejection feeds an auditable reason back to the model. `allow-once` grants unconditionally; it exists only for unattended deployments whose admin accepts that risk.
364
364
  - **Read-only reviewer.** The reviewer's `toolFilter` allow-list (`read`/`glob`/`grep`) cannot write, edit, run bash, fetch the network, or delegate (`maxDepth` = its own depth). Its session log is persisted and auditable.
365
365
  - **Context-isolated reviewer.** The reviewer child's steps are filtered on the documented `agent/pre-step` seam: only its own prompt and its own read-only tool results enter them. Workspace instruction files (`AGENTS.md` / `CLAUDE.md`) and context-injecting plugins are dropped from the step this seam controls. **The host's own runtime-context snapshot is re-admitted by the loop after `agent/pre-step`; the firewall does not remove it, so the reviewer sees it.** This holds under EITHER subagent provider. The filter is an allow-list over message SOURCES, so a plugin that declares a new source kind is dropped too.
366
- - **Sensitive arguments are redacted** (key-name matching: `token`, `password`, `api_key`, `Authorization`, credentials, private keys …) before entering the reviewer prompt; the plugin never executes the reviewed arguments. Redaction is key-based, not content-based — do not AI-review tools whose argument values you cannot afford to show a model.
366
+ - **Sensitive arguments are redacted** (key-name matching: `token`, `password`, `api_key`, `Authorization`, credentials, private keys …) before entering the reviewer prompt, and a free-form string argument additionally passes an inline heuristic that masks the `KEY=VALUE` / `KEY: VALUE` / `--flag VALUE` spellings and `Bearer`/`Basic` schemes — which is how a secret usually appears in a shell call. The plugin never executes the reviewed arguments. Both passes are best-effort, not a secret scanner: a bare secret with no key next to it stays visible, so do not AI-review tools whose argument values you cannot afford to show a model.
367
367
  - **Hard disables explain themselves.** A `never` tool or risk rule rejects deterministically AND records a log-only `autoReview/rejection` event, then injects a `[auto-review-never]` marker into the denied tool result — the model learns the action is hard-disabled instead of retrying it (invariant-checked: marker ⟺ event).
368
368
  - **Rejection circuit breaker.** A run of denials in one turn trips the breaker (`consecutiveDenies` / `windowDenies` inside `windowSize`), recorded as a log-only `autoReview/circuit` event; later requests follow its `action` (`delegate` / `reject` / `abort-turn`).
369
369
  - **Reviewer context is presented transcript.** `contextBudget` feeds already-presented session content to the reviewer. With the default same-route reviewer model that content stays inside one provider; configure `reviewerModel` to a different provider only if you accept presenting that transcript to it.
File without changes
package/bin/dsh-eval.mjs CHANGED
File without changes
package/cordis.patch.yml CHANGED
File without changes
File without changes
package/eval/cordis.yml CHANGED
File without changes
package/lib/client.js CHANGED
File without changes
package/lib/client.js.map CHANGED
File without changes
package/lib/eval-cli.js CHANGED
File without changes
package/lib/eval.js CHANGED
File without changes
package/lib/index.js CHANGED
@@ -5965,10 +5965,19 @@ const AUTO_REVIEW_PROJECTION_SCHEMA = object({
5965
5965
  * State for the empty log. `enabled` starts at the resolved
5966
5966
  * `enableByDefault` — the projection cannot know the plugin config on its
5967
5967
  * own, so the unit is built per mount by {@link makeAutoReviewProjection}.
5968
+ *
5969
+ * `sessionId` is part of the state since state version 4: the fold records
5970
+ * which session it belongs to so a host that cannot accept the
5971
+ * `autoReview/*` audit events can serve the panel from the live in-memory
5972
+ * mirror instead (see `AutoReviewRuntime.livePanelView`). The pure fold
5973
+ * cannot resolve a session on its own, so the id is supplied by the caller
5974
+ * that knows the log header.
5968
5975
  * @param enabledByDefault - the resolved `enableByDefault` config value.
5976
+ * @param sessionId - the owning session id, as text.
5969
5977
  */
5970
- function initAutoReviewProjection(enabledByDefault) {
5978
+ function initAutoReviewProjection(enabledByDefault, sessionId) {
5971
5979
  return {
5980
+ sessionId,
5972
5981
  enabled: enabledByDefault,
5973
5982
  turnOpen: false,
5974
5983
  verdictsUsed: 0,
@@ -6088,6 +6097,7 @@ function viewAutoReview(state) {
6088
6097
  }
6089
6098
  /** Strict state schema — validates the persisted fold state before it seeds a fold. */
6090
6099
  const AUTO_REVIEW_STATE_SCHEMA = object({
6100
+ sessionId: string(),
6091
6101
  enabled: boolean(),
6092
6102
  turnOpen: boolean(),
6093
6103
  verdictsUsed: number().int().nonnegative(),
@@ -6110,20 +6120,29 @@ const AUTO_REVIEW_STATE_SCHEMA = object({
6110
6120
  * the answerer applies. Registrants sharing the `autoReview` key share one
6111
6121
  * unit; the first mount's default wins, which only matters for a double
6112
6122
  * mount with conflicting `enableByDefault` values.
6123
+ *
6124
+ * `liveView` substitutes the wire `view`
6125
+ * with a runtime-aware one. The pure fold can only report what the session
6126
+ * log carries, and a host that refuses the unmarked `autoReview/*` events
6127
+ * writes none of them — which is why the panel used to read an empty log and
6128
+ * show all zeros. The runtime passes `AutoReviewRuntime.livePanelView`, which
6129
+ * falls back to the in-memory mirror on such a host and to the fold
6130
+ * everywhere else. Omitted (or `undefined`) keeps the pure fold.
6113
6131
  * @param enabledByDefault - the resolved `enableByDefault` config value.
6132
+ * @param liveView - an optional runtime-aware wire view.
6114
6133
  * @returns the projection unit for `ctx.sessionProjections.register`.
6115
6134
  */
6116
- function makeAutoReviewProjection(enabledByDefault) {
6135
+ function makeAutoReviewProjection(enabledByDefault, liveView) {
6117
6136
  return {
6118
6137
  key: "autoReview",
6119
6138
  stateSchema: AUTO_REVIEW_STATE_SCHEMA,
6120
- init: (_header) => initAutoReviewProjection(enabledByDefault),
6139
+ init: (header) => initAutoReviewProjection(enabledByDefault, String(header.id)),
6121
6140
  apply: applyAutoReview,
6122
6141
  wire: {
6123
6142
  viewSchema: AUTO_REVIEW_PROJECTION_SCHEMA,
6124
- view: viewAutoReview
6143
+ view: liveView === void 0 ? viewAutoReview : liveView
6125
6144
  },
6126
- stateVersion: 3
6145
+ stateVersion: 4
6127
6146
  };
6128
6147
  }
6129
6148
  //#endregion
@@ -6168,8 +6187,33 @@ const SENSITIVE_KEY_PATTERN = /(?:api[_-]?key|token|secret|password|passwd|pwd|a
6168
6187
  /** The redaction placeholder for sensitive argument values. */
6169
6188
  const REDACTED = "[REDACTED]";
6170
6189
  /**
6190
+ * Inline secrets inside a free-form STRING
6191
+ * argument (a shell command line), which the key-based pass cannot see because
6192
+ * it inspects keys only. Covers `KEY=VALUE` / `KEY: VALUE` / `--flag VALUE`
6193
+ * spellings, with an optional `Bearer`/`Basic` scheme so
6194
+ * `Authorization: Bearer <token>` masks the token and not just the scheme word.
6195
+ * Best effort by construction: a bare secret with no key next to it stays
6196
+ * visible.
6197
+ */
6198
+ const INLINE_SECRET_KEY = "(?:api[_-]?key|token|secret|password|passwd|pwd|authorization|credential|private[_-]?key|access[_-]?key)";
6199
+ /** The `KEY=VALUE` / `KEY: VALUE` spelling of an inline secret, scheme included. */
6200
+ const INLINE_SECRET_ASSIGN = new RegExp(`(${INLINE_SECRET_KEY}\\s*[:=]\\s*(?:Bearer\\s+|Basic\\s+)?)(["']?)([^\\s"';&|,)]+)`, "giu");
6201
+ /** The `--flag VALUE` spelling of an inline secret. */
6202
+ const INLINE_SECRET_FLAG = new RegExp(`(--?${INLINE_SECRET_KEY}\\s+)(["']?)([^\\s"';&|,)]+)`, "giu");
6203
+ /**
6204
+ * Redact inline secrets from a free-form string argument. The key-based pass
6205
+ * in {@link sanitizeArguments} only inspects object KEYS, so a secret spelled
6206
+ * inside a shell command line would otherwise reach the reviewer verbatim.
6207
+ * @param text - the free-form string to redact.
6208
+ * @returns the string with every recognized inline secret masked.
6209
+ */
6210
+ function redactInlineSecrets(text) {
6211
+ return text.replace(INLINE_SECRET_ASSIGN, (_match, head, quote) => head + quote + REDACTED).replace(INLINE_SECRET_FLAG, (_match, head, quote) => head + quote + REDACTED);
6212
+ }
6213
+ /**
6171
6214
  * Recursively redact sensitive values from a JSON value: any object key
6172
- * matching {@link SENSITIVE_KEY_PATTERN} has its value replaced.
6215
+ * matching {@link SENSITIVE_KEY_PATTERN} has its value replaced, and any
6216
+ * free-form string is passed through {@link redactInlineSecrets}.
6173
6217
  * @param value - the detached JSON value to sanitize.
6174
6218
  * @returns a detached copy with sensitive values redacted.
6175
6219
  */
@@ -6180,6 +6224,7 @@ function sanitizeArguments(value) {
6180
6224
  for (const [key, item] of Object.entries(value)) copy[key] = SENSITIVE_KEY_PATTERN.test(key) ? REDACTED : sanitizeArguments(item);
6181
6225
  return copy;
6182
6226
  }
6227
+ if (typeof value === "string") return redactInlineSecrets(value);
6183
6228
  return value;
6184
6229
  }
6185
6230
  /**
@@ -6208,6 +6253,12 @@ function sanitizedArgumentsText(events, callId) {
6208
6253
  /**
6209
6254
  * Render the tool-call context for the reviewer: the already-streamed call
6210
6255
  * arguments (parsed, redacted, truncated) or a note when the log lacks them.
6256
+ *
6257
+ * A truncated preview is DISCLOSED as truncated and matched with an explicit
6258
+ * deny rule (see {@link buildReviewPrompt}): previously the
6259
+ * prompt silently showed a `…`-suffixed head and left the verdict rules to
6260
+ * infer that the evidence was incomplete, so a reviewer reading the visible
6261
+ * part as benign could allow an action whose decisive arguments were cut off.
6211
6262
  * @param events - the requesting session's log.
6212
6263
  * @param callId - the call to render.
6213
6264
  * @param maxChars - the preview cap (shared with reason truncation).
@@ -6217,7 +6268,8 @@ function renderCallContext(events, callId, maxChars) {
6217
6268
  if (callId === void 0) return "Tool call arguments: (not available — the asker attached no call id)";
6218
6269
  const text = sanitizedArgumentsText(events, callId);
6219
6270
  if (text === void 0) return "Tool call arguments: (not found or unparseable in the presented transcript)";
6220
- return `Tool call arguments (sensitive values redacted):\n${truncate(text, maxChars)}`;
6271
+ if (text.length <= maxChars) return `Tool call arguments (sensitive values redacted):\n${text}`;
6272
+ return `Tool call arguments (sensitive values redacted, TRUNCATED):\n${truncate(text, maxChars)}\n[TRUNCATED EVIDENCE: only the first ${maxChars} of ${text.length} characters are shown; the remainder is NOT visible to you. An action you cannot see in full is insufficient evidence — the verdict rules require DENY on insufficient evidence. Deny unless the visible part alone proves the action safe.]`;
6221
6273
  }
6222
6274
  /** How many characters one transcript line may take before truncation. */
6223
6275
  const CONTEXT_LINE_BUDGET = 300;
@@ -6231,7 +6283,7 @@ function contextLine(event) {
6231
6283
  case "user/message": return `[user] ${truncate(contentText(event.data.content), CONTEXT_LINE_BUDGET)}`;
6232
6284
  case "assistant/message": return `[agent] ${truncate(contentText(event.data.message.content), CONTEXT_LINE_BUDGET)}`;
6233
6285
  case "tool/call": {
6234
- const args = event.data.arguments;
6286
+ const args = redactInlineSecrets(event.data.arguments);
6235
6287
  return `[tool call ${event.data.name}] ${truncate(args, CONTEXT_LINE_BUDGET)}`;
6236
6288
  }
6237
6289
  case "tool/result": {
@@ -6247,9 +6299,10 @@ function contextLine(event) {
6247
6299
  * Build the compact-transcript section for the reviewer: the tail of the
6248
6300
  * session's user/assistant messages and tool calls/results, most recent
6249
6301
  * last, bounded by {@link ContextBudgetConfig}. `turns: 0` yields an empty
6250
- * section. Tool-call arguments and tool-result text enter verbatim (they are
6251
- * already-presented transcript content); the presented call of the pending
6252
- * request is redacted separately in {@link renderCallContext}.
6302
+ * section. Tool-call arguments pass through {@link redactInlineSecrets} and
6303
+ * tool-result text enters verbatim — both are already-presented transcript
6304
+ * content, and the pending request's own call is redacted separately in
6305
+ * {@link renderCallContext}.
6253
6306
  *
6254
6307
  * The character budget is spent from the NEWEST line backwards, so an
6255
6308
  * over-budget transcript loses its oldest lines. Truncating the joined text
@@ -6357,6 +6410,7 @@ function buildReviewPrompt(request, config, override, verdictChannel = "structur
6357
6410
  "- allow: the action is safe, reversible, within the stated scope, and its reason is plausible.",
6358
6411
  "- deny: the action is destructive, irreversible, credential- or data-exfiltrating, or the",
6359
6412
  " evidence is insufficient to judge it. When unsure, DENY.",
6413
+ "- truncated evidence counts as insufficient: when the arguments are marked TRUNCATED, DENY.",
6360
6414
  "- riskLevel: low / medium / high, your overall confidence-weighted severity.",
6361
6415
  "",
6362
6416
  verdictChannel === "structured_output" ? "Report your decision by calling the structured_output tool EXACTLY ONCE with:" : "Report your decision by returning EXACTLY ONE JSON object and nothing else — no prose, no markdown fence — with:",
@@ -6679,6 +6733,8 @@ function newRejectionId() {
6679
6733
  const FEEDBACK_TTL_MS = 3e5;
6680
6734
  /** Cap on the in-memory denial feed (approve indexes are bounded by this). */
6681
6735
  const MEMORY_DENIES_CAP = 200;
6736
+ /** Cap on the recent-verdict feed the panel's tally carries. */
6737
+ const MEMORY_RECENT_CAP = 10;
6682
6738
  /** Cap on the in-memory override list (a session cannot meaningfully accumulate more). */
6683
6739
  const MEMORY_OVERRIDES_CAP = 50;
6684
6740
  /** Closed-union backstop for the fallback-policy switch. */
@@ -6733,6 +6789,13 @@ var AutoReviewRuntime = class AutoReviewRuntime {
6733
6789
  auditSupport = "unknown";
6734
6790
  /** In-memory audit mirrors per session, used when the host cannot stamp audit events. */
6735
6791
  memory = /* @__PURE__ */ new WeakMap();
6792
+ /**
6793
+ * Process-lifetime review tallies per session, which the
6794
+ * per-turn mirror cannot carry. Keyed by session so a restarted turn (which
6795
+ * rebuilds the mirror) keeps the panel's cumulative rows. A `WeakMap`, so a
6796
+ * discarded session releases its tally with it.
6797
+ */
6798
+ tallies = /* @__PURE__ */ new WeakMap();
6736
6799
  /** The one-time warning that session-log audit was disabled was already logged. */
6737
6800
  warnedUnmarked = false;
6738
6801
  /** Pending circuit-breaker abort-turn timers, cleared when the plugin unloads. */
@@ -6809,8 +6872,30 @@ var AutoReviewRuntime = class AutoReviewRuntime {
6809
6872
  };
6810
6873
  this.memory.set(session, entry);
6811
6874
  }
6875
+ if (entry.tally === void 0) entry.tally = this.tallyFor(session);
6812
6876
  return entry;
6813
6877
  }
6878
+ /**
6879
+ * The process-lifetime tally behind the panel's cumulative
6880
+ * rows, created on first use and kept for the life of the mount.
6881
+ * @param session - the session the tally belongs to.
6882
+ * @returns the session's tally, creating it when absent.
6883
+ */
6884
+ tallyFor(session) {
6885
+ let tally = this.tallies.get(session);
6886
+ if (tally === void 0) {
6887
+ tally = {
6888
+ allows: 0,
6889
+ denies: 0,
6890
+ fallbacks: 0,
6891
+ rejections: 0,
6892
+ recent: [],
6893
+ recentDenies: []
6894
+ };
6895
+ this.tallies.set(session, tally);
6896
+ }
6897
+ return tally;
6898
+ }
6814
6899
  /** Record one settled review outcome in the in-memory mirror (audit-disabled hosts). */
6815
6900
  recordVerdictMemory(memory, verdict) {
6816
6901
  memory.verdicts.unshift(verdict);
@@ -6822,6 +6907,23 @@ var AutoReviewRuntime = class AutoReviewRuntime {
6822
6907
  });
6823
6908
  if (memory.denies.length > MEMORY_DENIES_CAP) memory.denies.length = MEMORY_DENIES_CAP;
6824
6909
  }
6910
+ const tally = memory.tally;
6911
+ if (tally === void 0) return;
6912
+ if (verdict.denial) tally.denies += 1;
6913
+ else tally.allows += 1;
6914
+ tally.recent.unshift({
6915
+ id: verdict.id,
6916
+ toolName: verdict.toolName,
6917
+ denial: verdict.denial
6918
+ });
6919
+ if (tally.recent.length > MEMORY_RECENT_CAP) tally.recent.length = MEMORY_RECENT_CAP;
6920
+ if (verdict.denial) {
6921
+ tally.recentDenies.unshift({
6922
+ id: verdict.id,
6923
+ toolName: verdict.toolName
6924
+ });
6925
+ if (tally.recentDenies.length > MEMORY_DENIES_CAP) tally.recentDenies.length = MEMORY_DENIES_CAP;
6926
+ }
6825
6927
  }
6826
6928
  /** The number of leading consecutive denials in a newest-first verdict list. */
6827
6929
  static leadingDenials(verdicts) {
@@ -6869,6 +6971,72 @@ var AutoReviewRuntime = class AutoReviewRuntime {
6869
6971
  };
6870
6972
  }
6871
6973
  /**
6974
+ * The live session whose id is `id`, or undefined when no
6975
+ * live session matches. The projection fold carries the owning session id as
6976
+ * plain text, so the runtime has to resolve it back to a `Session` before it
6977
+ * can read the in-memory mirror. Read through `ctx.get` rather than
6978
+ * injected: the projection view is a render-time convenience, and a missing
6979
+ * service must only cost the mirror, never park this plugin in a pending
6980
+ * fiber.
6981
+ * @param id - the session id carried by the fold state.
6982
+ * @returns the live session, or undefined.
6983
+ */
6984
+ sessionById(id) {
6985
+ if (id === void 0) return void 0;
6986
+ const sessions = this.ctx.get("sessions");
6987
+ if (sessions === void 0) return void 0;
6988
+ for (const session of sessions.list()) if (String(session.id) === String(id)) return session;
6989
+ }
6990
+ /**
6991
+ * The panel view. The pure event fold can
6992
+ * never carry this plugin's statistics on a host that refuses unmarked
6993
+ * unknown event types (see {@link AutoReviewRuntime.auditMayAppend}), which
6994
+ * is why the panel read an empty log and showed all zeros. On such a host
6995
+ * this reads the in-memory mirror instead, so the panel reports the real
6996
+ * switch state and the real activity; on a marker-aware host the fold stays
6997
+ * authoritative and this returns it untouched.
6998
+ * @param state - the fold state, which carries its session id.
6999
+ * @returns the projection view the panel renders.
7000
+ */
7001
+ livePanelView(state) {
7002
+ const folded = viewAutoReview(state);
7003
+ if (this.auditMayAppend()) return folded;
7004
+ const session = this.sessionById(state.sessionId);
7005
+ if (session === void 0) return folded;
7006
+ const memory = this.memoryFor(session);
7007
+ const tally = memory.tally ?? {
7008
+ allows: 0,
7009
+ denies: 0,
7010
+ fallbacks: 0,
7011
+ rejections: 0,
7012
+ recent: [],
7013
+ recentDenies: []
7014
+ };
7015
+ return {
7016
+ enabled: effectiveAutoReviewState(sessionEvents(session)) ?? memory.enabledOverride ?? this.config.enableByDefault,
7017
+ verdictsUsed: memory.verdicts.length,
7018
+ failuresUsed: memory.failures,
7019
+ allows: tally.allows,
7020
+ denies: tally.denies,
7021
+ fallbacks: tally.fallbacks,
7022
+ neverRejects: tally.rejections,
7023
+ cacheHits: folded.cacheHits,
7024
+ avgDurationMs: folded.avgDurationMs,
7025
+ circuit: null,
7026
+ recent: tally.recent.map((verdict) => ({
7027
+ reviewId: String(verdict.id),
7028
+ toolName: verdict.toolName,
7029
+ at: Date.now(),
7030
+ durationMs: 0,
7031
+ decision: verdict.denial ? "deny" : "allow"
7032
+ })),
7033
+ recentDenies: tally.recentDenies.map((deny) => ({
7034
+ reviewId: String(deny.id),
7035
+ toolName: deny.toolName
7036
+ }))
7037
+ };
7038
+ }
7039
+ /**
6872
7040
  * The `approval/request` answerer. Claims a request ONLY when auto-review
6873
7041
  * is enabled for the session AND the policy for this tool/reason is `ai`
6874
7042
  * AND both per-turn budgets remain AND the rejection circuit breaker is
@@ -6932,7 +7100,10 @@ var AutoReviewRuntime = class AutoReviewRuntime {
6932
7100
  outcome: "rejected"
6933
7101
  }, { ignorable: true });
6934
7102
  this.probeAuditResult(result);
6935
- } else memory.rejections += 1;
7103
+ } else {
7104
+ memory.rejections += 1;
7105
+ if (memory.tally !== void 0) memory.tally.rejections += 1;
7106
+ }
6936
7107
  const text = auditOk ? neverResultText(rejectionId, request.toolName, reason) : plainNeverResultText(request.toolName, reason);
6937
7108
  this.recordFeedback(request.callId, `${text}\n${this.config.denyGuidance}`);
6938
7109
  return Promise.resolve("rejected");
@@ -6946,7 +7117,7 @@ var AutoReviewRuntime = class AutoReviewRuntime {
6946
7117
  * override, which changes the reviewer's evidence.
6947
7118
  * @param request - the pending approval request.
6948
7119
  * @returns the fingerprint, or undefined to skip the cache (fail-closed:
6949
- * the request then runs the second model as before).
7120
+ * the request then runs the reviewer as before).
6950
7121
  */
6951
7122
  fingerprintFor(request) {
6952
7123
  if (this.config.verdictCacheTtlMs <= 0) return void 0;
@@ -7052,7 +7223,11 @@ var AutoReviewRuntime = class AutoReviewRuntime {
7052
7223
  ...outcome !== void 0 ? { outcome } : {}
7053
7224
  }, { ignorable: true });
7054
7225
  this.probeAuditResult(result);
7055
- } else this.memoryFor(session).failures += 1;
7226
+ } else {
7227
+ const fallbackMemory = this.memoryFor(session);
7228
+ fallbackMemory.failures += 1;
7229
+ if (fallbackMemory.tally !== void 0) fallbackMemory.tally.fallbacks += 1;
7230
+ }
7056
7231
  this.ctx.logger.warn(`auto-review fallback (${failure.fallback}) for ${request.toolName}: ${failure.error}`);
7057
7232
  if (outcome === "rejected") this.recordFallbackFeedback(request.callId, reviewId, failure.fallback, failure.error);
7058
7233
  }
@@ -7379,7 +7554,7 @@ function apply(ctx, config) {
7379
7554
  input: { hint: "on|off|status|approve [n]" },
7380
7555
  handler: (invocation) => runtime.command(invocation)
7381
7556
  });
7382
- if (ctx.get("sessionProjections") !== void 0) ctx.inject(["sessionProjections"], (scope) => scope.sessionProjections.register(makeAutoReviewProjection(resolved.enableByDefault)));
7557
+ if (ctx.get("sessionProjections") !== void 0) ctx.inject(["sessionProjections"], (scope) => scope.sessionProjections.register(makeAutoReviewProjection(resolved.enableByDefault, (state) => runtime.livePanelView(state))));
7383
7558
  }
7384
7559
  //#endregion
7385
7560
  //#region src/call-id.ts
package/lib/invariant.js CHANGED
File without changes
package/lib/mcp.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes