chainlesschain 0.162.158 → 0.162.160

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 (219) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-BwpTJTN_.js → AIOps-CkRHjW8e.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-BtQOoo3l.js → ActionButton-BP3MeNi2.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-Duf_rXZa.js → Analytics-CjbjQsOr.js} +2 -2
  6. package/src/assets/web-panel/assets/{AppLayout-XQK3wjvB.js → AppLayout-i0adH1M8.js} +5 -5
  7. package/src/assets/web-panel/assets/Artifacts-CCCG2GOU.js +1 -0
  8. package/src/assets/web-panel/assets/{Audit-DNjOGQLh.js → Audit-DrsgvwHC.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-FH9RAfB5.js → BackgroundAgents-DkXNkbZM.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-ZIbrR4-U.js → Backup-DarTWCex.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-DSt3NaND.js → BaseInput-DaDzhcto.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-CY-wvbjv.js → Chat-C4EzGw6V.js} +5 -5
  13. package/src/assets/web-panel/assets/ChatBubbleRenderer-lkavIWMS.js +1 -0
  14. package/src/assets/web-panel/assets/{Checkbox-8JVadE_B.js → Checkbox-gddNgaQi.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-CN83nPYI.js → Codegen-BKldFWf9.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-xPSvqdN8.js → Col-ChR-brDs.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-D78cnIjG.js → Community-DlHtZX86.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-CSw6Csbp.js → Compact-BAhfcBpC.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-E_HhjcYR.js → Compliance-gIrKkm6m.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-DybeQjmm.js → Cowork-BYnzLBO3.js} +3 -3
  21. package/src/assets/web-panel/assets/{Cron-DS0BJeLY.js → Cron-ByUDIljI.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-BOwVgkbs.js → Crosschain-DOdNbHFp.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-DRUr9YuX.js → DID-BJHaNWD2.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-Cht-yUKW.js → Dashboard-Drm_tClz.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-BLr_EJ9X.js → Dropdown-CA1WLzGN.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-Drexuh5W.js → EmailListRenderer-Di1SKjNO.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-D9Q2Kq63.js → FamilyGuardDashboard-44Ve7ZFe.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-C-Z1xI-T.js → Federation-CubPwv_N.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-DLHPMkQF.js → FormItemContext-CsUFe5bU.js} +1 -1
  30. package/src/assets/web-panel/assets/GenericCardRenderer-D7xRjPjZ.js +1 -0
  31. package/src/assets/web-panel/assets/{Git-syATcaBt.js → Git-D-Qx9dhO.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-CYe_r8Zf.js → Governance-_YMYaU4n.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-GWxErB7k.js → Inference-J7Yzh-K_.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-qQvVCZoi.js → KnowledgeGraph-D37iI_Mo.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-BenAU2W8.js → Logs-oMTSYfGY.js} +2 -2
  36. package/src/assets/web-panel/assets/{MarkdownRenderer-7agE3Or1.js → MarkdownRenderer-BqaPmC0W.js} +1 -1
  37. package/src/assets/web-panel/assets/{Marketplace-D4Jn5fpD.js → Marketplace-DKUqYDaF.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-DtAwoqiQ.js → McpTools-BEv3Gx61.js} +4 -4
  39. package/src/assets/web-panel/assets/{Memory-BI_O50I8.js → Memory-CtA8XD2y.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-CQWE2lqx.js → MobileBridge-ug7c1mMj.js} +1 -1
  41. package/src/assets/web-panel/assets/MobileProjects-BpX5g9It.js +1 -0
  42. package/src/assets/web-panel/assets/{Mtc-Dnr16qsn.js → Mtc-WJaMs4i2.js} +2 -2
  43. package/src/assets/web-panel/assets/{MtcAudit-CQ5Xe4-a.js → MtcAudit-DXnK_-ru.js} +2 -2
  44. package/src/assets/web-panel/assets/{Multisig-Bjr8O-wy.js → Multisig-C4Ev7726.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-CEe-KirU.js → NLProgramming-BMFb9_zk.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-spKJIKF9.js → Notes-Bl93nvBX.js} +4 -4
  47. package/src/assets/web-panel/assets/{NotificationSettings-DRp83LFG.js → NotificationSettings-BN2W5cl6.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-h1A0XB2a.js → OrderTableRenderer-Dxc9oN4o.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-BlgB0IYg.js → Organization-CAs48637.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-a_35HqZI.js → Overflow-Bhq27Zoc.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-CSNHCNPC.js → P2P-D61-2542.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-C8M1JH-1.js → PdhVaultBrowser-C1j4iUxz.js} +5 -5
  53. package/src/assets/web-panel/assets/{Permissions-DmjJi9Ms.js → Permissions-rJ2-fScM.js} +3 -3
  54. package/src/assets/web-panel/assets/{PersonalDataHub-DfuwRyf4.js → PersonalDataHub-DFIlTok6.js} +2 -2
  55. package/src/assets/web-panel/assets/{Pipeline-B2-S4ekE.js → Pipeline-BjIgEWoZ.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-B1dOiBLC.js → Privacy-BQoH5wcX.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-_pNmc-DX.js → ProjectInit-CYR_-_Ub.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-DmwG2Ii1.js → ProjectSettings-nkatCq-9.js} +2 -2
  59. package/src/assets/web-panel/assets/Projects-SS9uZKCl.js +1 -0
  60. package/src/assets/web-panel/assets/{Providers-C6HSjg1d.js → Providers-BnKM9fsF.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-CrDmS7l1.js → QrScannerModal-Cfe82dbz.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-CfKvWt-x.js → QuickAsk-r1RbPufV.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-DMa6Kuzv.js → Recommend-CjCbM2WE.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-DrktUo5x.js → RemoteSession-BuMGPH6V.js} +2 -2
  65. package/src/assets/web-panel/assets/{Reputation-BTPQ1vjL.js → Reputation-Bmj3NY1F.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-B6Q7QkMG.js → Row-BIsNcE_1.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-B1IJhAmX.js → RssFeed-BObmojD2.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-xbea1kGD.js → Search-BRmbkQNS.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-7rs0cO3F.js → Security-_rF3DRpY.js} +2 -2
  70. package/src/assets/web-panel/assets/{Services-0fjixK0S.js → Services-D0If9ZV4.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-B_rVq9Cu.js → Skeleton-o7zIksUg.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-C5uM90b9.js → Skills-oUR7zi9u.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-U4pYCIZH.js → Sla-fqKyGi03.js} +1 -1
  74. package/src/assets/web-panel/assets/{SpeechSettings-Cdz4yTWx.js → SpeechSettings-B1wZ_gxx.js} +1 -1
  75. package/src/assets/web-panel/assets/{SyncSettings-Me3aetHv.js → SyncSettings-B3qJWYXq.js} +2 -2
  76. package/src/assets/web-panel/assets/{Tasks-Cf6aNkL_.js → Tasks-BH5_dUfr.js} +1 -1
  77. package/src/assets/web-panel/assets/{Templates-CYatk4tU.js → Templates-CCIVpjRP.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-DIMKLw7o.js → Tenant-2GZtTGnB.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-C_cp-uSJ.js → Terminal-BlMkfs_u.js} +2 -2
  80. package/src/assets/web-panel/assets/{TimelineRenderer-cUPVNADp.js → TimelineRenderer-U6hi5OMc.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-CiaEIXVf.js → Tokens-C-_BerGJ.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-DB1WU5lQ.js → Trigger-DaKzBgN3.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-Cfht9Ijy.js → Trust-CLvcsco3.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-K3P4Vt9S.js → UkeySign-BFn_Y1zZ.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-DWjhjXRh.js → VideoEditing-B0V18dLT.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-D9-Q1Kan.js → Wallet-BbmP7Imu.js} +4 -4
  87. package/src/assets/web-panel/assets/{WebAuthn-BDszy-oV.js → WebAuthn-Dewj_0kt.js} +5 -5
  88. package/src/assets/web-panel/assets/{WorkflowEditor-DIhij4rA.js → WorkflowEditor-DOxz3Lwa.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-D7VCiKbh.js → chat-B1OIpZnh.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-BvOd9pVn.js → colors-CSGNVdri.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-DoqXBiAO.js → compact-item-8S0vcg0u.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-CrKRzUtp.js → createContext-vZyc27TN.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-DYEZcY55.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-BWAev4WC.js → hasIn-DWw3-UhA.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-DvDOpsJB.js → index-B4pP2ycr.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-oY45XrkG.js → index-BDHyovnK.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-CmJ6xKBO.js → index-BKEmQQvR.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BoYrS_L2.js → index-BQssf0EQ.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-ww1QtLnX.js → index-BZnauCqN.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BWWsaimP.js → index-BdyokEau.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CoD31Ndm.js → index-BeBv896m.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-DlGu7As6.js → index-BjC-TrOb.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-zH-tuuGZ.js → index-BkWd6YL7.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CEXfKSiI.js → index-BoIPHP4R.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CEPHMSih.js → index-BuB5P0TR.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-PDxGjKRu.js → index-BwKrTVV0.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BkgnGX7v.js → index-C6r27A-o.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DV-Rfmfx.js → index-CB2MOS_8.js} +3 -3
  109. package/src/assets/web-panel/assets/{index-KR1-Em9C.js → index-CFQWZvak.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-8g5TJCZ3.js → index-CI3slBKI.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BCyP071m.js → index-CJgKP2cm.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CSb-tnIo.js → index-CLPr2vC_.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-1poMpMU0.js → index-CbZqw7pg.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BZGk8AbZ.js → index-CfEvacz7.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BXyl8O23.js → index-CwXBblm-.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BHOcVYrx.js → index-D1i_8oop.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-Bq_ctRpz.js → index-D8qXVfoV.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-gmxqu5Kk.js → index-DKwl1AkQ.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-D23L1V76.js → index-DLAvUtOH.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-CDGLdn12.js → index-DLgkAhEE.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-YGDy7nNq.js → index-DPgY5GLy.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-Cx7M7PSF.js → index-Dc40pX3F.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-JzUmJMvn.js → index-DcfLXAW7.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-HwHdTusY.js → index-DeCS12Mh.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-B30NvR21.js → index-Dg0Izipr.js} +1 -1
  126. package/src/assets/web-panel/assets/index-DmVFfcNR.js +1 -0
  127. package/src/assets/web-panel/assets/{index-B0vGEbJT.js → index-DtHWHJnj.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-Bf9YlbDn.js → index-Q6oyLnKC.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-Bj7EgmOp.js → index-dxocDVD_.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-ISvPZ05r.js → index-t-q3rkVm.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-C5kRmj3P.js → index-tr1x96W9.js} +1 -1
  132. package/src/assets/web-panel/assets/index-x7YyhMUa.js +1 -0
  133. package/src/assets/web-panel/assets/{index-BN9m4Ux2.js → index-xx5M5Rbj.js} +1 -1
  134. package/src/assets/web-panel/assets/{initDefaultProps-XW-OR3mL.js → initDefaultProps-Di5YFId0.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-CeuEHh20.js → motion-BVSN0aeL.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-Dva79Keg.js → move-DHNiaP2l.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-C0B6bD2j.js → mtc-parser-xJPf3gQD.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-AkZvO-a2.js → omit-C7qzUdkN.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-B4r55qGc.js → pickAttrs-pSpjg3Le.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-BLH-z7YE.js → placementArrow-cs_jL_UC.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-ksEGZ2Wf.js → responsiveObserve-DkS_Zs4r.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-Cth4OD1Z.js → slide-B5O1Kpxo.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-9S83ezxw.js → statusUtils-BX02GnZp.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-7yQmQGZ4.js → styleChecker-EmBYRftd.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-DpGnMmPc.js → useFlexGapSupport-ZGzN1cU8.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-C4zDZIXH.js → useFs-CfP7M1m7.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-CoLICdUZ.js → usePersonalDataHub-CbIPHgNh.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-CKyH-35X.js → vnode-7xh0If_O.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-DRDGit17.js → zoom-DmitvTR2.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/command-manifest.json +17 -3
  152. package/src/commands/agent.js +72 -6
  153. package/src/commands/background-session.js +173 -3
  154. package/src/commands/browse.js +86 -0
  155. package/src/commands/cloud.js +232 -0
  156. package/src/commands/doctor.js +61 -6
  157. package/src/commands/mcp.js +107 -0
  158. package/src/commands/plugin.js +63 -1
  159. package/src/commands/routine.js +367 -0
  160. package/src/commands/session.js +396 -0
  161. package/src/data/changelog.json +12 -2
  162. package/src/harness/jsonl-session-store.js +152 -3
  163. package/src/harness/mcp-client.js +50 -0
  164. package/src/harness/session-index.js +295 -0
  165. package/src/harness/session-mirror.js +142 -0
  166. package/src/harness/transcript-integrity.js +182 -0
  167. package/src/index.js +8 -0
  168. package/src/lib/agent-sandbox.js +68 -0
  169. package/src/lib/agents.js +12 -4
  170. package/src/lib/api-key-helper.js +62 -0
  171. package/src/lib/background-agent-supervisor.js +158 -2
  172. package/src/lib/channels/channel-manager.js +126 -0
  173. package/src/lib/channels/telegram-channel.js +112 -0
  174. package/src/lib/channels/webhook-channel.js +140 -0
  175. package/src/lib/chrome-connector.js +386 -0
  176. package/src/lib/cloud/bundle.js +110 -0
  177. package/src/lib/cloud/cloud-client.js +152 -0
  178. package/src/lib/computer-use/authorization.js +173 -0
  179. package/src/lib/computer-use/computer-use-server.js +276 -0
  180. package/src/lib/computer-use/control-backend.js +150 -0
  181. package/src/lib/dependency-install-policy.js +130 -0
  182. package/src/lib/doctor-checkup.js +652 -0
  183. package/src/lib/exit-codes.cjs +77 -0
  184. package/src/lib/headless-manifest.js +175 -0
  185. package/src/lib/hook-runner.cjs +70 -4
  186. package/src/lib/ide-context-redaction.js +130 -0
  187. package/src/lib/ide-context.js +89 -13
  188. package/src/lib/jsonl-session-store.js +6 -0
  189. package/src/lib/llm-config-defaults.js +24 -8
  190. package/src/lib/lsp/lsp-server-registry.js +48 -21
  191. package/src/lib/plugin-runtime/governance.js +174 -0
  192. package/src/lib/pr-link-ledger.js +221 -0
  193. package/src/lib/project-mcp-trust.js +87 -0
  194. package/src/lib/routine-store.js +352 -0
  195. package/src/lib/sensitive-file-guard.js +51 -0
  196. package/src/lib/session-store-guard.js +40 -0
  197. package/src/lib/session-usage.js +318 -2
  198. package/src/lib/sub-agent-context.js +30 -3
  199. package/src/repl/agent-repl.js +361 -3
  200. package/src/repl/bg-dashboard.js +355 -0
  201. package/src/repl/fast-mode.js +103 -0
  202. package/src/repl/fullscreen-tui.js +197 -0
  203. package/src/repl/slash-command-registry.js +11 -3
  204. package/src/repl/voice-dictation.js +132 -0
  205. package/src/runtime/agent-core.js +344 -7
  206. package/src/runtime/coding-agent-contract-shared.cjs +107 -2
  207. package/src/runtime/coding-agent-policy.cjs +17 -0
  208. package/src/runtime/headless-runner.js +137 -14
  209. package/src/runtime/headless-stream.js +43 -9
  210. package/src/runtime/mcp-config.js +32 -1
  211. package/src/runtime/policies/agent-policy.js +3 -0
  212. package/src/assets/web-panel/assets/Artifacts-DQvJoQ6E.js +0 -1
  213. package/src/assets/web-panel/assets/ChatBubbleRenderer-BAV9V-t-.js +0 -1
  214. package/src/assets/web-panel/assets/GenericCardRenderer-DxxEdn2r.js +0 -1
  215. package/src/assets/web-panel/assets/MobileProjects-CVtTqgtg.js +0 -1
  216. package/src/assets/web-panel/assets/Projects-COATmHxF.js +0 -1
  217. package/src/assets/web-panel/assets/devWarning-nDB--2xr.js +0 -1
  218. package/src/assets/web-panel/assets/index-B1_pRTtB.js +0 -1
  219. package/src/assets/web-panel/assets/index-TMM9FuW-.js +0 -1
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Transcript integrity — hash-chained JSONL session transcripts.
3
+ *
4
+ * Every event appended to a session transcript carries `prevHash` (the hash of
5
+ * the previous event, or null for the first chained event) and `hash` — a
6
+ * SHA-256 over the previous hash plus the event's canonical core
7
+ * (`{type, timestamp, data}`). Editing, deleting, inserting, or reordering any
8
+ * chained line breaks the chain and is detected by `verifyTranscriptLines`.
9
+ *
10
+ * Design constraints:
11
+ * - Pure module: no fs, no paths — the store layer (jsonl-session-store.js)
12
+ * owns file IO so this stays cycle-free and unit-testable in isolation.
13
+ * - Backward compatible: transcripts written before chaining have no hash
14
+ * fields. A fully legacy file verifies as `legacy`; a legacy prefix followed
15
+ * by a valid chain is `partial` (chaining starts on the first upgraded
16
+ * append). An UNchained record appearing AFTER chained records is flagged as
17
+ * tampered — our writer never does that, so it is either a downgrade-write
18
+ * or a manual append, both of which the verifier must surface.
19
+ * - Honest limitation: truncating the file tail (deleting the newest N chained
20
+ * lines) is undetectable without an external anchor — the chain is
21
+ * self-contained. A half-written LAST line (crash mid-append) is reported as
22
+ * `truncatedTail: true` rather than tampered.
23
+ */
24
+
25
+ import { createHash } from "node:crypto";
26
+
27
+ export const TRANSCRIPT_CHAIN_STATUS = Object.freeze({
28
+ VERIFIED: "verified", // every record chained + chain valid
29
+ PARTIAL: "partial", // legacy prefix, then a valid chain
30
+ LEGACY: "legacy", // no chained records at all (pre-chaining transcript)
31
+ TAMPERED: "tampered", // chain broken: edit / delete / insert / reorder
32
+ EMPTY: "empty", // no records
33
+ });
34
+
35
+ /**
36
+ * Hash one event's canonical core against the previous chain hash.
37
+ * The core is rebuilt field-by-field (not the raw line) so the hash is
38
+ * independent of the presence/position of the prevHash/hash fields themselves;
39
+ * JSON.parse → JSON.stringify preserves the writer's key insertion order for
40
+ * `data`, so writer and verifier serialize identically.
41
+ */
42
+ export function computeEventHash(prevHash, core) {
43
+ const canonical = JSON.stringify({
44
+ type: core.type,
45
+ timestamp: core.timestamp,
46
+ data: core.data,
47
+ });
48
+ return createHash("sha256")
49
+ .update(`${prevHash || "genesis"}\n${canonical}`, "utf-8")
50
+ .digest("hex");
51
+ }
52
+
53
+ /**
54
+ * The hash the NEXT appended event must chain from: the `hash` of the last
55
+ * parseable line, or null when the file is empty / fully legacy / the last
56
+ * parseable line is unchained. A trailing half-written line (crash mid-append)
57
+ * is skipped so the writer chains from the last intact record.
58
+ */
59
+ export function latestChainHash(text) {
60
+ const lines = String(text || "").split("\n");
61
+ for (let i = lines.length - 1; i >= 0; i--) {
62
+ const line = lines[i];
63
+ if (!line.trim()) continue;
64
+ try {
65
+ const event = JSON.parse(line);
66
+ return typeof event?.hash === "string" ? event.hash : null;
67
+ } catch {
68
+ // half-written tail line — chain from the previous intact record
69
+ continue;
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+
75
+ /**
76
+ * Verify a transcript's hash chain.
77
+ *
78
+ * @param {string[]} lines raw transcript lines (as split on "\n")
79
+ * @returns {{
80
+ * status: string, // TRANSCRIPT_CHAIN_STATUS value
81
+ * chainedEvents: number,
82
+ * legacyEvents: number,
83
+ * malformedLines: number,
84
+ * truncatedTail: boolean, // last non-empty line is half-written JSON
85
+ * firstInvalidLine: number|null, // 1-based line number of the first break
86
+ * reason: string|null,
87
+ * }}
88
+ */
89
+ export function verifyTranscriptLines(lines) {
90
+ const input = Array.isArray(lines) ? lines : [];
91
+ let lastNonEmpty = -1;
92
+ for (let i = input.length - 1; i >= 0; i--) {
93
+ if (input[i] && input[i].trim()) {
94
+ lastNonEmpty = i;
95
+ break;
96
+ }
97
+ }
98
+
99
+ const result = {
100
+ status: TRANSCRIPT_CHAIN_STATUS.EMPTY,
101
+ chainedEvents: 0,
102
+ legacyEvents: 0,
103
+ malformedLines: 0,
104
+ truncatedTail: false,
105
+ firstInvalidLine: null,
106
+ reason: null,
107
+ };
108
+
109
+ let lastHash = null;
110
+ let sawChain = false;
111
+
112
+ const tampered = (lineNo, reason) => {
113
+ result.status = TRANSCRIPT_CHAIN_STATUS.TAMPERED;
114
+ result.firstInvalidLine = lineNo;
115
+ result.reason = reason;
116
+ return result;
117
+ };
118
+
119
+ for (let i = 0; i < input.length; i++) {
120
+ const line = input[i];
121
+ if (!line || !line.trim()) continue;
122
+ const lineNo = i + 1;
123
+
124
+ let event;
125
+ try {
126
+ event = JSON.parse(line);
127
+ } catch {
128
+ result.malformedLines++;
129
+ if (i === lastNonEmpty) {
130
+ // Crash mid-append: the newest line is half-written. Not tampering —
131
+ // report it so callers can surface a warning.
132
+ result.truncatedTail = true;
133
+ continue;
134
+ }
135
+ if (sawChain) {
136
+ return tampered(lineNo, "malformed line inside hash chain");
137
+ }
138
+ continue; // junk inside the legacy region — validate reports it
139
+ }
140
+
141
+ if (event && typeof event.hash === "string") {
142
+ const expectedPrev = sawChain ? lastHash : null;
143
+ if ((event.prevHash ?? null) !== expectedPrev) {
144
+ return tampered(
145
+ lineNo,
146
+ sawChain
147
+ ? "hash chain linkage broken (record deleted, inserted, or reordered)"
148
+ : "chain does not start at a genesis record (head records removed)",
149
+ );
150
+ }
151
+ if (computeEventHash(expectedPrev, event) !== event.hash) {
152
+ return tampered(lineNo, "event content does not match its hash");
153
+ }
154
+ sawChain = true;
155
+ lastHash = event.hash;
156
+ result.chainedEvents++;
157
+ } else {
158
+ if (sawChain) {
159
+ return tampered(
160
+ lineNo,
161
+ "unchained record after hash chain started (manual append or downgrade write)",
162
+ );
163
+ }
164
+ result.legacyEvents++;
165
+ }
166
+ }
167
+
168
+ if (result.chainedEvents > 0) {
169
+ result.status =
170
+ result.legacyEvents > 0
171
+ ? TRANSCRIPT_CHAIN_STATUS.PARTIAL
172
+ : TRANSCRIPT_CHAIN_STATUS.VERIFIED;
173
+ } else if (result.legacyEvents > 0 || result.malformedLines > 0) {
174
+ result.status = TRANSCRIPT_CHAIN_STATUS.LEGACY;
175
+ }
176
+ return result;
177
+ }
178
+
179
+ /** Convenience wrapper for whole-file text. */
180
+ export function verifyTranscriptText(text) {
181
+ return verifyTranscriptLines(String(text || "").split("\n"));
182
+ }
package/src/index.js CHANGED
@@ -159,6 +159,12 @@ import { registerRemoteControlCommand } from "./commands/remote-control.js";
159
159
  // Agenda: consumer for agent-scheduled wakeups/crons/monitors (第四阶段 #3)
160
160
  import { registerAgendaCommand } from "./commands/agenda.js";
161
161
 
162
+ // Routines: named scheduled/triggered agent tasks with run history (gap-2026-07-11 P1#8)
163
+ import { registerRoutineCommand } from "./commands/routine.js";
164
+
165
+ // Cloud: self-hosted cloud handoff — offload a task to a private runner (gap-2026-07-11 P1#7)
166
+ import { registerCloudCommand } from "./commands/cloud.js";
167
+
162
168
  // Batch: dynamic worktree batch — split a change into parallel units (第四阶段 #4)
163
169
  import { registerBatchCommand } from "./commands/batch.js";
164
170
 
@@ -597,6 +603,8 @@ export function createProgram(opts = {}) {
597
603
 
598
604
  // Agenda: fire agent-scheduled wakeups/crons/monitors
599
605
  registerAgendaCommand(program);
606
+ registerRoutineCommand(program);
607
+ registerCloudCommand(program);
600
608
 
601
609
  // Batch: dynamic worktree batch
602
610
  registerBatchCommand(program);
@@ -215,11 +215,79 @@ function executeBubblewrapShell(command, sandbox, options, hostCwd, policy) {
215
215
  };
216
216
  }
217
217
 
218
+ /**
219
+ * The run's TRUE isolation level (gap-analysis 2026-07-11 P0 "OS 级沙箱"):
220
+ * - "os-sandbox" : bubblewrap — kernel namespaces confine the child
221
+ * - "container" : docker — container boundary confines the child
222
+ * - "policy-only" : no sandbox — permission rules/shell policy are advisory
223
+ * for already-spawned subprocesses
224
+ * Surfaced in the headless init event and sandboxSummary so a caller can see
225
+ * what actually confines tool subprocesses instead of assuming.
226
+ */
227
+ export function isolationLevel(sandbox) {
228
+ if (!sandbox) return "policy-only";
229
+ if (sandbox.engine === "bubblewrap") return "os-sandbox";
230
+ if (sandbox.engine === "docker") return "container";
231
+ return "policy-only";
232
+ }
233
+
234
+ /**
235
+ * Probe whether the configured sandbox ENGINE is actually runnable on this
236
+ * host (binary present + responds to a version query). Cheap: one spawnSync
237
+ * with a short timeout; no container is started.
238
+ * @returns {{available:boolean, reason:string|null}}
239
+ */
240
+ export function probeSandboxAvailability(sandbox, deps = _deps) {
241
+ if (!sandbox) return { available: true, reason: null };
242
+ const probeArgs =
243
+ sandbox.engine === "bubblewrap"
244
+ ? ["bwrap", ["--version"]]
245
+ : ["docker", ["version", "--format", "{{.Server.Version}}"]];
246
+ const result = deps.spawnSync(probeArgs[0], probeArgs[1], {
247
+ encoding: "utf8",
248
+ timeout: 10000,
249
+ windowsHide: true,
250
+ });
251
+ if (result.error) {
252
+ return {
253
+ available: false,
254
+ reason:
255
+ result.error.code === "ENOENT"
256
+ ? `${probeArgs[0]} is not installed`
257
+ : result.error.message,
258
+ };
259
+ }
260
+ if (typeof result.status === "number" && result.status !== 0) {
261
+ return {
262
+ available: false,
263
+ reason: `${probeArgs[0]} probe exited ${result.status}: ${(result.stderr || "").trim().slice(0, 200)}`,
264
+ };
265
+ }
266
+ return { available: true, reason: null };
267
+ }
268
+
269
+ /**
270
+ * Strict mode (`sandbox.failIfUnavailable: true` in settings): refuse to
271
+ * START the agent when the configured sandbox engine is unavailable, instead
272
+ * of silently degrading per command. Throws with an actionable message; a
273
+ * no-op when the flag is unset or the sandbox is fine.
274
+ */
275
+ export function assertSandboxAvailable(sandbox, deps = _deps) {
276
+ if (!sandbox || sandbox.policy?.failIfUnavailable !== true) return;
277
+ const probe = probeSandboxAvailability(sandbox, deps);
278
+ if (!probe.available) {
279
+ throw new Error(
280
+ `sandbox.failIfUnavailable: ${sandbox.engine} sandbox is unavailable (${probe.reason}) — refusing to start. Install/start ${sandbox.engine === "bubblewrap" ? "bubblewrap" : "Docker"}, or unset failIfUnavailable to allow per-command degradation.`,
281
+ );
282
+ }
283
+ }
284
+
218
285
  export function sandboxSummary(sandbox) {
219
286
  if (!sandbox) return null;
220
287
  return {
221
288
  engine: sandbox.engine,
222
289
  image: sandbox.image,
290
+ isolationLevel: isolationLevel(sandbox),
223
291
  network: sandbox.network ? "enabled" : "disabled",
224
292
  workspace: "read-write",
225
293
  policy: {
package/src/lib/agents.js CHANGED
@@ -8,10 +8,13 @@
8
8
  * `review/security.md` is the agent `review:security`.
9
9
  *
10
10
  * Frontmatter (all optional):
11
- * name override the filename-derived name
12
- * description one-line summary (when to use this agent)
13
- * tools allow-list — comma string or YAML array; omit = inherit all
14
- * model model override for runs of this agent
11
+ * name override the filename-derived name
12
+ * description one-line summary (when to use this agent)
13
+ * tools allow-list — comma string or YAML array; omit = inherit all
14
+ * disallowedTools deny-list — removed from whatever the allow-list resolves to
15
+ * model model override for runs of this agent
16
+ * maxTurns per-run iteration cap for this agent (positive integer)
17
+ * isolation "worktree" → run in a fresh git worktree (gap 2026-07-11)
15
18
  *
16
19
  * Project scope shadows personal on a name clash. Discovery + parse are pure
17
20
  * (inject fs/path/home) so the whole thing is unit-testable.
@@ -132,13 +135,18 @@ export function parseAgentFile(file, scope, opts = {}) {
132
135
  return null;
133
136
  }
134
137
  const { data, body } = parseFrontmatter(content);
138
+ const maxTurns = Number(data.maxTurns);
135
139
  return {
136
140
  file,
137
141
  scope,
138
142
  name: data.name || null, // resolved against the path in discoverAgents
139
143
  description: data.description || "",
140
144
  tools: normalizeTools(data.tools),
145
+ disallowedTools: normalizeTools(data.disallowedTools),
141
146
  model: data.model || null,
147
+ maxTurns:
148
+ Number.isFinite(maxTurns) && maxTurns > 0 ? Math.floor(maxTurns) : null,
149
+ isolation: data.isolation === "worktree" ? "worktree" : null,
142
150
  systemPrompt: body || "",
143
151
  };
144
152
  }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `llm.apiKeyHelper` (gap-analysis 2026-07-11 P0 "依赖安装与凭据"): fetch the
3
+ * LLM API key from an external command instead of storing it in plaintext
4
+ * config.json. Point it at your OS credential store, e.g.:
5
+ *
6
+ * Windows : "powershell -NoProfile -Command \"...CredentialManager...\""
7
+ * macOS : "security find-generic-password -s cc-llm -w"
8
+ * Linux : "secret-tool lookup service cc-llm"
9
+ *
10
+ * Resolution precedence stays: --api-key > CC_API_KEY > config llm.apiKey >
11
+ * llm.apiKeyHelper. The helper's stdout (trimmed) is the key; a failing or
12
+ * empty helper resolves null so the run fails with the provider's own
13
+ * missing-key error (never a silent fallback — see the no-silent-substitution
14
+ * rule). Cached per process (5 min) so multi-call runs don't re-spawn it.
15
+ */
16
+
17
+ import { execSync } from "node:child_process";
18
+
19
+ const TTL_MS = 5 * 60 * 1000;
20
+ const _cache = new Map(); // helper command → { key, at }
21
+ let _warnedFailure = false;
22
+
23
+ /**
24
+ * @param {string} helperCmd shell command printing the key on stdout
25
+ * @param {{exec?:Function, now?:()=>number, writeErr?:(s:string)=>void}} [opts]
26
+ * @returns {string|null}
27
+ */
28
+ export function resolveApiKeyFromHelper(helperCmd, opts = {}) {
29
+ const cmd = typeof helperCmd === "string" ? helperCmd.trim() : "";
30
+ if (!cmd) return null;
31
+ const now = opts.now || Date.now;
32
+ const hit = _cache.get(cmd);
33
+ if (hit && now() - hit.at < TTL_MS) return hit.key;
34
+ const exec = opts.exec || execSync;
35
+ try {
36
+ const out = exec(cmd, {
37
+ encoding: "utf-8",
38
+ timeout: 10000,
39
+ windowsHide: true,
40
+ stdio: ["ignore", "pipe", "pipe"],
41
+ });
42
+ const key = String(out || "").trim();
43
+ if (!key) return null;
44
+ _cache.set(cmd, { key, at: now() });
45
+ return key;
46
+ } catch (err) {
47
+ // Warn once per process, then stay quiet — the provider's missing-key
48
+ // error is the actionable surface.
49
+ if (!_warnedFailure) {
50
+ _warnedFailure = true;
51
+ const writeErr = opts.writeErr || ((s) => process.stderr.write(s));
52
+ writeErr(`[api-key-helper] llm.apiKeyHelper failed: ${err.message}\n`);
53
+ }
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /** Test seam: drop the per-process cache + warn-once latch. */
59
+ export function clearApiKeyHelperCache() {
60
+ _cache.clear();
61
+ _warnedFailure = false;
62
+ }
@@ -8,6 +8,7 @@ import {
8
8
  readdirSync,
9
9
  renameSync,
10
10
  rmSync,
11
+ statSync,
11
12
  writeFileSync,
12
13
  } from "node:fs";
13
14
  import { randomBytes } from "node:crypto";
@@ -61,12 +62,57 @@ export function logPath(id) {
61
62
  return join(backgroundAgentsDir(), `${safeId(id)}.log`);
62
63
  }
63
64
 
65
+ const TERMINAL_STATUSES = new Set(["completed", "failed", "stopped", "lost"]);
66
+
64
67
  export function writeBackgroundAgentState(state) {
65
68
  const target = statePath(state.id);
69
+ // Field-aware merge against the freshest on-disk state. The state file has
70
+ // multiple concurrent read-modify-write writers (launcher, worker heartbeat/
71
+ // turn/finalize, rename/pin/stop from other processes) with last-writer-wins
72
+ // semantics — a writer holding a stale snapshot used to clobber a terminal
73
+ // status back to "running" (phantom session, real exit code lost) or roll a
74
+ // fresh rename back to the old title. Two invariants restore convergence:
75
+ // 1. a terminal status wins over a racing "running" snapshot — there is no
76
+ // legitimate same-id terminal→running transition (resume mints a NEW
77
+ // id, and the worker's own writeHeartbeat already refuses to resurrect);
78
+ // 2. the newest rename/pin (by renamedAt/pinnedAt) wins regardless of
79
+ // which writer's snapshot carries it.
80
+ // The read happens immediately before the atomic rename, shrinking the
81
+ // clobber window from "any caller's RMW span" to microseconds; rename/pin
82
+ // additionally verify-and-retry on top of this.
83
+ let next = state;
84
+ const current = readBackgroundAgentState(state.id);
85
+ if (current) {
86
+ if (TERMINAL_STATUSES.has(current.status) && next.status === "running") {
87
+ next = {
88
+ ...next,
89
+ status: current.status,
90
+ endedAt: current.endedAt ?? next.endedAt ?? null,
91
+ exitCode: current.exitCode ?? next.exitCode ?? null,
92
+ ...(current.signal !== undefined ? { signal: current.signal } : {}),
93
+ ...(current.error !== undefined ? { error: current.error } : {}),
94
+ ...(current.lostReason !== undefined
95
+ ? { lostReason: current.lostReason }
96
+ : {}),
97
+ ...(current.stoppedByUser !== undefined
98
+ ? { stoppedByUser: current.stoppedByUser }
99
+ : {}),
100
+ // terminal sessions have no live phase or transport endpoint
101
+ phase: null,
102
+ transport: null,
103
+ };
104
+ }
105
+ if (Number(current.renamedAt || 0) > Number(next.renamedAt || 0)) {
106
+ next = { ...next, title: current.title, renamedAt: current.renamedAt };
107
+ }
108
+ if (Number(current.pinnedAt || 0) > Number(next.pinnedAt || 0)) {
109
+ next = { ...next, pinned: current.pinned, pinnedAt: current.pinnedAt };
110
+ }
111
+ }
66
112
  const tmp = `${target}.${process.pid}.tmp`;
67
- writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 0o600 });
113
+ writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
68
114
  renameSync(tmp, target);
69
- return state;
115
+ return next;
70
116
  }
71
117
 
72
118
  export function readBackgroundAgentState(id) {
@@ -177,6 +223,66 @@ export function renameBackgroundAgent(id, title, options = {}) {
177
223
  return next;
178
224
  }
179
225
 
226
+ /**
227
+ * Remove a background agent's RECORD (state file + log) — `cc daemon rm`
228
+ * (gap-analysis 2026-07-11 P0 "后台 Agent Supervisor" 补 rm 动词). Terminal
229
+ * sessions (completed/failed/stopped/lost) remove directly; a still-running
230
+ * one is refused unless `force`, which stops it first. The underlying JSONL
231
+ * conversation session is NOT touched — that's `cc session delete`.
232
+ */
233
+ export function removeBackgroundAgent(id, options = {}) {
234
+ const state = effectiveBackgroundAgentState(readBackgroundAgentState(id), {
235
+ now: options.now,
236
+ heartbeatStaleMs: options.heartbeatStaleMs,
237
+ });
238
+ if (!state) throw new Error(`Background agent not found: ${id}`);
239
+ if (state.status === "running") {
240
+ if (options.force !== true) {
241
+ throw new Error(
242
+ `${id} is still running — stop it first (cc daemon stop ${id}) or pass --force`,
243
+ );
244
+ }
245
+ try {
246
+ stopBackgroundAgent(id);
247
+ } catch {
248
+ /* best-effort — the record removal below is the point of rm --force */
249
+ }
250
+ }
251
+ rmSync(statePath(id), { force: true });
252
+ if (options.keepLog !== true) {
253
+ rmSync(logPath(id), { force: true });
254
+ }
255
+ return { id, removed: true, status: state.status };
256
+ }
257
+
258
+ /**
259
+ * Pin/unpin a session for the dashboard (`cc daemon view`) — pinned sessions
260
+ * sort first inside their group. Same read-modify-write + verify-retry dance
261
+ * as rename: the worker's periodic state merges can clobber a write that
262
+ * lands inside its read→write window.
263
+ */
264
+ export function setBackgroundAgentPinned(id, pinned, options = {}) {
265
+ const state = effectiveBackgroundAgentState(readBackgroundAgentState(id), {
266
+ now: options.now,
267
+ heartbeatStaleMs: options.heartbeatStaleMs,
268
+ });
269
+ if (!state) throw new Error(`Background agent not found: ${id}`);
270
+ const value = pinned === true;
271
+ const pinnedAt = typeof options.now === "number" ? options.now : Date.now();
272
+ let next = { ...state, pinned: value, pinnedAt };
273
+ for (let attempt = 0; attempt < 4; attempt++) {
274
+ writeBackgroundAgentState(next);
275
+ sleepSyncMs(15);
276
+ const current = readBackgroundAgentState(id) || next;
277
+ if (current.pinned === value) {
278
+ return { ...current, pinned: value, pinnedAt };
279
+ }
280
+ next = { ...current, pinned: value, pinnedAt };
281
+ }
282
+ writeBackgroundAgentState(next);
283
+ return next;
284
+ }
285
+
180
286
  /**
181
287
  * Build the follow-up argv template for interactive attach turns: the launch
182
288
  * argv minus every token that carried the FIRST turn's prompt (positional
@@ -258,6 +364,33 @@ export function resumeBackgroundAgent(id, prompt, options = {}) {
258
364
  });
259
365
  }
260
366
 
367
+ /**
368
+ * Fail fast on an unusable cwd. Without this, spawn() surfaces a bad cwd as
369
+ * an ASYNC 'error' event on the detached child — which nothing listened to
370
+ * (uncaught exception with no context) — and the pre-written state/job files
371
+ * stayed behind as a phantom "running" session. Deleted, file-replaced, and
372
+ * unmounted paths all land here with one clear message.
373
+ */
374
+ function assertUsableCwd(cwd) {
375
+ let reason = null;
376
+ if (!cwd) {
377
+ reason = "no working directory given";
378
+ } else if (!existsSync(cwd)) {
379
+ reason = "directory does not exist (deleted or unmounted?)";
380
+ } else {
381
+ try {
382
+ if (!statSync(cwd).isDirectory()) reason = "path is not a directory";
383
+ } catch (err) {
384
+ reason = `directory is not accessible (${err.code || err.message})`;
385
+ }
386
+ }
387
+ if (reason) {
388
+ throw new Error(
389
+ `Cannot launch background agent: cwd "${cwd ?? ""}" — ${reason}`,
390
+ );
391
+ }
392
+ }
393
+
261
394
  export function launchBackgroundAgent({
262
395
  argv,
263
396
  cwd,
@@ -266,6 +399,7 @@ export function launchBackgroundAgent({
266
399
  cliEntry,
267
400
  followUpArgv,
268
401
  }) {
402
+ assertUsableCwd(cwd);
269
403
  const id = createBackgroundAgentId();
270
404
  const dir = backgroundAgentsDir();
271
405
  const jobFile = join(dir, `${id}.job.${process.pid}.json`);
@@ -317,6 +451,28 @@ export function launchBackgroundAgent({
317
451
  rmSync(statePath(id), { force: true });
318
452
  throw error;
319
453
  }
454
+ // Async spawn failures (EPERM, cwd raced away between the check and the
455
+ // spawn, …) arrive as an 'error' event on the detached child. Reap them
456
+ // into the state file instead of leaving a phantom "running" session and
457
+ // an uncaught exception.
458
+ if (typeof child.on === "function") {
459
+ child.on("error", (error) => {
460
+ try {
461
+ rmSync(jobFile, { force: true });
462
+ const current = readBackgroundAgentState(id);
463
+ if (current && current.status === "running") {
464
+ writeBackgroundAgentState({
465
+ ...current,
466
+ status: "failed",
467
+ endedAt: Date.now(),
468
+ lostReason: `spawn-error: ${error.code || error.message}`,
469
+ });
470
+ }
471
+ } catch {
472
+ /* best-effort */
473
+ }
474
+ });
475
+ }
320
476
  child.unref();
321
477
  const current = readBackgroundAgentState(id) || state;
322
478
  const withPid = {