chainlesschain 0.162.166 → 0.162.168

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 (185) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-LbDx7jL6.js → AIOps-CDzzuWZV.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-DnurtK34.js → ActionButton-Dy-8MmQi.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-DXSsV1am.js → Analytics-BtJ_VWkx.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-dqRpzNuD.js → AppLayout-DkjiI4VS.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-BwdmYepK.js → Artifacts-h8PyDO86.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-oSX0kOUG.js → Audit-DiDV0C6l.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-C2KxJahT.js → BackgroundAgents-DTw_aOaf.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-B4TkxWkC.js → Backup-CihFuUO9.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-CX5-KxeU.js → BaseInput-CqP2QNoR.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-O_HzD-PL.js → Chat-tm-ygX-p.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-Br8saHQA.js → ChatBubbleRenderer-BVKEhj_y.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-ChepDpC1.js → Checkbox-BFMyRN3Q.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-D9z8kc7q.js → Codegen-B1gTeoAS.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-B-H5eoNw.js → Col-1GP8MvKL.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-Ccyp_Kk5.js → Community-BLY938F6.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-BNvBznp_.js → Compact-DVt_p36I.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-D1FjRd8n.js → Compliance-DxqEBXVG.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-CV9B9u-8.js → Cowork-CyWrM-T8.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-BowJ4VIx.js → Cron-Domu6aI9.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-BJc6jzRF.js → Crosschain-C8LKpjHJ.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BEp8NV6D.js → DID-XBy15RZ5.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-Dls0B3YB.js → Dashboard-DDAyPD-V.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-DQ7zXVtD.js → Dropdown-BHV0MZ13.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-B9q79tOl.js → EmailListRenderer-DmFGW4PX.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CD8Ie-88.js → FamilyGuardDashboard-CqYdbKvd.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-5o7XU5Vf.js → Federation-YP5-Puv5.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-DeKbMvOs.js → FormItemContext-BOmcIcTe.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-CZiQvz_z.js → GenericCardRenderer-CvTZdoc4.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-CTvFxVV8.js → Git-Q2oxua0L.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-DwzFIjHt.js → Governance-D-WpyWEU.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CJBk59c-.js → Inference-NUtsYHQo.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-DfsaXN2z.js → KnowledgeGraph-BHQLa8fr.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-DVRYf18S.js → Logs-6WsR5Q1G.js} +2 -2
  35. package/src/assets/web-panel/assets/MarkdownRenderer-DuWoDwMx.js +1 -0
  36. package/src/assets/web-panel/assets/{Marketplace-BuIwyE4t.js → Marketplace-CigohRRW.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-D3U0o78E.js → McpTools-jcp6jVLf.js} +5 -5
  38. package/src/assets/web-panel/assets/{Memory-QYVmyb3b.js → Memory-B00NU1ph.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-BFgrj6eZ.js → MobileBridge-Cs_p3PzU.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-BFs2WljZ.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-BZQu8TcH.js → Mtc-CSEflwzw.js} +5 -5
  42. package/src/assets/web-panel/assets/{MtcAudit-BnFfEkLo.js → MtcAudit-dxSiEZ96.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-iUnNS9Cm.js → Multisig-CDAFEAyq.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-DNNz1y9Y.js → NLProgramming-HoFpZNiB.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-CZezNYRU.js → Notes-tAoQvVcE.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-DBvrplIR.js → NotificationSettings-Czo2M9F4.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-CTDtee49.js → OrderTableRenderer-CM1kNTp6.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-CCOUJX1R.js → Organization-Cudr0VOC.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-DcZghdk0.js → Overflow-BSXeDKiN.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-jmddWX9q.js → P2P-Cqv_g-F9.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-wfjMcczl.js → PdhVaultBrowser-7rjprPh8.js} +4 -4
  52. package/src/assets/web-panel/assets/{Permissions-DU1GJjca.js → Permissions-97pIDQKE.js} +3 -3
  53. package/src/assets/web-panel/assets/{PersonalDataHub-BzTMzDF9.js → PersonalDataHub-bparK9eQ.js} +3 -3
  54. package/src/assets/web-panel/assets/{Pipeline-DPmjlpVc.js → Pipeline-CJgerM09.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-B6HRLpfK.js → Privacy-JqFS3PN7.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-Cyy86AsN.js → ProjectInit-DMITd-2c.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-Bl_WQxJ5.js → ProjectSettings-1UkdJ8Ii.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-Co9yKThA.js → Projects-CYZqegqw.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-DLa4NlIs.js → Providers-ChfXz3U0.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-BvzM7q5f.js → QrScannerModal-Rzkka-mf.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-BN8yyyUj.js → QuickAsk-DEz9HaON.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-QsVN4w0i.js → Recommend-EFGP8AVu.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-tkDXw-TY.js → RemoteSession-6um0lGGy.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-DAifkGsy.js → Reputation-CA1BXTup.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-D7dZEyak.js → Row-stTnDJ9y.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-DB5sImTA.js → RssFeed-B10qCCoG.js} +2 -2
  67. package/src/assets/web-panel/assets/{Search-3py3TO9V.js → Search-DSJroZ6p.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-DhNUfT_k.js → Security-BpXkwiMW.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-DjJAA_5D.js → Services-BPQxjwK6.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-u95JjK3n.js → Skeleton-CfbKAMul.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-DPo3L0Tz.js → Skills-CttYTfX0.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-DWhkAZV1.js → Sla-ByluHich.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-PUegMtqV.js → SpeechSettings-BdZXKsfV.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-BMNYrnrU.js → SyncSettings-xCR7iDzy.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-CYm_oTJC.js → Tasks-qTko_INn.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-Dw9Er5DN.js → Templates-CkIzhgDJ.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-7P3QIBSo.js → Tenant-BIaCaaK8.js} +1 -1
  78. package/src/assets/web-panel/assets/Terminal-B2Qq38XP.js +3 -0
  79. package/src/assets/web-panel/assets/{TimelineRenderer-DbCd1bDH.js → TimelineRenderer-D5eQDnEF.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-EQofpA0t.js → Tokens-DmeJ8NMQ.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-ChNLLypd.js → Trigger-C5sgb3C7.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-dlVGUKs5.js → Trust-H2vOb3Rb.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-BXyBUhNF.js → UkeySign-NmPtHK2u.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-BE8-68ge.js → VideoEditing-DHv-Ti7K.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-7ZIJOwNg.js → Wallet-BYZ3-PRP.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-DgNd2Amr.js → WebAuthn-BaSlP7om.js} +4 -4
  87. package/src/assets/web-panel/assets/{WorkflowEditor-Z-Un-9l5.js → WorkflowEditor-CZ7IHuIi.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-CtVDEkUc.js → chat-Dd9kY8gn.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-PXtJ2f1B.js → colors-yhzo6QSV.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-oN96nuBi.js → compact-item-C1JneVdM.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-okCJXQXa.js → createContext-C_waVCg2.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-DaDlyVDY.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-pcHm--4o.js → hasIn-CkVg0Xnh.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-BeAWCGEK.js → index-1VYhIr6D.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-DC0bqrqV.js → index-BTMiVIBI.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CQYes2mk.js → index-BYOyb5X2.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-vW0lwj0Y.js → index-BjDnI7oM.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BUUJ2WfG.js → index-BlJNvg-2.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-C0rfJBU3.js → index-BlzbSUK9.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Rk0X8lVz.js → index-BrN2hAeE.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BDR2mi1p.js → index-BsSsurcn.js} +1 -1
  102. package/src/assets/web-panel/assets/index-BtsLExcc.js +1 -0
  103. package/src/assets/web-panel/assets/{index-jAY55MSx.js → index-Bwuu3Bf3.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-_SaFHNsG.js → index-Bx3A6X4J.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-T3QE8LuV.js → index-C-Sscgnv.js} +1 -1
  106. package/src/assets/web-panel/assets/index-C3qDvvIb.js +1 -0
  107. package/src/assets/web-panel/assets/{index-BUTyx-Lk.js → index-C4HNdMCt.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-nMqLdwao.js → index-CBwmwTNa.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-BuME9k4Y.js → index-CErewFeT.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-BHJIfdCV.js → index-CGtoL5mR.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-D0bfypsF.js → index-CUDjGt_V.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-Bl6N7ZyX.js → index-C_qqYZT7.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CXhezK4d.js → index-C_riAa6d.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BnhHxZul.js → index-CfEMjbhu.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Cs1fGk1Z.js → index-ClQhh6v3.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BjVIZfdA.js → index-CmiLmG0i.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CWUJlCvK.js → index-D1xIbAxQ.js} +3 -3
  118. package/src/assets/web-panel/assets/{index-DfL10EcN.js → index-D6UXDH2D.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-C4ravjdv.js → index-D7kQ_4uL.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DtsyPnF5.js → index-DPkqQ2Pr.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CQ1ZzJjN.js → index-DUA96wEj.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-5aO2V0V4.js → index-DXHk9Vof.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CopGaCI_.js → index-DapftLUr.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-D1efSh1X.js → index-Dez7TUgG.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DBcKBb1E.js → index-DhZgULsY.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-Bht9n8Sm.js → index-Diquc7-l.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CAQktO-6.js → index-DuGtLiTW.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CK0H3pQW.js → index-U_nLXGIx.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BZKk4Lnm.js → index-_M8El5iz.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-Cc_MkksW.js → index-_VmzCYFn.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-mB1UV7wA.js → index-_rZxgFtc.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-BEtzqJw3.js → index-ewb0-LoH.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-CG1jyiI7.js → initDefaultProps-DNN6qtGa.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-C8BYa98D.js → motion-BcM9exeo.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-D1hqwitW.js → move-DsKJ-YvA.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-C2lol8rp.js → mtc-parser-glgGkxaK.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-Ct6Zghfi.js → omit-BwHCU50S.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-CSYbYfdc.js → pickAttrs-BVp-bLt7.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-WCk8di0z.js → placementArrow-CZNe0r-w.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-CEC8CZ-3.js → responsiveObserve-DUafj9pU.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-BsXyd7Lq.js → slide-H4NqXQxu.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-tDKldiUT.js → statusUtils-UjMzzGZ5.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-BIS2unhx.js → styleChecker-qO5aCjyk.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-yFTVMDUv.js → useFlexGapSupport-ChecYwC8.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-CZYbJwkO.js → useFs-BR0QvwO3.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-sbEHRLAM.js → usePersonalDataHub-DitK2Qgz.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-BVjeSEQK.js → vnode-dxSb40dY.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-BZKVvEZ6.js → zoom-ekJk8YB2.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agent.js +75 -0
  151. package/src/commands/session.js +132 -0
  152. package/src/data/changelog.json +8 -2
  153. package/src/harness/jsonl-session-store.js +9 -0
  154. package/src/harness/session-mirror.js +212 -6
  155. package/src/harness/worktree-isolator.js +10 -2
  156. package/src/lib/agent-core.js +1 -0
  157. package/src/lib/async-hook-queue.cjs +288 -0
  158. package/src/lib/async-hook-supervisor.cjs +52 -0
  159. package/src/lib/background-phase-reporter.js +116 -0
  160. package/src/lib/doctor-checkup.js +184 -4
  161. package/src/lib/hook-runner.cjs +248 -36
  162. package/src/lib/install-command-policy.js +343 -0
  163. package/src/lib/jsonl-session-store.js +1 -0
  164. package/src/lib/lsp/__tests__/lsp-server-registry.test.js +27 -0
  165. package/src/lib/lsp/code-intelligence.js +5 -1
  166. package/src/lib/lsp/lsp-server-registry.js +14 -0
  167. package/src/lib/lsp/workspace-roots.js +115 -0
  168. package/src/lib/runtime-checkup.js +33 -0
  169. package/src/lib/settings-hook-events.cjs +173 -3
  170. package/src/lib/settings-hooks.cjs +4 -0
  171. package/src/lib/shell-approval.js +47 -1
  172. package/src/lib/sub-agent-context.js +13 -0
  173. package/src/repl/add-dir.js +59 -1
  174. package/src/repl/agent-repl.js +85 -3
  175. package/src/runtime/agent-core.js +411 -82
  176. package/src/runtime/headless-runner.js +111 -9
  177. package/src/runtime/headless-stream.js +39 -0
  178. package/src/runtime/system-prompt.js +39 -12
  179. package/src/workers/background-agent-worker.js +8 -0
  180. package/src/assets/web-panel/assets/MarkdownRenderer-BL_WsWZM.js +0 -1
  181. package/src/assets/web-panel/assets/MobileProjects-Bp5QB-46.js +0 -1
  182. package/src/assets/web-panel/assets/Terminal-ClPrfWW7.js +0 -3
  183. package/src/assets/web-panel/assets/devWarning-BzV6dk1B.js +0 -1
  184. package/src/assets/web-panel/assets/index-BFrAwXHp.js +0 -1
  185. package/src/assets/web-panel/assets/index-DJQFP47u.js +0 -1
@@ -0,0 +1,288 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * async-hook-queue — a small, best-effort, PERSISTENT queue for async settings
5
+ * hooks that opted into `asyncRewake: true` and finished in a FAILURE state.
6
+ *
7
+ * Why: AsyncHookSupervisor runs `async: true` hooks fire-and-forget and keeps
8
+ * their results ONLY in memory (`_results`/`_rewakes`). A rewake is the one
9
+ * actionable signal it produces — "a background check (e.g. `npm test`) failed,
10
+ * re-engage the agent with the error". If the process dies (crash, kill,
11
+ * hard exit) after the rewake was recorded but BEFORE the turn loop drained and
12
+ * surfaced it, that signal is silently lost: the next `--resume` of the same
13
+ * session has no idea a background check failed.
14
+ *
15
+ * This module gives the supervisor a durable place to park undrained rewakes,
16
+ * keyed by session id, so `--resume <id>` can recover them. The gap's P2 "Hooks"
17
+ * item calls for an "async hook 持久队列"; this is the reliability half of it
18
+ * (paired with the already-shipped `allowManagedHooksOnly`).
19
+ *
20
+ * Lifecycle (rewake-scoped on purpose — non-rewake results are informational and
21
+ * cheap to lose; rewakes are the failure signal worth surviving a crash):
22
+ * record a rewake → appendRewake() (immediate write, survives even SIGKILL)
23
+ * drain / surface → removeRewakes() (consumed → no longer pending)
24
+ * resume a session → takePending() (load + clear that session's bucket)
25
+ *
26
+ * Design mirrors hook-stats-store.cjs: pure folds (no clock/RNG/fs inside), an
27
+ * injected fs for load/save, atomic tmp+rename writes, and per-session +
28
+ * per-session-count bounds so a runaway producer never grows the file without
29
+ * bound. Every path is best-effort — a corrupt/io-failed queue file must never
30
+ * block a hook or the agent.
31
+ */
32
+
33
+ const os = require("node:os");
34
+ const path = require("node:path");
35
+ const fsDefault = require("node:fs");
36
+
37
+ const CONFIG_DIR_NAME = ".chainlesschain";
38
+ const MAX_RECORDS_PER_SESSION = 200; // drop oldest-recorded beyond this
39
+ const MAX_SESSIONS = 100; // bound distinct session buckets
40
+ const DEFAULT_STALE_MS = 7 * 24 * 60 * 60 * 1000; // prune buckets untouched >7d
41
+
42
+ /** Default on-disk location (matches the ESM getHomeDir() layout). */
43
+ function defaultAsyncHookQueuePath() {
44
+ return path.join(os.homedir(), CONFIG_DIR_NAME, "async-hook-queue.json");
45
+ }
46
+
47
+ /**
48
+ * Stable de-dupe key for one rewake record. Two records with the same
49
+ * (command, event, ts, ms, exitCode) are treated as the same pending item —
50
+ * enough to make removeRewakes() idempotent and appendRewake() not double-store
51
+ * a record that a caller replays. Pure, no hashing needed.
52
+ */
53
+ function queueEntryKey(rec) {
54
+ if (!rec || typeof rec !== "object") return "";
55
+ return [
56
+ String(rec.command || ""),
57
+ String(rec.event || ""),
58
+ String(rec.ts ?? ""),
59
+ String(rec.ms ?? ""),
60
+ String(rec.exitCode ?? ""),
61
+ ].join("|");
62
+ }
63
+
64
+ /** Normalize a raw supervisor record into a compact stored rewake entry. */
65
+ function normalizeRecord(rec) {
66
+ if (!rec || typeof rec !== "object") return null;
67
+ const command = String(rec.command || "").trim();
68
+ if (!command) return null;
69
+ return {
70
+ command,
71
+ event: rec.event ?? null,
72
+ exitCode: rec.exitCode ?? null,
73
+ error: rec.error ? String(rec.error) : null,
74
+ additionalContext: rec.additionalContext
75
+ ? String(rec.additionalContext)
76
+ : null,
77
+ blocked: !!rec.blocked,
78
+ ts: Number.isFinite(Number(rec.ts)) ? Number(rec.ts) : 0,
79
+ ms: Number.isFinite(Number(rec.ms)) ? Number(rec.ms) : 0,
80
+ };
81
+ }
82
+
83
+ /** Fresh empty queue. */
84
+ function emptyQueue() {
85
+ return { sessions: {} };
86
+ }
87
+
88
+ /** Coerce an arbitrary parsed value into the queue shape. */
89
+ function coerceQueue(parsed) {
90
+ if (
91
+ !parsed ||
92
+ typeof parsed !== "object" ||
93
+ typeof parsed.sessions !== "object" ||
94
+ !parsed.sessions
95
+ ) {
96
+ return emptyQueue();
97
+ }
98
+ return { sessions: parsed.sessions };
99
+ }
100
+
101
+ /**
102
+ * PURE fold: append `records` into `queue` under `sessionId` (mutates + returns
103
+ * `queue`). De-dupes by queueEntryKey, keeps the most-recent MAX_RECORDS_PER_
104
+ * SESSION, and stamps the bucket's `updatedAt` for stale pruning. `now` is
105
+ * supplied by the caller so the fold stays clock-free.
106
+ */
107
+ function foldAppend(queue, sessionId, records, now) {
108
+ if (!queue || typeof queue !== "object") queue = emptyQueue();
109
+ if (!queue.sessions) queue.sessions = {};
110
+ const id = String(sessionId || "").trim();
111
+ if (!id) return queue;
112
+ const list = Array.isArray(records) ? records : [records];
113
+ const bucket = queue.sessions[id] || { records: [], updatedAt: 0 };
114
+ const seen = new Set(bucket.records.map(queueEntryKey));
115
+ for (const raw of list) {
116
+ const rec = normalizeRecord(raw);
117
+ if (!rec) continue;
118
+ const key = queueEntryKey(rec);
119
+ if (seen.has(key)) continue;
120
+ seen.add(key);
121
+ bucket.records.push(rec);
122
+ }
123
+ // Never persist an empty bucket (all-invalid input, or nothing new for a
124
+ // session that had none) — it would just be dead weight in the file.
125
+ if (bucket.records.length === 0) return queue;
126
+ if (bucket.records.length > MAX_RECORDS_PER_SESSION) {
127
+ bucket.records = bucket.records.slice(
128
+ bucket.records.length - MAX_RECORDS_PER_SESSION,
129
+ );
130
+ }
131
+ bucket.updatedAt = Number.isFinite(Number(now))
132
+ ? Number(now)
133
+ : bucket.updatedAt;
134
+ queue.sessions[id] = bucket;
135
+ boundSessions(queue);
136
+ return queue;
137
+ }
138
+
139
+ /**
140
+ * PURE fold: remove `records` (by key) from `sessionId`'s bucket (mutates +
141
+ * returns `queue`). Empties → the bucket is deleted so the file self-cleans.
142
+ */
143
+ function foldRemove(queue, sessionId, records) {
144
+ if (!queue || !queue.sessions) return queue || emptyQueue();
145
+ const id = String(sessionId || "").trim();
146
+ const bucket = queue.sessions[id];
147
+ if (!bucket) return queue;
148
+ const kill = new Set(
149
+ (Array.isArray(records) ? records : [records])
150
+ .map(normalizeRecord)
151
+ .filter(Boolean)
152
+ .map(queueEntryKey),
153
+ );
154
+ bucket.records = bucket.records.filter((r) => !kill.has(queueEntryKey(r)));
155
+ if (bucket.records.length === 0) delete queue.sessions[id];
156
+ return queue;
157
+ }
158
+
159
+ /** Keep only the MAX_SESSIONS most-recently-updated buckets. */
160
+ function boundSessions(queue) {
161
+ if (!queue || !queue.sessions) return queue;
162
+ const ids = Object.keys(queue.sessions);
163
+ if (ids.length <= MAX_SESSIONS) return queue;
164
+ ids
165
+ .sort(
166
+ (a, b) =>
167
+ (queue.sessions[a].updatedAt || 0) - (queue.sessions[b].updatedAt || 0),
168
+ )
169
+ .slice(0, ids.length - MAX_SESSIONS)
170
+ .forEach((id) => delete queue.sessions[id]);
171
+ return queue;
172
+ }
173
+
174
+ /**
175
+ * PURE fold: drop buckets whose `updatedAt` is older than `now - staleMs`
176
+ * (mutates + returns `queue`). Guards the file against unbounded growth from
177
+ * sessions that were never resumed to drain their pending rewakes.
178
+ */
179
+ function foldPruneStale(queue, now, staleMs = DEFAULT_STALE_MS) {
180
+ if (!queue || !queue.sessions) return queue || emptyQueue();
181
+ const cutoff = (Number.isFinite(Number(now)) ? Number(now) : 0) - staleMs;
182
+ for (const [id, bucket] of Object.entries(queue.sessions)) {
183
+ if (!bucket || (bucket.updatedAt || 0) < cutoff) delete queue.sessions[id];
184
+ }
185
+ return queue;
186
+ }
187
+
188
+ /** Best-effort read of the on-disk queue (→ empty on missing/corrupt). */
189
+ function loadQueue(file = defaultAsyncHookQueuePath(), fs = fsDefault) {
190
+ try {
191
+ if (!fs.existsSync(file)) return emptyQueue();
192
+ return coerceQueue(JSON.parse(fs.readFileSync(file, "utf-8")));
193
+ } catch {
194
+ return emptyQueue();
195
+ }
196
+ }
197
+
198
+ /** Best-effort atomic write (tmp + rename). Never throws. */
199
+ function saveQueue(queue, file = defaultAsyncHookQueuePath(), fs = fsDefault) {
200
+ try {
201
+ const dir = path.dirname(file);
202
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
203
+ const tmp = `${file}.tmp`;
204
+ fs.writeFileSync(tmp, JSON.stringify(queue), "utf-8");
205
+ fs.renameSync(tmp, file);
206
+ return true;
207
+ } catch {
208
+ return false;
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Append one/more rewake records for `sessionId` and write back. Best-effort;
214
+ * returns true on a successful write. `now` defaults to Date.now() (the only
215
+ * non-pure entry point besides load/save).
216
+ */
217
+ function appendRewake(
218
+ { sessionId, records, now } = {},
219
+ file = defaultAsyncHookQueuePath(),
220
+ fs = fsDefault,
221
+ ) {
222
+ const id = String(sessionId || "").trim();
223
+ if (!id || records == null) return false;
224
+ const at = Number.isFinite(Number(now)) ? Number(now) : Date.now();
225
+ const queue = foldAppend(loadQueue(file, fs), id, records, at);
226
+ return saveQueue(queue, file, fs);
227
+ }
228
+
229
+ /**
230
+ * Remove (mark consumed) rewake records for `sessionId` and write back.
231
+ * Best-effort; a no-op when nothing matches.
232
+ */
233
+ function removeRewakes(
234
+ { sessionId, records } = {},
235
+ file = defaultAsyncHookQueuePath(),
236
+ fs = fsDefault,
237
+ ) {
238
+ const id = String(sessionId || "").trim();
239
+ if (!id || records == null) return false;
240
+ const queue = foldRemove(loadQueue(file, fs), id, records);
241
+ return saveQueue(queue, file, fs);
242
+ }
243
+
244
+ /**
245
+ * Load a session's pending rewakes AND clear its bucket (also prunes stale
246
+ * buckets while we hold the file). Returns the recovered records (possibly []).
247
+ * The single call a resuming runner makes on startup.
248
+ */
249
+ function takePending(
250
+ { sessionId, now, staleMs } = {},
251
+ file = defaultAsyncHookQueuePath(),
252
+ fs = fsDefault,
253
+ ) {
254
+ const id = String(sessionId || "").trim();
255
+ if (!id) return [];
256
+ const at = Number.isFinite(Number(now)) ? Number(now) : Date.now();
257
+ let queue = loadQueue(file, fs);
258
+ const bucket = queue.sessions[id];
259
+ const recovered =
260
+ bucket && Array.isArray(bucket.records) ? bucket.records : [];
261
+ if (bucket) delete queue.sessions[id];
262
+ queue = foldPruneStale(queue, at, staleMs);
263
+ // Only rewrite if we actually changed something (recovered a bucket or the
264
+ // prune dropped one) — a pure read of an empty/unrelated queue leaves the
265
+ // file byte-unchanged.
266
+ if (recovered.length > 0 || bucket) saveQueue(queue, file, fs);
267
+ return recovered;
268
+ }
269
+
270
+ module.exports = {
271
+ defaultAsyncHookQueuePath,
272
+ queueEntryKey,
273
+ normalizeRecord,
274
+ emptyQueue,
275
+ coerceQueue,
276
+ foldAppend,
277
+ foldRemove,
278
+ foldPruneStale,
279
+ boundSessions,
280
+ loadQueue,
281
+ saveQueue,
282
+ appendRewake,
283
+ removeRewakes,
284
+ takePending,
285
+ MAX_RECORDS_PER_SESSION,
286
+ MAX_SESSIONS,
287
+ DEFAULT_STALE_MS,
288
+ };
@@ -73,6 +73,22 @@ class AsyncHookSupervisor {
73
73
  ? this._statsStore.defaultHookStatsPath()
74
74
  : null);
75
75
  this._statsFs = opts.statsFs || null; // injected fs for tests
76
+ // Persistent rewake queue (opt-in). A rewake — an async check that OPTED IN
77
+ // and finished in a FAILURE state — is the one actionable signal this
78
+ // supervisor produces, but it lives only in `_rewakes` until the turn loop
79
+ // drains it. If the process dies in that window the failure is lost. When a
80
+ // `sessionId` + `persistQueue` (or an explicit `queuePath`) are supplied,
81
+ // each rewake is durably parked keyed by session so `--resume` can recover
82
+ // it; it's removed again the moment the run drains it. Disabled (no writes,
83
+ // byte-unchanged) when either is absent — mirrors the stats-store opt-in.
84
+ this._queueStore = opts.queueStore || require("./async-hook-queue.cjs");
85
+ this._queueSessionId = opts.sessionId ? String(opts.sessionId) : null;
86
+ this._queuePath =
87
+ opts.queuePath ||
88
+ (opts.persistQueue === true
89
+ ? this._queueStore.defaultAsyncHookQueuePath()
90
+ : null);
91
+ this._queueFs = opts.queueFs || null; // injected fs for tests
76
92
  // The exit reaper is installed LAZILY (on first spawn), not in the ctor:
77
93
  // a supervisor that never dispatches a hook (or the many built in tests)
78
94
  // must not leave a permanent `process.once('exit')` listener behind —
@@ -359,6 +375,29 @@ class AsyncHookSupervisor {
359
375
  if (hook.asyncRewake && !rec.ok) {
360
376
  this._rewakes.push(rec);
361
377
  if (this._rewakes.length > RESULT_RING) this._rewakes.shift();
378
+ // Durably park it (opt-in) so a crash before the turn loop drains this
379
+ // rewake doesn't lose the failure signal. Immediate write, not deferred to
380
+ // stopAll — a rewake must survive even a hard exit that never runs stopAll.
381
+ this._persistRewake(rec);
382
+ }
383
+ }
384
+
385
+ /** True when a session id + queue location were both configured. */
386
+ _queueEnabled() {
387
+ return !!(this._queueSessionId && this._queuePath);
388
+ }
389
+
390
+ /** Best-effort durable append of one rewake record. Never throws. */
391
+ _persistRewake(rec) {
392
+ if (!this._queueEnabled()) return;
393
+ try {
394
+ this._queueStore.appendRewake(
395
+ { sessionId: this._queueSessionId, records: [rec], now: rec.ts },
396
+ this._queuePath,
397
+ this._queueFs || undefined,
398
+ );
399
+ } catch {
400
+ /* queue is best-effort */
362
401
  }
363
402
  }
364
403
 
@@ -377,6 +416,19 @@ class AsyncHookSupervisor {
377
416
  drainRewakes() {
378
417
  const r = this._rewakes;
379
418
  this._rewakes = [];
419
+ // The run has now consumed these rewakes (it will surface / re-engage on
420
+ // them), so they no longer need to survive a crash — drop the durable copies.
421
+ if (r.length > 0 && this._queueEnabled()) {
422
+ try {
423
+ this._queueStore.removeRewakes(
424
+ { sessionId: this._queueSessionId, records: r },
425
+ this._queuePath,
426
+ this._queueFs || undefined,
427
+ );
428
+ } catch {
429
+ /* queue is best-effort */
430
+ }
431
+ }
380
432
  return r;
381
433
  }
382
434
 
@@ -0,0 +1,116 @@
1
+ /**
2
+ * background-phase-reporter — the missing PRODUCER for the background-agent
3
+ * `waiting_permission` phase (P0 state-machine slice,
4
+ * CLAUDE_CODE_CLI_INCREMENTAL_GAP_ANALYSIS_2026-07-12 §"后台 Agent 状态机").
5
+ *
6
+ * The consuming contract has existed for a while: `background-agent-phase.js`
7
+ * normalizes `waiting_permission` and `pendingApprovals > 0` into the
8
+ * dashboard's "Needs input" group, and `session-lifecycle.js` folds them into
9
+ * `waitingApproval`. But nothing ever WROTE those fields — the worker only
10
+ * emits `turn`/`idle`, so a background agent blocked on a human approval
11
+ * (`--permission-prompt-tool`, `--remote-control`) showed as "Working"
12
+ * indefinitely while a decision was actually pending.
13
+ *
14
+ * The production seam is the agent CHILD process, not the worker: the worker
15
+ * spawns each turn with `CC_BACKGROUND_AGENT_ID` in the environment, and the
16
+ * only places a plain headless run genuinely blocks on a human are the
17
+ * confirmer overrides installed on the ApprovalGate. This module wraps those
18
+ * confirmers: while one is awaiting a decision it patches the shared state
19
+ * file with `phase: "waiting_permission"` + a live `pendingApprovals` count,
20
+ * and restores `phase: "turn"` when every pending approval has settled.
21
+ *
22
+ * Safety properties:
23
+ * - Disabled (no `CC_BACKGROUND_AGENT_ID`) → `wrapConfirmer` returns the
24
+ * SAME function object; zero reads, zero writes, byte-identical behavior
25
+ * for every non-background run.
26
+ * - Never resurrects a terminal session: writes are skipped when the current
27
+ * on-disk status is anything other than absent/"running" (mirrors the
28
+ * worker's own writeHeartbeat guard).
29
+ * - Best-effort throughout: a state-file hiccup must never break an approval.
30
+ * - IO is injectable so tests never touch the real user data dir.
31
+ */
32
+
33
+ import {
34
+ readBackgroundAgentState,
35
+ writeBackgroundAgentState,
36
+ } from "./background-agent-supervisor.js";
37
+
38
+ /**
39
+ * Create a phase reporter for the current process.
40
+ *
41
+ * @param {object} [opts]
42
+ * @param {string|null} [opts.agentId] background session id; defaults to
43
+ * `process.env.CC_BACKGROUND_AGENT_ID`. Falsy → disabled no-op reporter.
44
+ * @param {(id:string)=>object|null} [opts.readState] injectable state reader
45
+ * @param {(state:object)=>void} [opts.writeState] injectable state writer
46
+ */
47
+ export function createBackgroundPhaseReporter(opts = {}) {
48
+ const agentId =
49
+ opts.agentId !== undefined
50
+ ? opts.agentId
51
+ : process.env.CC_BACKGROUND_AGENT_ID;
52
+ const readState = opts.readState || readBackgroundAgentState;
53
+ const writeState = opts.writeState || writeBackgroundAgentState;
54
+ const enabled = typeof agentId === "string" && agentId.trim() !== "";
55
+ let pending = 0;
56
+
57
+ function patch(fields) {
58
+ if (!enabled) return false;
59
+ try {
60
+ const current = readState(agentId) || { id: agentId };
61
+ // Terminal states are owned by the worker/stopper — never write a
62
+ // "waiting" phase over a completed/failed/stopped record.
63
+ if (current.status && current.status !== "running") return false;
64
+ writeState({ ...current, id: agentId, ...fields });
65
+ return true;
66
+ } catch {
67
+ return false; // state persistence must never break the approval itself
68
+ }
69
+ }
70
+
71
+ return {
72
+ enabled,
73
+ /** Current in-process pending-approval count (for tests/introspection). */
74
+ pendingCount() {
75
+ return pending;
76
+ },
77
+ /** A human approval just started blocking progress. */
78
+ beginApproval() {
79
+ if (!enabled) return;
80
+ pending++;
81
+ patch({
82
+ phase: "waiting_permission",
83
+ pendingApprovals: pending,
84
+ });
85
+ },
86
+ /** A previously reported approval settled (approved, denied, or failed). */
87
+ endApproval() {
88
+ if (!enabled) return;
89
+ pending = Math.max(0, pending - 1);
90
+ if (pending === 0) {
91
+ // Back to a live turn — the worker owns the turn/idle transition, we
92
+ // only hand the phase back to its steady in-turn value.
93
+ patch({ phase: "turn", pendingApprovals: 0 });
94
+ } else {
95
+ patch({ pendingApprovals: pending });
96
+ }
97
+ },
98
+ /**
99
+ * Wrap a (potentially human-blocking) confirmer so the pending window is
100
+ * visible in the background state. Disabled → returns `confirmer`
101
+ * unchanged (same object identity, zero overhead).
102
+ */
103
+ wrapConfirmer(confirmer) {
104
+ if (!enabled || typeof confirmer !== "function") return confirmer;
105
+ const self = this;
106
+ return async function phaseReportingConfirmer(...args) {
107
+ self.beginApproval();
108
+ try {
109
+ return await confirmer(...args);
110
+ } finally {
111
+ self.endApproval();
112
+ }
113
+ };
114
+ },
115
+ };
116
+ }
@@ -26,6 +26,11 @@ import {
26
26
  import { execSync, spawnSync } from "node:child_process";
27
27
  import { join, basename } from "node:path";
28
28
  import { getHomeDir, getConfigPath } from "./paths.js";
29
+ import {
30
+ languageIdForFile,
31
+ resolveServer,
32
+ describeServer,
33
+ } from "./lsp/lsp-server-registry.js";
29
34
  import {
30
35
  checkAgenda,
31
36
  checkInstructionFiles,
@@ -33,6 +38,8 @@ import {
33
38
  checkHooks,
34
39
  checkSandbox,
35
40
  checkDuplicateSkills,
41
+ checkLspReadiness,
42
+ checkOrphanProcesses,
36
43
  DEFAULT_CHECKUP_THRESHOLDS,
37
44
  } from "./runtime-checkup.js";
38
45
 
@@ -604,16 +611,84 @@ async function worktreeSection(opts, deps) {
604
611
 
605
612
  // ── runtime checkup: agenda expiry + verbose instruction files ──────────────
606
613
  // Only the gaps the OTHER sections don't already cover. Stale sessions,
607
- // worktrees, orphan processes and lost background agents are handled by
608
- // transcriptSection / worktreeSection / backgroundSection — surfacing them here
609
- // too would double-report, so this section deliberately covers just the agenda
610
- // schedule store and the instruction files (cc.md / AGENTS.md / CLAUDE.md).
614
+ // worktrees and lost background agents (worker STATUS) are handled by
615
+ // transcriptSection / worktreeSection / backgroundSection — surfacing those
616
+ // here too would double-report. This section covers the agenda schedule store,
617
+ // instruction files (cc.md / AGENTS.md / CLAUDE.md), and — distinct from
618
+ // backgroundSection's worker-status check — leaked agent CHILD subprocesses
619
+ // (a per-turn agentPid still alive after its worker died), probed via a static
620
+ // one-shot PID-liveness check and the shared checkOrphanProcesses evaluator.
611
621
  const SEVERITY_TO_LEVEL = {
612
622
  error: CHECK_LEVELS.ERR,
613
623
  warn: CHECK_LEVELS.WARN,
614
624
  info: CHECK_LEVELS.INFO,
615
625
  };
616
626
 
627
+ // Heavy / generated directories a project-language scan must never descend into
628
+ // (they'd dominate the budget and aren't the user's source).
629
+ const LSP_SCAN_SKIP_DIRS = new Set([
630
+ "node_modules",
631
+ "dist",
632
+ "build",
633
+ "out",
634
+ "target",
635
+ "coverage",
636
+ "vendor",
637
+ "venv",
638
+ "__pycache__",
639
+ ]);
640
+
641
+ /**
642
+ * Bounded shallow walk of a project root, counting source files per LSP
643
+ * languageId (via `languageIdForFile`). Deliberately capped by total entries
644
+ * visited AND depth so `cc doctor` stays fast on a huge monorepo; heavy build /
645
+ * vendor dirs and dot-dirs are skipped. fs access goes through injected `deps`
646
+ * (readdirSync/statSync) so it's unit-testable. Returns a Map languageId→count.
647
+ */
648
+ function scanProjectLanguages(
649
+ root,
650
+ deps,
651
+ { maxEntries = 20000, maxDepth = 8 } = {},
652
+ ) {
653
+ const counts = new Map();
654
+ let visited = 0;
655
+ const stack = [{ dir: root, depth: 0 }];
656
+ while (stack.length && visited < maxEntries) {
657
+ const { dir, depth } = stack.pop();
658
+ let names;
659
+ try {
660
+ names = deps.readdirSync(dir);
661
+ } catch {
662
+ continue; // unreadable dir — skip, never crash the doctor
663
+ }
664
+ for (const name of names) {
665
+ if (visited >= maxEntries) break;
666
+ visited += 1;
667
+ const lang = languageIdForFile(name);
668
+ if (lang) {
669
+ counts.set(lang, (counts.get(lang) || 0) + 1);
670
+ continue;
671
+ }
672
+ // Not a source file — maybe a directory to descend (bounded). Skip heavy
673
+ // build/vendor trees and dot-dirs (rarely hold the user's own source).
674
+ if (
675
+ depth < maxDepth &&
676
+ !LSP_SCAN_SKIP_DIRS.has(name) &&
677
+ !String(name).startsWith(".")
678
+ ) {
679
+ let isDir = false;
680
+ try {
681
+ isDir = deps.statSync(join(dir, name)).isDirectory();
682
+ } catch {
683
+ isDir = false;
684
+ }
685
+ if (isDir) stack.push({ dir: join(dir, name), depth: depth + 1 });
686
+ }
687
+ }
688
+ }
689
+ return counts;
690
+ }
691
+
617
692
  async function runtimeSection(opts, deps) {
618
693
  const checks = [];
619
694
  const now = deps.now();
@@ -789,6 +864,111 @@ async function runtimeSection(opts, deps) {
789
864
  checks.push(failedCheck("sandbox", "sandbox availability", err));
790
865
  }
791
866
 
867
+ // LSP readiness — languages the project actually contains but for which no
868
+ // language server is installed: the silent reason `code_intelligence` /
869
+ // `cc code-intel` degrades to plain text search. A STATIC install-gap check
870
+ // (no server is started); the project scan is bounded so it stays fast.
871
+ try {
872
+ const root = opts.cwd || process.cwd();
873
+ const langCounts = scanProjectLanguages(root, deps);
874
+ if (langCounts.size > 0) {
875
+ const languages = [];
876
+ for (const [languageId, fileCount] of langCounts) {
877
+ const resolved = resolveServer(languageId, root);
878
+ const desc = describeServer(languageId);
879
+ languages.push({
880
+ languageId,
881
+ fileCount,
882
+ available: !!resolved,
883
+ serverId: (resolved && resolved.id) || (desc && desc.id) || null,
884
+ exampleBin: desc && desc.bins ? desc.bins[0] : null,
885
+ });
886
+ }
887
+ for (const f of checkLspReadiness(languages)) {
888
+ checks.push(
889
+ check(
890
+ f.id,
891
+ `lsp ${f.ref}`,
892
+ SEVERITY_TO_LEVEL[f.severity] || CHECK_LEVELS.INFO,
893
+ `${f.message} — ${f.remediation}`,
894
+ ),
895
+ );
896
+ }
897
+ }
898
+ } catch (err) {
899
+ checks.push(failedCheck("lsp-readiness", "lsp server readiness", err));
900
+ }
901
+
902
+ // Orphaned agent subprocesses — a background worker died but its per-turn
903
+ // agent CHILD (the LLM/tool subprocess recorded as `agentPid`) is still
904
+ // alive: a leaked process nothing reaped. backgroundSection reports the
905
+ // worker's STATUS; this probes the actual child PID's liveness (static,
906
+ // one-shot, anchored on the process start time to defeat PID reuse) and
907
+ // feeds the shared checkOrphanProcesses evaluator. Only PIDs we recorded are
908
+ // probed → no false positives, no OS-wide process-table scan. Tests inject
909
+ // opts.backgroundProcesses / opts.activeSessionIds (mirrors
910
+ // opts.skillLayerEntries below).
911
+ try {
912
+ let processes = opts.backgroundProcesses;
913
+ let activeSessionIds = opts.activeSessionIds;
914
+ if (!processes) {
915
+ const bgMod = await import("./background-agent-supervisor.js");
916
+ const agents = bgMod.listBackgroundAgents({ all: true }) || [];
917
+ activeSessionIds = agents
918
+ .filter((a) => a.status === "running")
919
+ .map((a) => a.sessionId)
920
+ .filter(Boolean);
921
+ processes = [];
922
+ for (const a of agents) {
923
+ let state;
924
+ try {
925
+ state = bgMod.readBackgroundAgentState(a.id);
926
+ } catch {
927
+ continue; // unreadable/raced state file — skip
928
+ }
929
+ const agentPid = Number(state?.agentPid);
930
+ if (!Number.isInteger(agentPid) || agentPid <= 0) continue;
931
+ if (agentPid === Number(state?.pid)) continue; // that's the worker itself
932
+ // Alive AND still the same process we launched (not a reused PID).
933
+ const anchor = state.agentStartedAt || state.startedAt;
934
+ if (!bgMod.isSameProcess(agentPid, anchor)) continue; // dead → nothing to reap
935
+ const parentAlive = bgMod.isSameProcess(
936
+ Number(state.pid),
937
+ state.startedAt,
938
+ );
939
+ processes.push({
940
+ pid: agentPid,
941
+ alive: true,
942
+ parentAlive,
943
+ sessionId: state.sessionId || null,
944
+ kind: "agent-child",
945
+ });
946
+ }
947
+ }
948
+ for (const f of checkOrphanProcesses(processes, activeSessionIds || [])) {
949
+ const killCmd =
950
+ process.platform === "win32"
951
+ ? `taskkill /PID ${f.ref} /T /F`
952
+ : `kill ${f.ref}`;
953
+ checks.push(
954
+ check(
955
+ f.id,
956
+ `process pid=${f.ref}`,
957
+ SEVERITY_TO_LEVEL[f.severity] || CHECK_LEVELS.WARN,
958
+ `${f.message} — ${f.remediation}`,
959
+ {
960
+ id: `reap-orphan-${f.ref}`,
961
+ safe: false,
962
+ description: `terminate leaked agent subprocess pid=${f.ref}`,
963
+ command: killCmd,
964
+ },
965
+ ),
966
+ );
967
+ }
968
+ } catch (err) {
969
+ checks.push(failedCheck("orphan-process", "orphan agent processes", err));
970
+ }
971
+
792
972
  // Duplicate skills — an id defined in more than one layer silently shadows
793
973
  // (loadAll keeps only the winner), so a user's custom skill can override, or
794
974
  // be overridden by, another without any signal. opts.skillLayerEntries lets a