chainlesschain 0.166.46 → 0.166.48

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 (232) hide show
  1. package/README.md +6 -4
  2. package/package.json +4 -2
  3. package/src/assets/web-panel/assets/{AIOps-Cp8KiMmv.js → AIOps-BLGEffkh.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-B4d9BGsm.js → ActionButton-Cz1z2VGD.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-Cf9yXTkv.js → Analytics-C3j9cf6q.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-40UkmxtP.js → AppLayout-BR9ERuCA.js} +4 -4
  7. package/src/assets/web-panel/assets/{Artifacts-Ckkb4oOA.js → Artifacts-CozJCm4y.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-Dnf569k5.js → Audit-DC5g9Ra2.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-CA6bLem4.js → BackgroundAgents-B1zrQ0dW.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-ChQU0I_Q.js → Backup-ChVnfjpm.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-BHeZXnNN.js → BaseInput-8_vLQsLf.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-DcFVYB2E.js → Chat-Dct9bm6L.js} +5 -5
  13. package/src/assets/web-panel/assets/ChatBubbleRenderer-BvoSsCKh.js +1 -0
  14. package/src/assets/web-panel/assets/{Checkbox-2PDbejv2.js → Checkbox-DZQIsCIC.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-DJBn1RAu.js → Codegen-CdzHxmnq.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-CPRq7n6y.js → Col-DdAN8LeW.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-axvINFMi.js → Community-CNgjT9MA.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-_OX4Qbe0.js → Compact-BtQiX6G-.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-DWAkvzRL.js → Compliance-OGX5ma3j.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-CUIJ25oj.js → Cowork-CgugQnGM.js} +3 -3
  21. package/src/assets/web-panel/assets/{Cron-nR0m7H3_.js → Cron-Bpg7cPFD.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-CgTLXzh_.js → Crosschain-D1hsuswf.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-BllzRB6Q.js → DID-miDRsMBw.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-C9f0IIBN.js → Dashboard-CGcl8htP.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-BkahetiO.js → Dropdown-BFaxTdVE.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-rartVC-k.js → EmailListRenderer-BQdGLZIO.js} +1 -1
  27. package/src/assets/web-panel/assets/{EvolutionSettings-BqpG3XHV.js → EvolutionSettings-bMvgBk6r.js} +1 -1
  28. package/src/assets/web-panel/assets/{FamilyGuardDashboard-DgUdFvw1.js → FamilyGuardDashboard-c1WVC_--.js} +1 -1
  29. package/src/assets/web-panel/assets/{Federation-oFpIQU6g.js → Federation-DyAcFrQY.js} +1 -1
  30. package/src/assets/web-panel/assets/{FormItemContext-BUH5xlyP.js → FormItemContext-CfwpfD2B.js} +1 -1
  31. package/src/assets/web-panel/assets/{GenericCardRenderer-ilRa-ueg.js → GenericCardRenderer-DkJcbBpS.js} +1 -1
  32. package/src/assets/web-panel/assets/{Git-DlHHSF8j.js → Git-DgYIrPJh.js} +2 -2
  33. package/src/assets/web-panel/assets/{Governance-yQiiXIWe.js → Governance-BF1tYPJ6.js} +1 -1
  34. package/src/assets/web-panel/assets/{Inference-BUDRVGOP.js → Inference-BFkwRIrU.js} +1 -1
  35. package/src/assets/web-panel/assets/{KnowledgeGraph-BaGyIoN1.js → KnowledgeGraph-DpCoqhBa.js} +1 -1
  36. package/src/assets/web-panel/assets/{Logs-C1cO6zE0.js → Logs-DU2qAJmD.js} +2 -2
  37. package/src/assets/web-panel/assets/{MarkdownRenderer-_z_Iaoes.js → MarkdownRenderer-QWv7UJLq.js} +1 -1
  38. package/src/assets/web-panel/assets/{Marketplace-CM8cGL0K.js → Marketplace-Dz9NUrfo.js} +1 -1
  39. package/src/assets/web-panel/assets/{McpTools-CDqAMU0U.js → McpTools-B67I3d5v.js} +3 -3
  40. package/src/assets/web-panel/assets/{Memory-CfB0-Ofv.js → Memory-crkElJSy.js} +2 -2
  41. package/src/assets/web-panel/assets/{MobileBridge-BL18ytvB.js → MobileBridge-BO20VbdX.js} +1 -1
  42. package/src/assets/web-panel/assets/{MobileProjects-PIyyxbuJ.js → MobileProjects-Ba-3gZZd.js} +1 -1
  43. package/src/assets/web-panel/assets/{Mtc-BTTrFCol.js → Mtc-BAHZM084.js} +4 -4
  44. package/src/assets/web-panel/assets/{MtcAudit-JHZn037c.js → MtcAudit-CL3DzajL.js} +4 -4
  45. package/src/assets/web-panel/assets/{Multisig-CMf9NEdB.js → Multisig-BSWWzvWH.js} +3 -3
  46. package/src/assets/web-panel/assets/{NLProgramming-Dhk3qaus.js → NLProgramming-fnxqXM_3.js} +1 -1
  47. package/src/assets/web-panel/assets/{Notes-6F8EarNN.js → Notes-qUZPIQXT.js} +3 -3
  48. package/src/assets/web-panel/assets/{NotificationSettings-gxdaVEit.js → NotificationSettings-x9Xrixqu.js} +1 -1
  49. package/src/assets/web-panel/assets/{OrderTableRenderer-gVgXQWdc.js → OrderTableRenderer-BtMtQUt7.js} +1 -1
  50. package/src/assets/web-panel/assets/{Organization-CzvHcDNO.js → Organization-C946Hzna.js} +4 -4
  51. package/src/assets/web-panel/assets/{Overflow-Dclx30Vv.js → Overflow-BYf8KIts.js} +1 -1
  52. package/src/assets/web-panel/assets/{P2P-CM0Wm9vZ.js → P2P-Cbqf5dnv.js} +2 -2
  53. package/src/assets/web-panel/assets/PdhVaultBrowser-rmxp0cuF.js +7 -0
  54. package/src/assets/web-panel/assets/{Permissions-B_oATenG.js → Permissions-CZBw45EW.js} +3 -3
  55. package/src/assets/web-panel/assets/{PersonalDataHub-CptiVBh5.js → PersonalDataHub-BKwUAt43.js} +4 -4
  56. package/src/assets/web-panel/assets/{Pipeline-DkHzgyyI.js → Pipeline-DmFD2QW4.js} +1 -1
  57. package/src/assets/web-panel/assets/{Privacy-DzZvwcKh.js → Privacy-KpuwWvt5.js} +1 -1
  58. package/src/assets/web-panel/assets/{ProjectInit-CzkCs-5G.js → ProjectInit-Bt8UheX6.js} +2 -2
  59. package/src/assets/web-panel/assets/{ProjectSettings-BB8dcxRD.js → ProjectSettings-2ZeAn9CP.js} +2 -2
  60. package/src/assets/web-panel/assets/{Projects-B7PW9CFY.js → Projects-zBuet8Zs.js} +1 -1
  61. package/src/assets/web-panel/assets/{Providers-Tfx-ti1V.js → Providers-DcEAnw6L.js} +1 -1
  62. package/src/assets/web-panel/assets/{QrScannerModal-EvWlNhfX.js → QrScannerModal-qF7liYG9.js} +1 -1
  63. package/src/assets/web-panel/assets/{QuickAsk-CUspgbNC.js → QuickAsk-qiESlVBJ.js} +1 -1
  64. package/src/assets/web-panel/assets/{Recommend-COY2g77J.js → Recommend-Bk-cQyi9.js} +1 -1
  65. package/src/assets/web-panel/assets/{RemoteSession-BzMBU6E6.js → RemoteSession-CwrekErS.js} +2 -2
  66. package/src/assets/web-panel/assets/{Reputation-RtYRF4YP.js → Reputation-Bmw_UQtW.js} +1 -1
  67. package/src/assets/web-panel/assets/{Row-Bva3l9Lc.js → Row-CPdqZIUX.js} +1 -1
  68. package/src/assets/web-panel/assets/{RssFeed-BhjzLvW3.js → RssFeed-D_W-xxvS.js} +2 -2
  69. package/src/assets/web-panel/assets/{Search-BYzT2PTR.js → Search-Hu0oDFj_.js} +1 -1
  70. package/src/assets/web-panel/assets/{Security-Bcl4-KTv.js → Security-DeRxUwmq.js} +3 -3
  71. package/src/assets/web-panel/assets/{Services-BRRa4A0f.js → Services-CsNmHFkf.js} +2 -2
  72. package/src/assets/web-panel/assets/{Skeleton-DYQ016qy.js → Skeleton-gv_Vz0tN.js} +1 -1
  73. package/src/assets/web-panel/assets/{Skills-CAUKL9JA.js → Skills-Dgg-_rCB.js} +1 -1
  74. package/src/assets/web-panel/assets/{Sla-BTyUqfuD.js → Sla-Bpu-bsEJ.js} +1 -1
  75. package/src/assets/web-panel/assets/{SpeechSettings-CUjMihrs.js → SpeechSettings-CrhleHuF.js} +1 -1
  76. package/src/assets/web-panel/assets/{SyncSettings-CvfOSupr.js → SyncSettings-JHjUN6I9.js} +2 -2
  77. package/src/assets/web-panel/assets/{Tasks-EvjNXrWH.js → Tasks-DKRWKkl9.js} +1 -1
  78. package/src/assets/web-panel/assets/{Templates-wPAp0EoM.js → Templates-leiWntLq.js} +1 -1
  79. package/src/assets/web-panel/assets/{Tenant-CP0D-q9Q.js → Tenant--1bYulvn.js} +1 -1
  80. package/src/assets/web-panel/assets/{Terminal-CQ0f2iFg.js → Terminal-C0y6FvSS.js} +2 -2
  81. package/src/assets/web-panel/assets/{TimelineRenderer-D3JrGSPb.js → TimelineRenderer-CQK4PVv4.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tokens-yXf2PPp9.js → Tokens-DbIvF-DV.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trigger-DfmMOoZ2.js → Trigger-CkXJ2vuq.js} +1 -1
  84. package/src/assets/web-panel/assets/{Trust-BTCyMgf1.js → Trust-B02WXJPw.js} +1 -1
  85. package/src/assets/web-panel/assets/{UkeySign-DKCwIBHi.js → UkeySign-VPMkfzmw.js} +1 -1
  86. package/src/assets/web-panel/assets/{VideoEditing-CQpDSzLO.js → VideoEditing-C5ezUnah.js} +1 -1
  87. package/src/assets/web-panel/assets/{Wallet-CyFrzC3K.js → Wallet-CYfvYRrY.js} +4 -4
  88. package/src/assets/web-panel/assets/{WebAuthn-D6Nr_1kd.js → WebAuthn-DtKl-kpr.js} +4 -4
  89. package/src/assets/web-panel/assets/{WorkflowEditor-CgR9tNrB.js → WorkflowEditor-DACITF-u.js} +1 -1
  90. package/src/assets/web-panel/assets/{chat-CRG_IeyP.js → chat-qnygfnLi.js} +1 -1
  91. package/src/assets/web-panel/assets/{colors-MdE3TStI.js → colors-D4mISLBD.js} +1 -1
  92. package/src/assets/web-panel/assets/{compact-item-B0LRuiNV.js → compact-item-BBPfxYGL.js} +1 -1
  93. package/src/assets/web-panel/assets/{createContext-B_V5fjcX.js → createContext-Byx4ndHp.js} +1 -1
  94. package/src/assets/web-panel/assets/devWarning-DBy6gsuI.js +1 -0
  95. package/src/assets/web-panel/assets/{hasIn-DSL-c_hr.js → hasIn-CGjLkkn3.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DE7bSYmW.js → index-B8UpadHC.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BD0w-888.js → index-B9jH-h4X.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BbZZ7P_c.js → index-BN7_TSgU.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BIDCNwy6.js → index-BNe2lCk6.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CkN53KiJ.js → index-B_fHPZ8E.js} +1 -1
  101. package/src/assets/web-panel/assets/index-BfEDXk_E.js +1 -0
  102. package/src/assets/web-panel/assets/{index-BsAEd-Do.js → index-BklGH3YI.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-CS3bOhV7.js → index-BmKYaaGP.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-B3iW-mhn.js → index-BxQF1wCU.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-SmQfEHjp.js → index-ByA1NyFP.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-0200QxC4.js → index-BzeBBAl5.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-c8OqO3rq.js → index-C3abGsSW.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-C-aKWR2v.js → index-C9tcHs46.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-BNhWoD6d.js → index-CD5Btq6p.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-CVDi4rS9.js → index-CFMWjuav.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-CEOAZm5j.js → index-CMj8qmpX.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-C67dPW0L.js → index-CVUJjQAT.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-D3pJBTvo.js → index-CWhI8G14.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-DtOTppk8.js → index-CZjNamSU.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DRRY44XP.js → index-CmHE-lY_.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-Tg3yl8wc.js → index-CoA8WsVr.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CXMeisJK.js → index-Cpg3AWRy.js} +1 -1
  118. package/src/assets/web-panel/assets/index-CrxUwNM8.js +1 -0
  119. package/src/assets/web-panel/assets/{index-Bq8-2Ibv.js → index-CsVuQsY1.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BHp1nSur.js → index-CydHMjwc.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CPYshcic.js → index-DH6iC5b2.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-DBwBZwio.js → index-DZXhKAk9.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-Didyuq2Z.js → index-D_g9mA1N.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CdHMc9LQ.js → index-DboTeuPF.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-C5dqieYX.js → index-DfgpKd64.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CdplnAS9.js → index-Dmfe0L2Z.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-KaSejHjf.js → index-Dzni_5Qw.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-BpwGsAA-.js → index-J5JkStvY.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-OBhlHSMN.js → index-Ne8UGllV.js} +3 -3
  130. package/src/assets/web-panel/assets/{index-CZICtYJ0.js → index-O09jXbir.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-DVgTf7CZ.js → index-Y7el_3WS.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-BQAsL19t.js → index-c_tBkIlU.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-BMm8tO2X.js → index-lKJWAmd4.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-CNytQuFq.js → index-xzRKTy0R.js} +1 -1
  135. package/src/assets/web-panel/assets/{initDefaultProps-P8SBN6Di.js → initDefaultProps-XuhfEIhB.js} +1 -1
  136. package/src/assets/web-panel/assets/{motion-BubbjUT_.js → motion-Cbxh6AxO.js} +1 -1
  137. package/src/assets/web-panel/assets/{move-Chcykv0I.js → move-C7IEnFly.js} +1 -1
  138. package/src/assets/web-panel/assets/{mtc-parser-DyiYuGDu.js → mtc-parser-lLADx4LL.js} +1 -1
  139. package/src/assets/web-panel/assets/{omit-Rdwb_Neb.js → omit-mIbic_k-.js} +1 -1
  140. package/src/assets/web-panel/assets/{pickAttrs-CCr0MYHl.js → pickAttrs-_xIYEqu3.js} +1 -1
  141. package/src/assets/web-panel/assets/{placementArrow-B1ZXz3bb.js → placementArrow-ClB-E0ny.js} +1 -1
  142. package/src/assets/web-panel/assets/{responsiveObserve-BGEdwej6.js → responsiveObserve-D36Bg2ut.js} +1 -1
  143. package/src/assets/web-panel/assets/{slide-CpnsUMaT.js → slide-CUuO08Bl.js} +1 -1
  144. package/src/assets/web-panel/assets/{statusUtils-C7x-TVp_.js → statusUtils-BKahwLbv.js} +1 -1
  145. package/src/assets/web-panel/assets/{styleChecker-CC5k_791.js → styleChecker-2pT-T8vy.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFlexGapSupport-BpZFKsJW.js → useFlexGapSupport-DyH5g7Rg.js} +1 -1
  147. package/src/assets/web-panel/assets/{useFs-CVq5HySy.js → useFs-CTOauB3X.js} +1 -1
  148. package/src/assets/web-panel/assets/{usePersonalDataHub-BGkdLryu.js → usePersonalDataHub-B02JYIMe.js} +1 -1
  149. package/src/assets/web-panel/assets/{vnode-uLtOUEK-.js → vnode-Do22wQrc.js} +1 -1
  150. package/src/assets/web-panel/assets/{zoom-BfVC9iUs.js → zoom-BfnL7rit.js} +1 -1
  151. package/src/assets/web-panel/index.html +1 -1
  152. package/src/command-help-index.json +3 -3
  153. package/src/commands/agent.js +8 -0
  154. package/src/commands/background-session.js +31 -3
  155. package/src/commands/chat.js +5 -1
  156. package/src/commands/eval.js +86 -28
  157. package/src/commands/llm.js +104 -1
  158. package/src/commands/plugin.js +161 -1
  159. package/src/commands/sandbox.js +121 -2
  160. package/src/commands/serve.js +11 -2
  161. package/src/commands/skill.js +29 -0
  162. package/src/data/changelog.json +26 -0
  163. package/src/gateways/ws/session-protocol.js +13 -0
  164. package/src/gateways/ws/ws-agent-handler.js +17 -0
  165. package/src/gateways/ws/ws-server.js +3 -0
  166. package/src/gateways/ws/ws-session-state.js +131 -0
  167. package/src/generated/cc-agent-protocol.schema.json +48 -1
  168. package/src/lazy-dispatch.js +8 -1
  169. package/src/lib/agent-sandbox.js +337 -53
  170. package/src/lib/app-server/cli-agent-kernel-adapter.js +28 -0
  171. package/src/lib/app-server/server.js +93 -0
  172. package/src/lib/background-agent-keeper-client.js +2 -0
  173. package/src/lib/background-agent-keeper-protocol.js +7 -0
  174. package/src/lib/background-agent-list-index.js +222 -0
  175. package/src/lib/background-agent-supervisor.js +232 -47
  176. package/src/lib/codex-app-server-adapter.js +82 -27
  177. package/src/lib/context-memory-kernel/durable-memory-port.js +31 -8
  178. package/src/lib/context-memory-kernel/provider-context.js +118 -19
  179. package/src/lib/deferred-question-context.js +278 -0
  180. package/src/lib/eval/evidence.js +202 -64
  181. package/src/lib/eval/plugin-suite.js +1181 -0
  182. package/src/lib/eval/runner.js +10 -3
  183. package/src/lib/evolution/agent-evolution-runtime-composition.js +236 -1
  184. package/src/lib/evolution/agent-model-projection.js +97 -1
  185. package/src/lib/evolution/evidence-backed-wiki-maintainer.js +154 -1
  186. package/src/lib/evolution/evolution-artifact-ports.js +6 -0
  187. package/src/lib/evolution/evolution-deployment-loader.js +87 -3
  188. package/src/lib/evolution/evolution-eval-gate.js +8 -0
  189. package/src/lib/evolution/evolution-eval-runtime-composition.js +255 -0
  190. package/src/lib/evolution/evolution-evidence-artifact-adapter.js +58 -2
  191. package/src/lib/evolution/evolution-ledger-file-backend.js +23 -2
  192. package/src/lib/evolution/evolution-ledger-manifest-chain.js +223 -7
  193. package/src/lib/evolution/evolution-ledger-v2-journal.js +525 -0
  194. package/src/lib/evolution/evolution-ledger-v2-manifest-backend.js +46 -2
  195. package/src/lib/evolution/evolution-ledger.js +57 -1
  196. package/src/lib/evolution/evolution-run-ledger-adapter.js +93 -0
  197. package/src/lib/evolution/evolution-run-wiki-evidence-resolver.js +259 -0
  198. package/src/lib/evolution/governed-hub-llm.js +238 -5
  199. package/src/lib/evolution/governed-hub-resolver.js +82 -4
  200. package/src/lib/evolution/governed-hub-skill.js +11 -5
  201. package/src/lib/evolution/governed-skill-synthesis-provider-chat.js +10 -1
  202. package/src/lib/evolution/skill-runtime-revalidation-ledger-adapter.js +254 -0
  203. package/src/lib/evolution/skill-runtime-revalidation.js +530 -0
  204. package/src/lib/evolution/skill-target-matrix-eval.js +4 -4
  205. package/src/lib/evolution/wiki-evidence-commit-guard.js +366 -0
  206. package/src/lib/evolution/wiki-informed-skill-proposer.js +13 -1
  207. package/src/lib/goal-context.js +14 -5
  208. package/src/lib/interaction-adapter.js +166 -1
  209. package/src/lib/model-capabilities.js +271 -0
  210. package/src/lib/model-context-catalog.js +105 -0
  211. package/src/lib/model-context-window.js +4 -49
  212. package/src/lib/openai-responses.js +428 -0
  213. package/src/lib/plugin-runtime/scopes.js +7 -1
  214. package/src/lib/remote-read-loop-guard.js +2 -2
  215. package/src/lib/runtime-usage-ledger.js +16 -0
  216. package/src/lib/session-host-snapshot.js +16 -0
  217. package/src/lib/skill-loader.js +12 -0
  218. package/src/lib/with-file-lock.js +3 -1
  219. package/src/repl/agent-repl.js +5 -1
  220. package/src/runtime/agent-core.js +348 -23
  221. package/src/runtime/agent-runtime.js +7 -0
  222. package/src/runtime/coding-agent-contract-shared.cjs +13 -1
  223. package/src/runtime/headless-runner.js +4 -1
  224. package/src/runtime/headless-stream.js +283 -45
  225. package/src/runtime/runtime-factory.js +26 -2
  226. package/src/workers/background-agent-keeper.js +23 -13
  227. package/src/workers/background-agent-worker.js +27 -13
  228. package/src/assets/web-panel/assets/ChatBubbleRenderer-Bi2SY9E_.js +0 -1
  229. package/src/assets/web-panel/assets/PdhVaultBrowser-fYmQzmTA.js +0 -7
  230. package/src/assets/web-panel/assets/devWarning-BtFLt3qI.js +0 -1
  231. package/src/assets/web-panel/assets/index-BhuWQHeG.js +0 -1
  232. package/src/assets/web-panel/assets/index-D-4tFek3.js +0 -1
@@ -0,0 +1,1181 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { runEvalSuite } from "./runner.js";
5
+ import { parsePluginManifest } from "../plugin-runtime/manifest.js";
6
+ import { encodeName } from "../plugin-runtime/scopes.js";
7
+
8
+ export const PLUGIN_EVAL_SUITE_SCHEMA = "chainlesschain.plugin-eval-suite/v1";
9
+ export const PLUGIN_EVAL_REPORT_SCHEMA = "chainlesschain.plugin-eval-report/v1";
10
+ export const PLUGIN_EVAL_HOLDOUT_SCHEMA =
11
+ "chainlesschain.plugin-eval-holdout/v1";
12
+
13
+ const DEFAULT_SUITE_PATH = "evals/suite.json";
14
+ const MAX_SUITE_BYTES = 1024 * 1024;
15
+ const MAX_TASKS = 64;
16
+ const MAX_FIXTURES_PER_TASK = 128;
17
+ const MAX_ASSERTIONS_PER_TASK = 128;
18
+ const MAX_FIXTURE_BYTES = 256 * 1024;
19
+ const MAX_PLUGIN_FILES = 4096;
20
+ const MAX_PLUGIN_ENTRIES = 8192;
21
+ const MAX_PLUGIN_DEPTH = 64;
22
+ const MAX_PLUGIN_BYTES = 32 * 1024 * 1024;
23
+ const MAX_ASSERTION_FILE_BYTES = 4 * 1024 * 1024;
24
+ const MAX_RUNTIME_OUTPUT_BYTES = 16 * 1024 * 1024;
25
+ const MAX_RUNTIME_TOOL_CALLS = 10_000;
26
+ const MAX_TRIGGER_SIGNALS = 256;
27
+ const MAX_SAMPLE_RUNS = 20;
28
+ const TASK_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/u;
29
+ const EXPECTATIONS = new Set([
30
+ "should_trigger",
31
+ "should_not_trigger",
32
+ "optional",
33
+ ]);
34
+ const ASSERTION_TYPES = new Set([
35
+ "file_exists",
36
+ "file_absent",
37
+ "file_equals",
38
+ "file_contains",
39
+ "json_equals",
40
+ ]);
41
+ const ARM_ORDERS = new Set(["balanced", "control-first", "candidate-first"]);
42
+
43
+ function digest(bytes, domain) {
44
+ return `sha256:${crypto
45
+ .createHash("sha256")
46
+ .update(domain)
47
+ .update("\0")
48
+ .update(bytes)
49
+ .digest("hex")}`;
50
+ }
51
+
52
+ function canonical(value) {
53
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
54
+ if (value && typeof value === "object") {
55
+ return `{${Object.keys(value)
56
+ .sort()
57
+ .map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
58
+ .join(",")}}`;
59
+ }
60
+ return JSON.stringify(value);
61
+ }
62
+
63
+ function plainObject(value, label) {
64
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
65
+ throw new TypeError(`${label} must be an object`);
66
+ }
67
+ return value;
68
+ }
69
+
70
+ function boundedString(
71
+ value,
72
+ label,
73
+ max,
74
+ { allowEmpty = false, allowFormatting = false } = {},
75
+ ) {
76
+ const controls =
77
+ typeof value === "string" && allowFormatting
78
+ ? value.replace(/[\t\r\n]/gu, "")
79
+ : value;
80
+ if (
81
+ typeof value !== "string" ||
82
+ (!allowEmpty && value.trim().length === 0) ||
83
+ value.length > max ||
84
+ /\p{Cc}/u.test(controls)
85
+ ) {
86
+ throw new TypeError(
87
+ `${label} must be a string of at most ${max} characters`,
88
+ );
89
+ }
90
+ return value;
91
+ }
92
+
93
+ function relativeTaskPath(value, label) {
94
+ const raw = boundedString(value, label, 512).replaceAll("\\", "/");
95
+ if (raw.startsWith("/") || /^[a-zA-Z]:\//u.test(raw)) {
96
+ throw new TypeError(`${label} must be relative`);
97
+ }
98
+ const normalized = path.posix.normalize(raw);
99
+ if (
100
+ normalized === "." ||
101
+ normalized === ".." ||
102
+ normalized.startsWith("../") ||
103
+ normalized === ".chainlesschain" ||
104
+ normalized.startsWith(".chainlesschain/")
105
+ ) {
106
+ throw new TypeError(
107
+ `${label} must stay inside the task workspace and outside .chainlesschain`,
108
+ );
109
+ }
110
+ return normalized;
111
+ }
112
+
113
+ function suiteRelativePath(root, suiteFile) {
114
+ const candidate = path.resolve(root, suiteFile || DEFAULT_SUITE_PATH);
115
+ const relation = path.relative(root, candidate);
116
+ if (
117
+ relation === "" ||
118
+ relation === ".." ||
119
+ relation.startsWith(`..${path.sep}`) ||
120
+ path.isAbsolute(relation)
121
+ ) {
122
+ throw new TypeError(
123
+ "plugin eval suite must be a file inside the plugin root",
124
+ );
125
+ }
126
+ return relation.split(path.sep).join("/");
127
+ }
128
+
129
+ function readBoundedJsonFile(file, label) {
130
+ const target = path.resolve(file);
131
+ const stat = fs.lstatSync(target);
132
+ if (!stat.isFile() || stat.isSymbolicLink()) {
133
+ throw new TypeError(`${label} must be a regular file, not a symlink`);
134
+ }
135
+ if (stat.size === 0 || stat.size > MAX_SUITE_BYTES) {
136
+ throw new RangeError(`${label} must be under ${MAX_SUITE_BYTES} bytes`);
137
+ }
138
+ const bytes = fs.readFileSync(target);
139
+ let value;
140
+ try {
141
+ value = JSON.parse(bytes.toString("utf8"));
142
+ } catch {
143
+ throw new TypeError(`${label} is not valid JSON`);
144
+ }
145
+ return { target, bytes, value: plainObject(value, label) };
146
+ }
147
+
148
+ function collectPluginSnapshot(root) {
149
+ const rootStat = fs.lstatSync(root);
150
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
151
+ throw new TypeError("plugin root must be a real directory, not a symlink");
152
+ }
153
+ const files = [];
154
+ let totalBytes = 0;
155
+ let entriesVisited = 0;
156
+ const walk = (directory, relativeDirectory = "", depth = 0) => {
157
+ if (depth > MAX_PLUGIN_DEPTH) {
158
+ throw new RangeError(
159
+ `plugin eval payload exceeds the ${MAX_PLUGIN_DEPTH}-directory depth limit`,
160
+ );
161
+ }
162
+ const entries = fs
163
+ .readdirSync(directory, { withFileTypes: true })
164
+ .sort((a, b) => a.name.localeCompare(b.name));
165
+ for (const entry of entries) {
166
+ if (!relativeDirectory && entry.name === ".git") continue;
167
+ entriesVisited += 1;
168
+ if (entriesVisited > MAX_PLUGIN_ENTRIES) {
169
+ throw new RangeError(
170
+ `plugin eval payload exceeds ${MAX_PLUGIN_ENTRIES} filesystem entries`,
171
+ );
172
+ }
173
+ const relative = relativeDirectory
174
+ ? `${relativeDirectory}/${entry.name}`
175
+ : entry.name;
176
+ const absolute = path.join(directory, entry.name);
177
+ const stat = fs.lstatSync(absolute);
178
+ if (stat.isSymbolicLink()) {
179
+ throw new TypeError(`plugin eval refuses symlinks: ${relative}`);
180
+ }
181
+ if (stat.isDirectory()) {
182
+ walk(absolute, relative, depth + 1);
183
+ continue;
184
+ }
185
+ if (!stat.isFile()) {
186
+ throw new TypeError(`plugin eval refuses special files: ${relative}`);
187
+ }
188
+ if (stat.size > MAX_PLUGIN_BYTES - totalBytes) {
189
+ throw new RangeError(
190
+ `plugin eval payload exceeds ${MAX_PLUGIN_BYTES} bytes`,
191
+ );
192
+ }
193
+ const bytes = fs.readFileSync(absolute);
194
+ totalBytes += bytes.length;
195
+ if (
196
+ files.length + 1 > MAX_PLUGIN_FILES ||
197
+ totalBytes > MAX_PLUGIN_BYTES
198
+ ) {
199
+ throw new RangeError(
200
+ `plugin eval payload exceeds ${MAX_PLUGIN_FILES} files or ${MAX_PLUGIN_BYTES} bytes`,
201
+ );
202
+ }
203
+ files.push({ relative, bytes, mode: stat.mode & 0o777 });
204
+ }
205
+ };
206
+ walk(root);
207
+ const hash = crypto.createHash("sha256");
208
+ hash.update("chainlesschain.plugin-eval-payload/v1\0");
209
+ for (const file of files) {
210
+ hash.update(file.relative);
211
+ hash.update("\0");
212
+ hash.update(String(file.bytes.length));
213
+ hash.update("\0");
214
+ hash.update(String(file.mode));
215
+ hash.update("\0");
216
+ hash.update(file.bytes);
217
+ hash.update("\0");
218
+ }
219
+ return {
220
+ files,
221
+ fileCount: files.length,
222
+ byteCount: totalBytes,
223
+ digest: `sha256:${hash.digest("hex")}`,
224
+ };
225
+ }
226
+
227
+ function parseThreshold(value, label, { min = 0, max = 1 } = {}) {
228
+ if (value == null) return null;
229
+ if (
230
+ typeof value !== "number" ||
231
+ !Number.isFinite(value) ||
232
+ value < min ||
233
+ value > max
234
+ ) {
235
+ throw new RangeError(`${label} must be a number between ${min} and ${max}`);
236
+ }
237
+ return value;
238
+ }
239
+
240
+ function parseSampleRuns(value, label = "sampling.runs") {
241
+ if (!Number.isSafeInteger(value) || value < 1 || value > MAX_SAMPLE_RUNS) {
242
+ throw new RangeError(
243
+ `${label} must be an integer between 1 and ${MAX_SAMPLE_RUNS}`,
244
+ );
245
+ }
246
+ return value;
247
+ }
248
+
249
+ function parseArmOrder(value, label = "sampling.armOrder") {
250
+ if (!ARM_ORDERS.has(value)) {
251
+ throw new TypeError(
252
+ `${label} must be balanced, control-first, or candidate-first`,
253
+ );
254
+ }
255
+ return value;
256
+ }
257
+
258
+ function normalizeAssertion(value, taskId, index) {
259
+ const assertion = plainObject(value, `task ${taskId} assertion ${index}`);
260
+ if (!ASSERTION_TYPES.has(assertion.type)) {
261
+ throw new TypeError(
262
+ `task ${taskId} assertion ${index} has an unsupported type`,
263
+ );
264
+ }
265
+ const normalized = {
266
+ type: assertion.type,
267
+ path: relativeTaskPath(
268
+ assertion.path,
269
+ `task ${taskId} assertion ${index}.path`,
270
+ ),
271
+ };
272
+ if (["file_equals", "file_contains"].includes(assertion.type)) {
273
+ normalized.value = boundedString(
274
+ assertion.value,
275
+ `task ${taskId} assertion ${index}.value`,
276
+ MAX_FIXTURE_BYTES,
277
+ { allowEmpty: true, allowFormatting: true },
278
+ );
279
+ } else if (assertion.type === "json_equals") {
280
+ if (assertion.value === undefined) {
281
+ throw new TypeError(
282
+ `task ${taskId} assertion ${index}.value is required`,
283
+ );
284
+ }
285
+ const encoded = canonical(assertion.value);
286
+ if (Buffer.byteLength(encoded, "utf8") > MAX_FIXTURE_BYTES) {
287
+ throw new RangeError(
288
+ `task ${taskId} assertion ${index}.value exceeds its size limit`,
289
+ );
290
+ }
291
+ normalized.value = assertion.value;
292
+ }
293
+ return normalized;
294
+ }
295
+
296
+ function normalizeTask(value, index) {
297
+ const task = plainObject(value, `tasks[${index}]`);
298
+ const id = boundedString(task.id, `tasks[${index}].id`, 128);
299
+ if (!TASK_ID.test(id))
300
+ throw new TypeError(`invalid plugin eval task id: ${id}`);
301
+ const expectation = task.expectation || "should_trigger";
302
+ if (!EXPECTATIONS.has(expectation)) {
303
+ throw new TypeError(`task ${id} has an invalid expectation`);
304
+ }
305
+ const fixturesInput = task.fixtures || [];
306
+ if (
307
+ !Array.isArray(fixturesInput) ||
308
+ fixturesInput.length > MAX_FIXTURES_PER_TASK
309
+ ) {
310
+ throw new RangeError(`task ${id} has too many fixtures`);
311
+ }
312
+ let fixtureBytes = 0;
313
+ const fixturePaths = new Set();
314
+ const fixtures = fixturesInput.map((fixtureValue, fixtureIndex) => {
315
+ const fixture = plainObject(
316
+ fixtureValue,
317
+ `task ${id} fixture ${fixtureIndex}`,
318
+ );
319
+ const fixturePath = relativeTaskPath(
320
+ fixture.path,
321
+ `task ${id} fixture ${fixtureIndex}.path`,
322
+ );
323
+ if (fixturePaths.has(fixturePath)) {
324
+ throw new TypeError(
325
+ `task ${id} has duplicate fixture path ${fixturePath}`,
326
+ );
327
+ }
328
+ fixturePaths.add(fixturePath);
329
+ const encoding = fixture.encoding || "utf8";
330
+ if (!new Set(["utf8", "base64"]).has(encoding)) {
331
+ throw new TypeError(
332
+ `task ${id} fixture ${fixtureIndex} has invalid encoding`,
333
+ );
334
+ }
335
+ const content = boundedString(
336
+ fixture.content,
337
+ `task ${id} fixture ${fixtureIndex}.content`,
338
+ MAX_FIXTURE_BYTES,
339
+ { allowEmpty: true, allowFormatting: true },
340
+ );
341
+ const bytes = Buffer.from(content, encoding);
342
+ fixtureBytes += bytes.length;
343
+ if (bytes.length > MAX_FIXTURE_BYTES || fixtureBytes > MAX_SUITE_BYTES) {
344
+ throw new RangeError(`task ${id} fixture content exceeds its size limit`);
345
+ }
346
+ return { path: fixturePath, bytes };
347
+ });
348
+ if (
349
+ !Array.isArray(task.assertions) ||
350
+ task.assertions.length === 0 ||
351
+ task.assertions.length > MAX_ASSERTIONS_PER_TASK
352
+ ) {
353
+ throw new RangeError(
354
+ `task ${id} requires 1-${MAX_ASSERTIONS_PER_TASK} assertions`,
355
+ );
356
+ }
357
+ const assertions = task.assertions.map((assertion, assertionIndex) =>
358
+ normalizeAssertion(assertion, id, assertionIndex),
359
+ );
360
+ const expectedInput = task.expectedFiles || [
361
+ ...new Set(
362
+ assertions
363
+ .filter((assertion) => assertion.type !== "file_absent")
364
+ .map((assertion) => assertion.path),
365
+ ),
366
+ ];
367
+ if (!Array.isArray(expectedInput) || expectedInput.length > 128) {
368
+ throw new RangeError(`task ${id} has too many expectedFiles`);
369
+ }
370
+ const expectedFiles = [
371
+ ...new Set(
372
+ expectedInput.map((file, fileIndex) =>
373
+ relativeTaskPath(file, `task ${id}.expectedFiles[${fileIndex}]`),
374
+ ),
375
+ ),
376
+ ];
377
+ const timeoutMs = task.timeoutMs == null ? undefined : Number(task.timeoutMs);
378
+ if (
379
+ timeoutMs !== undefined &&
380
+ (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1000 || timeoutMs > 600000)
381
+ ) {
382
+ throw new RangeError(`task ${id}.timeoutMs must be 1000-600000`);
383
+ }
384
+ return {
385
+ id,
386
+ description: boundedString(
387
+ task.description || "",
388
+ `task ${id}.description`,
389
+ 1024,
390
+ { allowEmpty: true, allowFormatting: true },
391
+ ),
392
+ prompt: boundedString(task.prompt, `task ${id}.prompt`, 32768, {
393
+ allowFormatting: true,
394
+ }),
395
+ expectation,
396
+ fixtures,
397
+ assertions,
398
+ expectedFiles,
399
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
400
+ };
401
+ }
402
+
403
+ export function loadPluginEvalDefinition(pluginRoot, { suiteFile } = {}) {
404
+ const root = path.resolve(pluginRoot);
405
+ const manifest = parsePluginManifest(root);
406
+ if (manifest.ok !== true) {
407
+ throw new TypeError(
408
+ `plugin manifest is invalid: ${manifest.errors.join("; ") || "unknown error"}`,
409
+ );
410
+ }
411
+ const snapshot = collectPluginSnapshot(root);
412
+ const suitePath = suiteRelativePath(root, suiteFile);
413
+ const suiteEntry = snapshot.files.find((file) => file.relative === suitePath);
414
+ if (
415
+ !suiteEntry ||
416
+ suiteEntry.bytes.length === 0 ||
417
+ suiteEntry.bytes.length > MAX_SUITE_BYTES
418
+ ) {
419
+ throw new TypeError(
420
+ `plugin eval suite ${suitePath} must be a non-empty regular file under ${MAX_SUITE_BYTES} bytes`,
421
+ );
422
+ }
423
+ let raw;
424
+ try {
425
+ raw = JSON.parse(suiteEntry.bytes.toString("utf8"));
426
+ } catch {
427
+ throw new TypeError(`plugin eval suite ${suitePath} is not valid JSON`);
428
+ }
429
+ plainObject(raw, "plugin eval suite");
430
+ if (raw.schema !== PLUGIN_EVAL_SUITE_SCHEMA) {
431
+ throw new TypeError(
432
+ `plugin eval suite must use ${PLUGIN_EVAL_SUITE_SCHEMA}`,
433
+ );
434
+ }
435
+ const identity = plainObject(raw.plugin, "plugin eval suite.plugin");
436
+ if (
437
+ identity.name !== manifest.metadata.name ||
438
+ identity.version !== manifest.metadata.version
439
+ ) {
440
+ throw new TypeError(
441
+ "plugin eval suite identity must match the parsed plugin manifest",
442
+ );
443
+ }
444
+ if (
445
+ !Array.isArray(raw.tasks) ||
446
+ raw.tasks.length === 0 ||
447
+ raw.tasks.length > MAX_TASKS
448
+ ) {
449
+ throw new RangeError(`plugin eval suite requires 1-${MAX_TASKS} tasks`);
450
+ }
451
+ const tasks = raw.tasks.map(normalizeTask);
452
+ const ids = new Set(tasks.map((task) => task.id));
453
+ if (ids.size !== tasks.length) {
454
+ throw new TypeError("plugin eval task ids must be unique");
455
+ }
456
+ const thresholds = raw.thresholds || {};
457
+ plainObject(thresholds, "plugin eval suite.thresholds");
458
+ const sampling = raw.sampling || {};
459
+ plainObject(sampling, "plugin eval suite.sampling");
460
+ const maxCandidateCostUsd = parseThreshold(
461
+ thresholds.maxCandidateCostUsd,
462
+ "thresholds.maxCandidateCostUsd",
463
+ { min: 0, max: 1_000_000 },
464
+ );
465
+ return {
466
+ schema: PLUGIN_EVAL_SUITE_SCHEMA,
467
+ pluginRoot: fs.realpathSync(root),
468
+ identity: {
469
+ name: manifest.metadata.name,
470
+ version: manifest.metadata.version,
471
+ skills: manifest.components.skills.map((skill) => skill.name),
472
+ },
473
+ suitePath,
474
+ suiteDigest: digest(
475
+ suiteEntry.bytes,
476
+ "chainlesschain.plugin-eval-suite-bytes/v1",
477
+ ),
478
+ payload: snapshot,
479
+ tasks,
480
+ sampling: {
481
+ runs: parseSampleRuns(sampling.runs ?? 1),
482
+ armOrder: parseArmOrder(sampling.armOrder || "balanced"),
483
+ },
484
+ thresholds: {
485
+ minPassRateDelta:
486
+ parseThreshold(
487
+ thresholds.minPassRateDelta,
488
+ "thresholds.minPassRateDelta",
489
+ { min: -1, max: 1 },
490
+ ) ?? 0,
491
+ maxUnrelatedChangeRate:
492
+ parseThreshold(
493
+ thresholds.maxUnrelatedChangeRate,
494
+ "thresholds.maxUnrelatedChangeRate",
495
+ ) ?? 1,
496
+ maxCandidateCostUsd,
497
+ },
498
+ };
499
+ }
500
+
501
+ /**
502
+ * Attach reviewer-owned tasks from a file outside the plugin payload. The
503
+ * digest is reported separately so an author-controlled payload cannot mutate
504
+ * or silently absorb the holdout corpus.
505
+ */
506
+ export function attachPluginEvalHoldout(definition, holdoutFile) {
507
+ if (!holdoutFile) return definition;
508
+ const { target, bytes, value } = readBoundedJsonFile(
509
+ holdoutFile,
510
+ "plugin eval holdout",
511
+ );
512
+ const holdoutReal = fs.realpathSync(target);
513
+ const relation = path.relative(definition.pluginRoot, holdoutReal);
514
+ if (
515
+ relation === "" ||
516
+ (!relation.startsWith(`..${path.sep}`) &&
517
+ relation !== ".." &&
518
+ !path.isAbsolute(relation))
519
+ ) {
520
+ throw new TypeError(
521
+ "plugin eval holdout must be outside the plugin payload root",
522
+ );
523
+ }
524
+ if (value.schema !== PLUGIN_EVAL_HOLDOUT_SCHEMA) {
525
+ throw new TypeError(
526
+ `plugin eval holdout must use ${PLUGIN_EVAL_HOLDOUT_SCHEMA}`,
527
+ );
528
+ }
529
+ const identity = plainObject(value.plugin, "plugin eval holdout.plugin");
530
+ if (
531
+ identity.name !== definition.identity.name ||
532
+ identity.version !== definition.identity.version
533
+ ) {
534
+ throw new TypeError(
535
+ "plugin eval holdout identity must match the parsed plugin manifest",
536
+ );
537
+ }
538
+ if (
539
+ !Array.isArray(value.tasks) ||
540
+ value.tasks.length === 0 ||
541
+ value.tasks.length > MAX_TASKS
542
+ ) {
543
+ throw new RangeError(`plugin eval holdout requires 1-${MAX_TASKS} tasks`);
544
+ }
545
+ const holdoutTasks = value.tasks.map((task, index) => ({
546
+ ...normalizeTask(task, index),
547
+ source: "holdout",
548
+ }));
549
+ if (definition.tasks.length + holdoutTasks.length > MAX_TASKS) {
550
+ throw new RangeError(
551
+ `plugin eval author and holdout tasks may not exceed ${MAX_TASKS}`,
552
+ );
553
+ }
554
+ const ids = new Set(definition.tasks.map((task) => task.id));
555
+ for (const task of holdoutTasks) {
556
+ if (ids.has(task.id)) {
557
+ throw new TypeError(`duplicate plugin eval task id: ${task.id}`);
558
+ }
559
+ ids.add(task.id);
560
+ }
561
+ return {
562
+ ...definition,
563
+ tasks: [
564
+ ...definition.tasks.map((task) => ({
565
+ ...task,
566
+ source: task.source || "author",
567
+ })),
568
+ ...holdoutTasks,
569
+ ],
570
+ holdout: {
571
+ schema: PLUGIN_EVAL_HOLDOUT_SCHEMA,
572
+ // Reports may be shared as CI artifacts; do not disclose the reviewer's
573
+ // absolute workstation path. The digest is the authoritative binding.
574
+ path: path.basename(target),
575
+ digest: digest(bytes, "chainlesschain.plugin-eval-holdout-bytes/v1"),
576
+ tasks: holdoutTasks.length,
577
+ independentFromPayload: true,
578
+ },
579
+ };
580
+ }
581
+
582
+ function materializePlugin(definition, workspace) {
583
+ const nameDir = path.join(
584
+ workspace,
585
+ ".chainlesschain",
586
+ "plugins",
587
+ encodeName(definition.identity.name),
588
+ );
589
+ const versionDir = path.join(nameDir, definition.identity.version);
590
+ for (const file of definition.payload.files) {
591
+ const target = path.join(versionDir, ...file.relative.split("/"));
592
+ fs.mkdirSync(path.dirname(target), { recursive: true });
593
+ fs.writeFileSync(target, file.bytes, { flag: "wx" });
594
+ fs.chmodSync(target, file.mode);
595
+ }
596
+ fs.writeFileSync(path.join(nameDir, ".active"), definition.identity.version, {
597
+ encoding: "utf8",
598
+ flag: "wx",
599
+ });
600
+ }
601
+
602
+ function readTaskFile(workspace, relative) {
603
+ const segments = relative.split("/");
604
+ let target = workspace;
605
+ try {
606
+ for (const [index, segment] of segments.entries()) {
607
+ target = path.join(target, segment);
608
+ const component = fs.lstatSync(target);
609
+ if (component.isSymbolicLink()) return null;
610
+ if (index < segments.length - 1 && !component.isDirectory()) return null;
611
+ }
612
+ const stat = fs.lstatSync(target);
613
+ if (
614
+ !stat.isFile() ||
615
+ stat.isSymbolicLink() ||
616
+ stat.size > MAX_ASSERTION_FILE_BYTES
617
+ ) {
618
+ return null;
619
+ }
620
+ const workspaceReal = fs.realpathSync(workspace);
621
+ const targetReal = fs.realpathSync(target);
622
+ const relation = path.relative(workspaceReal, targetReal);
623
+ if (
624
+ relation === ".." ||
625
+ relation.startsWith(`..${path.sep}`) ||
626
+ path.isAbsolute(relation)
627
+ ) {
628
+ return null;
629
+ }
630
+ const noFollow = fs.constants.O_NOFOLLOW || 0;
631
+ const descriptor = fs.openSync(target, fs.constants.O_RDONLY | noFollow);
632
+ try {
633
+ const opened = fs.fstatSync(descriptor);
634
+ if (!opened.isFile() || opened.size > MAX_ASSERTION_FILE_BYTES)
635
+ return null;
636
+ return fs.readFileSync(descriptor);
637
+ } finally {
638
+ fs.closeSync(descriptor);
639
+ }
640
+ } catch {
641
+ return null;
642
+ }
643
+ }
644
+
645
+ function checkAssertions(task, workspace) {
646
+ for (const assertion of task.assertions) {
647
+ const bytes = readTaskFile(workspace, assertion.path);
648
+ if (assertion.type === "file_absent") {
649
+ if (bytes !== null) {
650
+ return { pass: false, detail: `${assertion.path} should be absent` };
651
+ }
652
+ continue;
653
+ }
654
+ if (bytes === null) {
655
+ return { pass: false, detail: `${assertion.path} is missing` };
656
+ }
657
+ if (assertion.type === "file_exists") continue;
658
+ const text = bytes.toString("utf8");
659
+ if (assertion.type === "file_equals" && text !== assertion.value) {
660
+ return { pass: false, detail: `${assertion.path} content did not match` };
661
+ }
662
+ if (assertion.type === "file_contains" && !text.includes(assertion.value)) {
663
+ return {
664
+ pass: false,
665
+ detail: `${assertion.path} did not contain the expected text`,
666
+ };
667
+ }
668
+ if (assertion.type === "json_equals") {
669
+ let actual;
670
+ try {
671
+ actual = JSON.parse(text);
672
+ } catch {
673
+ return { pass: false, detail: `${assertion.path} is not valid JSON` };
674
+ }
675
+ if (canonical(actual) !== canonical(assertion.value)) {
676
+ return { pass: false, detail: `${assertion.path} JSON did not match` };
677
+ }
678
+ }
679
+ }
680
+ return { pass: true, detail: "all declarative assertions passed" };
681
+ }
682
+
683
+ function buildTasks(definition, { candidate }) {
684
+ return definition.tasks.map((task) => ({
685
+ id: task.id,
686
+ description: task.description,
687
+ source: task.source || "author",
688
+ prompt: task.prompt,
689
+ expectedFiles: task.expectedFiles,
690
+ ...(task.timeoutMs === undefined ? {} : { timeoutMs: task.timeoutMs }),
691
+ setup(workspace) {
692
+ for (const fixture of task.fixtures) {
693
+ const target = path.join(workspace, ...fixture.path.split("/"));
694
+ fs.mkdirSync(path.dirname(target), { recursive: true });
695
+ fs.writeFileSync(target, fixture.bytes, { flag: "wx" });
696
+ }
697
+ if (candidate) materializePlugin(definition, workspace);
698
+ },
699
+ check(workspace) {
700
+ return checkAssertions(task, workspace);
701
+ },
702
+ }));
703
+ }
704
+
705
+ function normalizedUsage(value) {
706
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
707
+ const output = {};
708
+ for (const key of [
709
+ "input_tokens",
710
+ "output_tokens",
711
+ "cache_read_input_tokens",
712
+ "cache_creation_input_tokens",
713
+ "reasoning_tokens",
714
+ ]) {
715
+ const amount = value[key];
716
+ if (typeof amount === "number" && Number.isFinite(amount) && amount >= 0) {
717
+ output[key] = amount;
718
+ }
719
+ }
720
+ return output;
721
+ }
722
+
723
+ export function parsePluginEvalRuntimeMetrics(output, identity) {
724
+ try {
725
+ const serialized = String(output || "");
726
+ if (Buffer.byteLength(serialized, "utf8") > MAX_RUNTIME_OUTPUT_BYTES) {
727
+ return { available: false, pluginTriggered: null };
728
+ }
729
+ const events = serialized
730
+ .split(/\r?\n/u)
731
+ .filter((line) => line.trim())
732
+ .map((line) => JSON.parse(line));
733
+ const terminals = events.filter((event) => event?.type === "result");
734
+ if (terminals.length !== 1 || events.at(-1) !== terminals[0]) {
735
+ return { available: false, pluginTriggered: null };
736
+ }
737
+ const terminal = terminals[0];
738
+ const toolCalls = Array.isArray(terminal.tool_calls)
739
+ ? terminal.tool_calls
740
+ : [];
741
+ if (toolCalls.length > MAX_RUNTIME_TOOL_CALLS) {
742
+ return { available: false, pluginTriggered: null };
743
+ }
744
+ const skillNames = new Set(identity.skills || []);
745
+ const signals = [];
746
+ for (const call of toolCalls) {
747
+ if (call?.plugin === identity.name) {
748
+ if (signals.length < MAX_TRIGGER_SIGNALS) {
749
+ signals.push({
750
+ source: "plugin_attribution",
751
+ tool: call.tool || null,
752
+ });
753
+ }
754
+ continue;
755
+ }
756
+ if (
757
+ call?.tool === "run_skill" &&
758
+ skillNames.has(call?.args?.skill_name)
759
+ ) {
760
+ if (signals.length < MAX_TRIGGER_SIGNALS) {
761
+ signals.push({
762
+ source: "skill_name",
763
+ tool: "run_skill",
764
+ skill: call.args.skill_name,
765
+ });
766
+ }
767
+ }
768
+ }
769
+ const cost = terminal.total_cost_usd;
770
+ return {
771
+ available: true,
772
+ pluginTriggered: signals.length > 0,
773
+ signals,
774
+ usage: normalizedUsage(terminal.usage),
775
+ totalCostUsd:
776
+ typeof cost === "number" && Number.isFinite(cost) && cost >= 0
777
+ ? cost
778
+ : null,
779
+ turns:
780
+ Number.isSafeInteger(terminal.num_turns) && terminal.num_turns >= 0
781
+ ? terminal.num_turns
782
+ : null,
783
+ toolCallCount: toolCalls.length,
784
+ };
785
+ } catch {
786
+ return { available: false, pluginTriggered: null };
787
+ }
788
+ }
789
+
790
+ function instrumentRunAgent(runAgent, identity) {
791
+ return async (input) => {
792
+ const result = await runAgent(input);
793
+ return {
794
+ ...result,
795
+ evaluationMetrics: parsePluginEvalRuntimeMetrics(
796
+ result?.output,
797
+ identity,
798
+ ),
799
+ };
800
+ };
801
+ }
802
+
803
+ function sumUsage(results) {
804
+ const totals = {};
805
+ let cost = 0;
806
+ let costAvailable = true;
807
+ for (const result of results) {
808
+ const metrics = result.evaluationMetrics;
809
+ if (metrics?.usage) {
810
+ for (const [key, value] of Object.entries(metrics.usage)) {
811
+ totals[key] = (totals[key] || 0) + value;
812
+ }
813
+ }
814
+ if (typeof metrics?.totalCostUsd === "number") cost += metrics.totalCostUsd;
815
+ else costAvailable = false;
816
+ }
817
+ return {
818
+ usage: totals,
819
+ totalCostUsd: costAvailable ? cost : null,
820
+ };
821
+ }
822
+
823
+ function projectArm(summary, definition, { candidate }) {
824
+ const byId = new Map(definition.tasks.map((task) => [task.id, task]));
825
+ const results = summary.results.map((result) => {
826
+ const task = byId.get(result.id);
827
+ const metrics = result.evaluationMetrics || {
828
+ available: false,
829
+ pluginTriggered: null,
830
+ };
831
+ const expectation = candidate ? task.expectation : "should_not_trigger";
832
+ const activationPassed =
833
+ metrics.available === true &&
834
+ (expectation === "optional" ||
835
+ (expectation === "should_trigger" &&
836
+ metrics.pluginTriggered === true) ||
837
+ (expectation === "should_not_trigger" &&
838
+ metrics.pluginTriggered === false));
839
+ return {
840
+ ...result,
841
+ source: task.source || "author",
842
+ expectation,
843
+ pluginTriggered: metrics.pluginTriggered,
844
+ activationEvidenceAvailable: metrics.available === true,
845
+ activationPassed,
846
+ effectivePass: result.pass && activationPassed,
847
+ };
848
+ });
849
+ const effectivePassed = results.filter(
850
+ (result) => result.effectivePass,
851
+ ).length;
852
+ const triggered = results.filter(
853
+ (result) => result.pluginTriggered === true,
854
+ ).length;
855
+ return {
856
+ ...summary,
857
+ results,
858
+ effectivePassed,
859
+ effectiveFailed: results.length - effectivePassed,
860
+ effectivePassRate: results.length ? effectivePassed / results.length : 0,
861
+ triggerRate: results.length ? triggered / results.length : 0,
862
+ activationEvidenceComplete: results.every(
863
+ (result) => result.activationEvidenceAvailable,
864
+ ),
865
+ ...sumUsage(results),
866
+ };
867
+ }
868
+
869
+ function reportArm(arm) {
870
+ return {
871
+ passed: arm.passed,
872
+ failed: arm.failed,
873
+ total: arm.total,
874
+ passRate: arm.passRate,
875
+ effectivePassed: arm.effectivePassed,
876
+ effectiveFailed: arm.effectiveFailed,
877
+ effectivePassRate: arm.effectivePassRate,
878
+ artifactChecksPassed: arm.artifactChecksPassed,
879
+ executionsSucceeded: arm.executionsSucceeded,
880
+ unrelatedChangeRate: arm.unrelatedChangeRate,
881
+ tasksWithUnrelatedChanges: arm.tasksWithUnrelatedChanges,
882
+ triggerRate: arm.triggerRate,
883
+ activationEvidenceComplete: arm.activationEvidenceComplete,
884
+ usage: arm.usage,
885
+ totalCostUsd: arm.totalCostUsd,
886
+ ms: arm.ms,
887
+ sampleRuns: arm.sampleRuns,
888
+ results: arm.results,
889
+ };
890
+ }
891
+
892
+ function aggregateArms(arms) {
893
+ const results = arms.flatMap((arm, sampleIndex) =>
894
+ arm.results.map((result) => ({ ...result, sample: sampleIndex + 1 })),
895
+ );
896
+ const total = results.length;
897
+ const measured = results.filter((result) =>
898
+ Array.isArray(result.unrelatedChanges),
899
+ );
900
+ const tasksWithUnrelatedChanges = measured.filter(
901
+ (result) => result.unrelatedChanges.length > 0,
902
+ ).length;
903
+ const passed = results.filter((result) => result.pass).length;
904
+ const effectivePassed = results.filter(
905
+ (result) => result.effectivePass,
906
+ ).length;
907
+ const triggered = results.filter(
908
+ (result) => result.pluginTriggered === true,
909
+ ).length;
910
+ return {
911
+ results,
912
+ passed,
913
+ failed: total - passed,
914
+ total,
915
+ passRate: total > 0 ? passed / total : 0,
916
+ effectivePassed,
917
+ effectiveFailed: total - effectivePassed,
918
+ effectivePassRate: total > 0 ? effectivePassed / total : 0,
919
+ artifactChecksPassed: results.filter((result) => result.artifactCheckPassed)
920
+ .length,
921
+ executionsSucceeded: results.filter((result) => result.executionSucceeded)
922
+ .length,
923
+ tasksWithUnrelatedChanges,
924
+ unrelatedChangeRate: measured.length
925
+ ? tasksWithUnrelatedChanges / measured.length
926
+ : 0,
927
+ triggerRate: total > 0 ? triggered / total : 0,
928
+ activationEvidenceComplete: results.every(
929
+ (result) => result.activationEvidenceAvailable,
930
+ ),
931
+ ...sumUsage(results),
932
+ ms: arms.reduce((totalMs, arm) => totalMs + arm.ms, 0),
933
+ sampleRuns: arms.length,
934
+ };
935
+ }
936
+
937
+ // Wilson score intervals stay well behaved for the small and extreme samples
938
+ // common in plugin author evals (unlike p +/- z*sqrt(p(1-p)/n)).
939
+ function wilson95(successes, total) {
940
+ if (!Number.isSafeInteger(total) || total <= 0) return null;
941
+ const z = 1.959963984540054;
942
+ const z2 = z * z;
943
+ const p = successes / total;
944
+ const denominator = 1 + z2 / total;
945
+ const center = (p + z2 / (2 * total)) / denominator;
946
+ const radius =
947
+ (z * Math.sqrt((p * (1 - p) + z2 / (4 * total)) / total)) / denominator;
948
+ return {
949
+ lower: Math.max(0, center - radius),
950
+ upper: Math.min(1, center + radius),
951
+ };
952
+ }
953
+
954
+ function confidenceComparison(control, candidate) {
955
+ const controlInterval = wilson95(control.effectivePassed, control.total);
956
+ const candidateInterval = wilson95(
957
+ candidate.effectivePassed,
958
+ candidate.total,
959
+ );
960
+ if (!controlInterval || !candidateInterval) return null;
961
+ return {
962
+ method: "wilson-95-newcombe-difference",
963
+ control: controlInterval,
964
+ candidate: candidateInterval,
965
+ passRateDelta: {
966
+ lower: candidateInterval.lower - controlInterval.upper,
967
+ upper: candidateInterval.upper - controlInterval.lower,
968
+ },
969
+ };
970
+ }
971
+
972
+ function sampleArmSequence(sampleRuns, armOrder, suiteDigest) {
973
+ if (armOrder === "control-first") {
974
+ return Array.from({ length: sampleRuns }, () => ["control", "candidate"]);
975
+ }
976
+ if (armOrder === "candidate-first") {
977
+ return Array.from({ length: sampleRuns }, () => ["candidate", "control"]);
978
+ }
979
+ // Bind the first arm to the suite digest, then alternate. This balances
980
+ // provider drift without introducing an unrecorded random source.
981
+ const startsWithCandidate =
982
+ Number.parseInt(String(suiteDigest).slice(-2), 16) % 2 === 1;
983
+ return Array.from({ length: sampleRuns }, (_, index) => {
984
+ const candidateFirst = startsWithCandidate
985
+ ? index % 2 === 0
986
+ : index % 2 === 1;
987
+ return candidateFirst ? ["candidate", "control"] : ["control", "candidate"];
988
+ });
989
+ }
990
+
991
+ export async function runPluginEval(
992
+ definition,
993
+ {
994
+ controlRunAgent,
995
+ candidateRunAgent,
996
+ runSuite = runEvalSuite,
997
+ keepWorkspaces = false,
998
+ provider = null,
999
+ model = null,
1000
+ dryRun = false,
1001
+ minPassRateDelta,
1002
+ sampleRuns,
1003
+ armOrder,
1004
+ onResult,
1005
+ } = {},
1006
+ ) {
1007
+ if (
1008
+ typeof controlRunAgent !== "function" ||
1009
+ typeof candidateRunAgent !== "function"
1010
+ ) {
1011
+ throw new TypeError("controlRunAgent and candidateRunAgent are required");
1012
+ }
1013
+ const resolvedSampleRuns = parseSampleRuns(
1014
+ sampleRuns === undefined ? definition.sampling?.runs || 1 : sampleRuns,
1015
+ "sampleRuns",
1016
+ );
1017
+ const resolvedArmOrder = parseArmOrder(
1018
+ armOrder === undefined
1019
+ ? definition.sampling?.armOrder || "balanced"
1020
+ : armOrder,
1021
+ "armOrder",
1022
+ );
1023
+ const runArm = async (arm, tasks, runAgent, sample) =>
1024
+ projectArm(
1025
+ await runSuite(tasks, {
1026
+ runAgent: instrumentRunAgent(runAgent, definition.identity),
1027
+ keepWorkspaces,
1028
+ onResult: onResult
1029
+ ? (result) => onResult({ arm, sample, result })
1030
+ : undefined,
1031
+ }),
1032
+ definition,
1033
+ { candidate: arm === "candidate" },
1034
+ );
1035
+ const armRuns = { control: [], candidate: [] };
1036
+ const sequence = sampleArmSequence(
1037
+ resolvedSampleRuns,
1038
+ resolvedArmOrder,
1039
+ definition.suiteDigest,
1040
+ );
1041
+ for (const [sampleIndex, orderedArms] of sequence.entries()) {
1042
+ for (const arm of orderedArms) {
1043
+ const candidate = arm === "candidate";
1044
+ armRuns[arm].push(
1045
+ await runArm(
1046
+ arm,
1047
+ buildTasks(definition, { candidate }),
1048
+ candidate ? candidateRunAgent : controlRunAgent,
1049
+ sampleIndex + 1,
1050
+ ),
1051
+ );
1052
+ }
1053
+ }
1054
+ const control = aggregateArms(armRuns.control);
1055
+ const candidate = aggregateArms(armRuns.candidate);
1056
+ const requiredDelta =
1057
+ minPassRateDelta === undefined
1058
+ ? definition.thresholds.minPassRateDelta
1059
+ : parseThreshold(minPassRateDelta, "minPassRateDelta", {
1060
+ min: -1,
1061
+ max: 1,
1062
+ });
1063
+ const passRateDelta = candidate.effectivePassRate - control.effectivePassRate;
1064
+ const reasons = [];
1065
+ if (dryRun) reasons.push("dry_run");
1066
+ if (
1067
+ !control.activationEvidenceComplete ||
1068
+ !candidate.activationEvidenceComplete
1069
+ ) {
1070
+ reasons.push("activation_evidence_incomplete");
1071
+ }
1072
+ if (candidate.effectivePassed !== candidate.total) {
1073
+ reasons.push("candidate_tasks_failed");
1074
+ }
1075
+ if (passRateDelta < requiredDelta) reasons.push("minimum_gain_not_met");
1076
+ if (
1077
+ candidate.unrelatedChangeRate > definition.thresholds.maxUnrelatedChangeRate
1078
+ ) {
1079
+ reasons.push("unrelated_change_threshold_exceeded");
1080
+ }
1081
+ if (
1082
+ definition.thresholds.maxCandidateCostUsd !== null &&
1083
+ (candidate.totalCostUsd === null ||
1084
+ candidate.totalCostUsd > definition.thresholds.maxCandidateCostUsd)
1085
+ ) {
1086
+ reasons.push("candidate_cost_threshold_not_met");
1087
+ }
1088
+ const uniqueReasons = [...new Set(reasons)];
1089
+ const status = dryRun
1090
+ ? "INSUFFICIENT_EVIDENCE"
1091
+ : uniqueReasons.length
1092
+ ? "FAIL"
1093
+ : "PASS";
1094
+ return {
1095
+ schema: PLUGIN_EVAL_REPORT_SCHEMA,
1096
+ scope: "local-plugin-eval",
1097
+ productionAttested: false,
1098
+ status,
1099
+ passed: status === "PASS",
1100
+ reasons: uniqueReasons,
1101
+ plugin: {
1102
+ name: definition.identity.name,
1103
+ version: definition.identity.version,
1104
+ payloadDigest: definition.payload.digest,
1105
+ payloadFiles: definition.payload.fileCount,
1106
+ payloadBytes: definition.payload.byteCount,
1107
+ },
1108
+ suite: {
1109
+ schema: definition.schema,
1110
+ path: definition.suitePath,
1111
+ digest: definition.suiteDigest,
1112
+ tasks: definition.tasks.length,
1113
+ },
1114
+ holdout: definition.holdout || null,
1115
+ selection: {
1116
+ provider,
1117
+ model,
1118
+ dryRun: dryRun === true,
1119
+ sampleRuns: resolvedSampleRuns,
1120
+ armOrder: resolvedArmOrder,
1121
+ },
1122
+ sampling: {
1123
+ runs: resolvedSampleRuns,
1124
+ armOrder: resolvedArmOrder,
1125
+ sequence,
1126
+ deterministic: true,
1127
+ },
1128
+ thresholds: {
1129
+ ...definition.thresholds,
1130
+ minPassRateDelta: requiredDelta,
1131
+ },
1132
+ comparison: {
1133
+ passRateDelta,
1134
+ outcome:
1135
+ passRateDelta > 0
1136
+ ? "gain"
1137
+ : passRateDelta < 0
1138
+ ? "regression"
1139
+ : "neutral",
1140
+ },
1141
+ statistics: {
1142
+ confidence95: confidenceComparison(control, candidate),
1143
+ },
1144
+ arms: {
1145
+ control: reportArm(control),
1146
+ candidate: reportArm(candidate),
1147
+ },
1148
+ };
1149
+ }
1150
+
1151
+ function escapeHtml(value) {
1152
+ return String(value)
1153
+ .replaceAll("&", "&amp;")
1154
+ .replaceAll("<", "&lt;")
1155
+ .replaceAll(">", "&gt;")
1156
+ .replaceAll('"', "&quot;")
1157
+ .replaceAll("'", "&#39;");
1158
+ }
1159
+
1160
+ export function renderPluginEvalHtml(report) {
1161
+ const rows = report.arms.candidate.results
1162
+ .map(
1163
+ (result) =>
1164
+ `<tr><td>${escapeHtml(result.id)}</td><td>${escapeHtml(result.expectation)}</td><td>${escapeHtml(result.pluginTriggered)}</td><td>${escapeHtml(result.effectivePass)}</td><td>${escapeHtml(result.detail || result.error || "")}</td></tr>`,
1165
+ )
1166
+ .join("");
1167
+ return [
1168
+ "<!doctype html>",
1169
+ '<html lang="en"><head><meta charset="utf-8">',
1170
+ `<title>Plugin eval: ${escapeHtml(report.plugin.name)}</title>`,
1171
+ "<style>body{font-family:system-ui,sans-serif;margin:2rem;max-width:1100px}table{border-collapse:collapse;width:100%}th,td{border:1px solid #ccc;padding:.5rem;text-align:left}code{overflow-wrap:anywhere}</style>",
1172
+ "</head><body>",
1173
+ `<h1>Plugin eval: ${escapeHtml(report.plugin.name)} ${escapeHtml(report.plugin.version)}</h1>`,
1174
+ `<p>Status: <strong>${escapeHtml(report.status)}</strong>; outcome: ${escapeHtml(report.comparison.outcome)}; pass-rate delta: ${escapeHtml(report.comparison.passRateDelta)}</p>`,
1175
+ `<p>Payload: <code>${escapeHtml(report.plugin.payloadDigest)}</code><br>Suite: <code>${escapeHtml(report.suite.digest)}</code></p>`,
1176
+ "<table><thead><tr><th>Task</th><th>Expectation</th><th>Triggered</th><th>Effective pass</th><th>Detail</th></tr></thead>",
1177
+ `<tbody>${rows}</tbody></table>`,
1178
+ `<h2>Machine-readable report</h2><pre>${escapeHtml(JSON.stringify(report, null, 2))}</pre>`,
1179
+ "</body></html>\n",
1180
+ ].join("");
1181
+ }