@raidiant/notifai 11.7.1 → 12.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (409) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/NOTICE +16 -0
  3. package/THIRD_PARTY_NOTICES +725 -0
  4. package/bin/notifai.mjs +11387 -0
  5. package/data/install.ps1 +318 -0
  6. package/data/release.json +1 -0
  7. package/inventory.json +1 -0
  8. package/npm-adapter-files.json +52 -0
  9. package/package.json +14 -44
  10. package/README.md +0 -168
  11. package/dist/agent-update-notice.d.ts +0 -7
  12. package/dist/agent-update-notice.js +0 -77
  13. package/dist/agent-update-notice.js.map +0 -1
  14. package/dist/atomic-file.d.ts +0 -30
  15. package/dist/atomic-file.js +0 -148
  16. package/dist/atomic-file.js.map +0 -1
  17. package/dist/attendant-update.d.ts +0 -15
  18. package/dist/attendant-update.js +0 -96
  19. package/dist/attendant-update.js.map +0 -1
  20. package/dist/claude-wake.d.ts +0 -108
  21. package/dist/claude-wake.js +0 -369
  22. package/dist/claude-wake.js.map +0 -1
  23. package/dist/cli-bin.d.ts +0 -26
  24. package/dist/cli-bin.js +0 -220
  25. package/dist/cli-bin.js.map +0 -1
  26. package/dist/cli-contract.d.ts +0 -16
  27. package/dist/cli-contract.js +0 -27
  28. package/dist/cli-contract.js.map +0 -1
  29. package/dist/cli-release.d.ts +0 -36
  30. package/dist/cli-release.js +0 -85
  31. package/dist/cli-release.js.map +0 -1
  32. package/dist/client.d.ts +0 -109
  33. package/dist/client.js +0 -217
  34. package/dist/client.js.map +0 -1
  35. package/dist/codex-answer-control.d.ts +0 -14
  36. package/dist/codex-answer-control.js +0 -41
  37. package/dist/codex-answer-control.js.map +0 -1
  38. package/dist/codex-answer-presentation.d.ts +0 -18
  39. package/dist/codex-answer-presentation.js +0 -113
  40. package/dist/codex-answer-presentation.js.map +0 -1
  41. package/dist/codex-input-lifecycle.d.ts +0 -10
  42. package/dist/codex-input-lifecycle.js +0 -63
  43. package/dist/codex-input-lifecycle.js.map +0 -1
  44. package/dist/codex-native-control.d.ts +0 -13
  45. package/dist/codex-native-control.js +0 -120
  46. package/dist/codex-native-control.js.map +0 -1
  47. package/dist/codex-native-turn.d.ts +0 -37
  48. package/dist/codex-native-turn.js +0 -155
  49. package/dist/codex-native-turn.js.map +0 -1
  50. package/dist/codex-question-bindings.d.ts +0 -76
  51. package/dist/codex-question-bindings.js +0 -151
  52. package/dist/codex-question-bindings.js.map +0 -1
  53. package/dist/codex-queue-control.d.ts +0 -21
  54. package/dist/codex-queue-control.js +0 -59
  55. package/dist/codex-queue-control.js.map +0 -1
  56. package/dist/codex-tool-messages.d.ts +0 -7
  57. package/dist/codex-tool-messages.js +0 -126
  58. package/dist/codex-tool-messages.js.map +0 -1
  59. package/dist/codex-wake.d.ts +0 -58
  60. package/dist/codex-wake.js +0 -135
  61. package/dist/codex-wake.js.map +0 -1
  62. package/dist/command-session.d.ts +0 -14
  63. package/dist/command-session.js +0 -31
  64. package/dist/command-session.js.map +0 -1
  65. package/dist/commands-acknowledge.d.ts +0 -13
  66. package/dist/commands-acknowledge.js +0 -116
  67. package/dist/commands-acknowledge.js.map +0 -1
  68. package/dist/commands-agent-sessions.d.ts +0 -6
  69. package/dist/commands-agent-sessions.js +0 -51
  70. package/dist/commands-agent-sessions.js.map +0 -1
  71. package/dist/commands-ask.d.ts +0 -62
  72. package/dist/commands-ask.js +0 -503
  73. package/dist/commands-ask.js.map +0 -1
  74. package/dist/commands-auth.d.ts +0 -76
  75. package/dist/commands-auth.js +0 -421
  76. package/dist/commands-auth.js.map +0 -1
  77. package/dist/commands-close.d.ts +0 -6
  78. package/dist/commands-close.js +0 -273
  79. package/dist/commands-close.js.map +0 -1
  80. package/dist/commands-config.d.ts +0 -31
  81. package/dist/commands-config.js +0 -272
  82. package/dist/commands-config.js.map +0 -1
  83. package/dist/commands-core.d.ts +0 -145
  84. package/dist/commands-core.js +0 -190
  85. package/dist/commands-core.js.map +0 -1
  86. package/dist/commands-devices.d.ts +0 -41
  87. package/dist/commands-devices.js +0 -144
  88. package/dist/commands-devices.js.map +0 -1
  89. package/dist/commands-doctor.d.ts +0 -25
  90. package/dist/commands-doctor.js +0 -1553
  91. package/dist/commands-doctor.js.map +0 -1
  92. package/dist/commands-guidance.d.ts +0 -27
  93. package/dist/commands-guidance.js +0 -150
  94. package/dist/commands-guidance.js.map +0 -1
  95. package/dist/commands-harness-context.d.ts +0 -57
  96. package/dist/commands-harness-context.js +0 -102
  97. package/dist/commands-harness-context.js.map +0 -1
  98. package/dist/commands-hook-attend.d.ts +0 -70
  99. package/dist/commands-hook-attend.js +0 -655
  100. package/dist/commands-hook-attend.js.map +0 -1
  101. package/dist/commands-hook-diagnostics.d.ts +0 -37
  102. package/dist/commands-hook-diagnostics.js +0 -136
  103. package/dist/commands-hook-diagnostics.js.map +0 -1
  104. package/dist/commands-hook-install.d.ts +0 -26
  105. package/dist/commands-hook-install.js +0 -784
  106. package/dist/commands-hook-install.js.map +0 -1
  107. package/dist/commands-hook-run.d.ts +0 -29
  108. package/dist/commands-hook-run.js +0 -933
  109. package/dist/commands-hook-run.js.map +0 -1
  110. package/dist/commands-hook-shape.d.ts +0 -19
  111. package/dist/commands-hook-shape.js +0 -48
  112. package/dist/commands-hook-shape.js.map +0 -1
  113. package/dist/commands-init.d.ts +0 -42
  114. package/dist/commands-init.js +0 -901
  115. package/dist/commands-init.js.map +0 -1
  116. package/dist/commands-io.d.ts +0 -2
  117. package/dist/commands-io.js +0 -117
  118. package/dist/commands-io.js.map +0 -1
  119. package/dist/commands-logs.d.ts +0 -30
  120. package/dist/commands-logs.js +0 -201
  121. package/dist/commands-logs.js.map +0 -1
  122. package/dist/commands-native-acknowledge.d.ts +0 -8
  123. package/dist/commands-native-acknowledge.js +0 -114
  124. package/dist/commands-native-acknowledge.js.map +0 -1
  125. package/dist/commands-project.d.ts +0 -4
  126. package/dist/commands-project.js +0 -41
  127. package/dist/commands-project.js.map +0 -1
  128. package/dist/commands-receive.d.ts +0 -3
  129. package/dist/commands-receive.js +0 -59
  130. package/dist/commands-receive.js.map +0 -1
  131. package/dist/commands-send-support.d.ts +0 -59
  132. package/dist/commands-send-support.js +0 -258
  133. package/dist/commands-send-support.js.map +0 -1
  134. package/dist/commands-send.d.ts +0 -34
  135. package/dist/commands-send.js +0 -900
  136. package/dist/commands-send.js.map +0 -1
  137. package/dist/commands-setup-proof.d.ts +0 -43
  138. package/dist/commands-setup-proof.js +0 -134
  139. package/dist/commands-setup-proof.js.map +0 -1
  140. package/dist/commands-skill.d.ts +0 -21
  141. package/dist/commands-skill.js +0 -199
  142. package/dist/commands-skill.js.map +0 -1
  143. package/dist/commands-sounds.d.ts +0 -12
  144. package/dist/commands-sounds.js +0 -35
  145. package/dist/commands-sounds.js.map +0 -1
  146. package/dist/commands-update-check.d.ts +0 -6
  147. package/dist/commands-update-check.js +0 -102
  148. package/dist/commands-update-check.js.map +0 -1
  149. package/dist/commands-update-resume.d.ts +0 -7
  150. package/dist/commands-update-resume.js +0 -143
  151. package/dist/commands-update-resume.js.map +0 -1
  152. package/dist/commands-update-skill.d.ts +0 -5
  153. package/dist/commands-update-skill.js +0 -36
  154. package/dist/commands-update-skill.js.map +0 -1
  155. package/dist/commands-update.d.ts +0 -11
  156. package/dist/commands-update.js +0 -286
  157. package/dist/commands-update.js.map +0 -1
  158. package/dist/commands.d.ts +0 -26
  159. package/dist/commands.js +0 -27
  160. package/dist/commands.js.map +0 -1
  161. package/dist/config-schema.d.ts +0 -62
  162. package/dist/config-schema.js +0 -302
  163. package/dist/config-schema.js.map +0 -1
  164. package/dist/config.d.ts +0 -181
  165. package/dist/config.js +0 -357
  166. package/dist/config.js.map +0 -1
  167. package/dist/credentials.d.ts +0 -96
  168. package/dist/credentials.js +0 -342
  169. package/dist/credentials.js.map +0 -1
  170. package/dist/file-lock.d.ts +0 -34
  171. package/dist/file-lock.js +0 -367
  172. package/dist/file-lock.js.map +0 -1
  173. package/dist/generated-session-label.d.ts +0 -2
  174. package/dist/generated-session-label.js +0 -152
  175. package/dist/generated-session-label.js.map +0 -1
  176. package/dist/guidance-content.d.ts +0 -53
  177. package/dist/guidance-content.js +0 -248
  178. package/dist/guidance-content.js.map +0 -1
  179. package/dist/guidance-render.d.ts +0 -25
  180. package/dist/guidance-render.js +0 -47
  181. package/dist/guidance-render.js.map +0 -1
  182. package/dist/guidance.d.ts +0 -69
  183. package/dist/guidance.js +0 -173
  184. package/dist/guidance.js.map +0 -1
  185. package/dist/harness-session-title.d.ts +0 -22
  186. package/dist/harness-session-title.js +0 -74
  187. package/dist/harness-session-title.js.map +0 -1
  188. package/dist/harnesses.d.ts +0 -65
  189. package/dist/harnesses.js +0 -159
  190. package/dist/harnesses.js.map +0 -1
  191. package/dist/hermes-attendant.d.ts +0 -38
  192. package/dist/hermes-attendant.js +0 -337
  193. package/dist/hermes-attendant.js.map +0 -1
  194. package/dist/hermes-plugin.d.ts +0 -11
  195. package/dist/hermes-plugin.js +0 -299
  196. package/dist/hermes-plugin.js.map +0 -1
  197. package/dist/hook-acknowledgements.d.ts +0 -70
  198. package/dist/hook-acknowledgements.js +0 -490
  199. package/dist/hook-acknowledgements.js.map +0 -1
  200. package/dist/hook-adapter.d.ts +0 -48
  201. package/dist/hook-adapter.js +0 -419
  202. package/dist/hook-adapter.js.map +0 -1
  203. package/dist/hook-events.d.ts +0 -105
  204. package/dist/hook-events.js +0 -130
  205. package/dist/hook-events.js.map +0 -1
  206. package/dist/hook-gates.d.ts +0 -7
  207. package/dist/hook-gates.js +0 -20
  208. package/dist/hook-gates.js.map +0 -1
  209. package/dist/hook-input.d.ts +0 -10
  210. package/dist/hook-input.js +0 -64
  211. package/dist/hook-input.js.map +0 -1
  212. package/dist/hook-lifecycle.d.ts +0 -112
  213. package/dist/hook-lifecycle.js +0 -2031
  214. package/dist/hook-lifecycle.js.map +0 -1
  215. package/dist/hook-project-sessions.d.ts +0 -28
  216. package/dist/hook-project-sessions.js +0 -141
  217. package/dist/hook-project-sessions.js.map +0 -1
  218. package/dist/hook-question-lock.d.ts +0 -29
  219. package/dist/hook-question-lock.js +0 -217
  220. package/dist/hook-question-lock.js.map +0 -1
  221. package/dist/hook-question-retirement.d.ts +0 -74
  222. package/dist/hook-question-retirement.js +0 -314
  223. package/dist/hook-question-retirement.js.map +0 -1
  224. package/dist/hook-question-state.d.ts +0 -58
  225. package/dist/hook-question-state.js +0 -239
  226. package/dist/hook-question-state.js.map +0 -1
  227. package/dist/hook-session-state.d.ts +0 -127
  228. package/dist/hook-session-state.js +0 -488
  229. package/dist/hook-session-state.js.map +0 -1
  230. package/dist/hook-types.d.ts +0 -476
  231. package/dist/hook-types.js +0 -4
  232. package/dist/hook-types.js.map +0 -1
  233. package/dist/injection-render.d.ts +0 -36
  234. package/dist/injection-render.js +0 -61
  235. package/dist/injection-render.js.map +0 -1
  236. package/dist/install-hooks.d.ts +0 -470
  237. package/dist/install-hooks.js +0 -1622
  238. package/dist/install-hooks.js.map +0 -1
  239. package/dist/integration-health.d.ts +0 -26
  240. package/dist/integration-health.js +0 -133
  241. package/dist/integration-health.js.map +0 -1
  242. package/dist/interactive.d.ts +0 -22
  243. package/dist/interactive.js +0 -644
  244. package/dist/interactive.js.map +0 -1
  245. package/dist/invocation-context.d.ts +0 -47
  246. package/dist/invocation-context.js +0 -164
  247. package/dist/invocation-context.js.map +0 -1
  248. package/dist/local-path.d.ts +0 -7
  249. package/dist/local-path.js +0 -27
  250. package/dist/local-path.js.map +0 -1
  251. package/dist/logging.d.ts +0 -157
  252. package/dist/logging.js +0 -599
  253. package/dist/logging.js.map +0 -1
  254. package/dist/main-run.d.ts +0 -1
  255. package/dist/main-run.js +0 -42
  256. package/dist/main-run.js.map +0 -1
  257. package/dist/main.d.ts +0 -2
  258. package/dist/main.js +0 -12
  259. package/dist/main.js.map +0 -1
  260. package/dist/native-answer-operation.d.ts +0 -70
  261. package/dist/native-answer-operation.js +0 -247
  262. package/dist/native-answer-operation.js.map +0 -1
  263. package/dist/native-skills.d.ts +0 -64
  264. package/dist/native-skills.js +0 -167
  265. package/dist/native-skills.js.map +0 -1
  266. package/dist/node-floor.d.ts +0 -19
  267. package/dist/node-floor.js +0 -32
  268. package/dist/node-floor.js.map +0 -1
  269. package/dist/npm-invocation.d.ts +0 -34
  270. package/dist/npm-invocation.js +0 -53
  271. package/dist/npm-invocation.js.map +0 -1
  272. package/dist/openclaw-continuation-bridge.d.ts +0 -7
  273. package/dist/openclaw-continuation-bridge.js +0 -64
  274. package/dist/openclaw-continuation-bridge.js.map +0 -1
  275. package/dist/openclaw-gateway-readiness.d.ts +0 -1
  276. package/dist/openclaw-gateway-readiness.js +0 -24
  277. package/dist/openclaw-gateway-readiness.js.map +0 -1
  278. package/dist/openclaw-generation.d.ts +0 -21
  279. package/dist/openclaw-generation.js +0 -115
  280. package/dist/openclaw-generation.js.map +0 -1
  281. package/dist/openclaw-message-bridge.d.ts +0 -8
  282. package/dist/openclaw-message-bridge.js +0 -93
  283. package/dist/openclaw-message-bridge.js.map +0 -1
  284. package/dist/openclaw-pending.d.ts +0 -8
  285. package/dist/openclaw-pending.js +0 -47
  286. package/dist/openclaw-pending.js.map +0 -1
  287. package/dist/openclaw-plugin.d.ts +0 -75
  288. package/dist/openclaw-plugin.js +0 -1353
  289. package/dist/openclaw-plugin.js.map +0 -1
  290. package/dist/openclaw-session-access.d.ts +0 -12
  291. package/dist/openclaw-session-access.js +0 -79
  292. package/dist/openclaw-session-access.js.map +0 -1
  293. package/dist/opencode-plugin.d.ts +0 -88
  294. package/dist/opencode-plugin.js +0 -286
  295. package/dist/opencode-plugin.js.map +0 -1
  296. package/dist/orca-session-title.d.ts +0 -11
  297. package/dist/orca-session-title.js +0 -111
  298. package/dist/orca-session-title.js.map +0 -1
  299. package/dist/pack-feedback-slice.d.ts +0 -35
  300. package/dist/pack-feedback-slice.js +0 -126
  301. package/dist/pack-feedback-slice.js.map +0 -1
  302. package/dist/pairing-qr.d.ts +0 -5
  303. package/dist/pairing-qr.js +0 -24
  304. package/dist/pairing-qr.js.map +0 -1
  305. package/dist/pending-pairing.d.ts +0 -38
  306. package/dist/pending-pairing.js +0 -70
  307. package/dist/pending-pairing.js.map +0 -1
  308. package/dist/platform.d.ts +0 -67
  309. package/dist/platform.js +0 -140
  310. package/dist/platform.js.map +0 -1
  311. package/dist/process-identity.d.ts +0 -26
  312. package/dist/process-identity.js +0 -82
  313. package/dist/process-identity.js.map +0 -1
  314. package/dist/program.d.ts +0 -59
  315. package/dist/program.js +0 -691
  316. package/dist/program.js.map +0 -1
  317. package/dist/project-enablement.d.ts +0 -21
  318. package/dist/project-enablement.js +0 -71
  319. package/dist/project-enablement.js.map +0 -1
  320. package/dist/question-settlement-process.d.ts +0 -17
  321. package/dist/question-settlement-process.js +0 -35
  322. package/dist/question-settlement-process.js.map +0 -1
  323. package/dist/question-timing.d.ts +0 -46
  324. package/dist/question-timing.js +0 -48
  325. package/dist/question-timing.js.map +0 -1
  326. package/dist/readiness.d.ts +0 -153
  327. package/dist/readiness.js +0 -149
  328. package/dist/readiness.js.map +0 -1
  329. package/dist/release.d.ts +0 -31
  330. package/dist/release.js +0 -52
  331. package/dist/release.js.map +0 -1
  332. package/dist/send-attempts.d.ts +0 -26
  333. package/dist/send-attempts.js +0 -139
  334. package/dist/send-attempts.js.map +0 -1
  335. package/dist/send.d.ts +0 -115
  336. package/dist/send.js +0 -421
  337. package/dist/send.js.map +0 -1
  338. package/dist/session-activation.d.ts +0 -13
  339. package/dist/session-activation.js +0 -64
  340. package/dist/session-activation.js.map +0 -1
  341. package/dist/session-attendant-probe.d.ts +0 -85
  342. package/dist/session-attendant-probe.js +0 -173
  343. package/dist/session-attendant-probe.js.map +0 -1
  344. package/dist/session-attendant-state.d.ts +0 -70
  345. package/dist/session-attendant-state.js +0 -256
  346. package/dist/session-attendant-state.js.map +0 -1
  347. package/dist/session-attendant.d.ts +0 -143
  348. package/dist/session-attendant.js +0 -520
  349. package/dist/session-attendant.js.map +0 -1
  350. package/dist/session-delivery.d.ts +0 -249
  351. package/dist/session-delivery.js +0 -576
  352. package/dist/session-delivery.js.map +0 -1
  353. package/dist/session-handoff.d.ts +0 -98
  354. package/dist/session-handoff.js +0 -118
  355. package/dist/session-handoff.js.map +0 -1
  356. package/dist/session-input-wakes.d.ts +0 -78
  357. package/dist/session-input-wakes.js +0 -239
  358. package/dist/session-input-wakes.js.map +0 -1
  359. package/dist/session-inputs.d.ts +0 -57
  360. package/dist/session-inputs.js +0 -434
  361. package/dist/session-inputs.js.map +0 -1
  362. package/dist/session-labels.d.ts +0 -56
  363. package/dist/session-labels.js +0 -396
  364. package/dist/session-labels.js.map +0 -1
  365. package/dist/session-message-handoff.d.ts +0 -39
  366. package/dist/session-message-handoff.js +0 -107
  367. package/dist/session-message-handoff.js.map +0 -1
  368. package/dist/setup-destinations.d.ts +0 -23
  369. package/dist/setup-destinations.js +0 -44
  370. package/dist/setup-destinations.js.map +0 -1
  371. package/dist/skill-integrity.d.ts +0 -54
  372. package/dist/skill-integrity.js +0 -164
  373. package/dist/skill-integrity.js.map +0 -1
  374. package/dist/skill-source/manifest.json +0 -40
  375. package/dist/skill-source/notifai/SKILL.md +0 -350
  376. package/dist/skill-source/notifai/references/diagnostics.md +0 -36
  377. package/dist/skill-source/notifai/references/harness-setup.md +0 -384
  378. package/dist/skill-source/notifai/references/native-questions.md +0 -51
  379. package/dist/skill-source/notifai/references/notes-and-edits.md +0 -54
  380. package/dist/skill-source/notifai/references/send-details.md +0 -14
  381. package/dist/skill-source/notifai/references/updates.md +0 -116
  382. package/dist/skill-source/notifai/references/writing-examples.md +0 -56
  383. package/dist/sound-ref.d.ts +0 -23
  384. package/dist/sound-ref.js +0 -28
  385. package/dist/sound-ref.js.map +0 -1
  386. package/dist/ui/banner.d.ts +0 -22
  387. package/dist/ui/banner.js +0 -87
  388. package/dist/ui/banner.js.map +0 -1
  389. package/dist/ui/config-view.d.ts +0 -30
  390. package/dist/ui/config-view.js +0 -108
  391. package/dist/ui/config-view.js.map +0 -1
  392. package/dist/ui/help.d.ts +0 -44
  393. package/dist/ui/help.js +0 -107
  394. package/dist/ui/help.js.map +0 -1
  395. package/dist/ui/theme.d.ts +0 -90
  396. package/dist/ui/theme.js +0 -244
  397. package/dist/ui/theme.js.map +0 -1
  398. package/dist/update-handoff.d.ts +0 -29
  399. package/dist/update-handoff.js +0 -69
  400. package/dist/update-handoff.js.map +0 -1
  401. package/dist/url-policy.d.ts +0 -85
  402. package/dist/url-policy.js +0 -369
  403. package/dist/url-policy.js.map +0 -1
  404. package/dist/version.d.ts +0 -40
  405. package/dist/version.js +0 -132
  406. package/dist/version.js.map +0 -1
  407. package/dist/wake-support.d.ts +0 -40
  408. package/dist/wake-support.js +0 -95
  409. package/dist/wake-support.js.map +0 -1
@@ -1,1553 +0,0 @@
1
- import { NOTIFICATION_CONTRACT_FINGERPRINT, PLATFORMS, SHIPPED_CLI_CAPABILITIES, } from '@raidiant/notifai-protocol';
2
- import { existsSync } from 'node:fs';
3
- import { hermesPluginListed, hermesVersionSupported } from './hermes-plugin.js';
4
- import { inspectClaudeInbox, systemClaudeWakeAdapters } from './claude-wake.js';
5
- import { ApiCallError, NetworkError } from './client.js';
6
- import { codexHome as codexQueueHomeDirectory, inspectCodexQueue } from './codex-wake.js';
7
- import {} from './config.js';
8
- import { HARNESS_LABELS, HERMES_QUESTION_ROUTING_UNAVAILABLE, isHookInstallableHarness, questionRoutingCapability, } from './harnesses.js';
9
- import { hookAdapterTargetsArtifact, inspectHookAdapter, isNpxAdapterTarget, } from './hook-adapter.js';
10
- import { readLiveProjectSessionPointers, readProjectSessionPointer, } from './hook-project-sessions.js';
11
- import { readSessionState } from './hook-session-state.js';
12
- import { openclawGatewayReady } from './openclaw-gateway-readiness.js';
13
- import { NON_ROUTING_BLOCKING_STOP_TIMEOUT_SECONDS, QUESTION_STOP_TIMEOUT_SECONDS, codexCoexistenceNotes, codexHomeNote, codexRepresentationProblems, codexTrustProblems, detectedHarnesses, findInstallations, findLegacyProjectInstallations, findObsoleteNotifaiPluginWiring, handlerEvent, } from './install-hooks.js';
14
- import { inferInvocationContext } from './invocation-context.js';
15
- import { firstBlocker, readinessJson, } from './readiness.js';
16
- import { packageVersion } from './release.js';
17
- import { EXIT, SETUP_COMMAND, diagnoseIgnoredOriginOverride, loadLoggedConfig, log, makeClient, resolvedBaseUrl, updateCliCommand, } from './commands-core.js';
18
- import { deviceInstallRemedy, readyCompanionDevices } from './commands-devices.js';
19
- import { setupAccessUrl, setupCompanionUrl } from './setup-destinations.js';
20
- import { readPendingPairing } from './pending-pairing.js';
21
- import { pendingApprovalBlocker } from './commands-auth.js';
22
- import { claudeSessionPid, resolveActiveHarness, } from './commands-harness-context.js';
23
- import { stopShapeProblems } from './commands-hook-shape.js';
24
- import { CODEX_FRESH_SESSION_USER_ACTION, CODEX_HOOK_APPROVAL_USER_ACTION, CODEX_STOP_DEFINITION_NOT_SINGULAR_PROBLEM, CODEX_STALE_STOP_DEFINITION_PROBLEM, activeQuestionRouteProblems, hookActivationAdvice, inspectOpenclawPluginLoadability, } from './commands-hook-diagnostics.js';
25
- import { HOOK_EVENTS, requiredHookEvents } from './hook-events.js';
26
- import { cliBinReadiness, inspectCliInstallations } from './cli-bin.js';
27
- import { newerPublishedCli, publishedCliDistTags, shouldConsultCliRegistry, thisCliVersion, } from './cli-release.js';
28
- import { SETUP_PROOF_STALE_MS, observedCompanionReceipt, observedSetupProof, readSetupProof, setupProofIsStale, setupProofApplies, writeSetupProof, } from './commands-setup-proof.js';
29
- import { skillReadiness } from './commands-skill.js';
30
- import { projectBinding, projectEnabled } from './project-enablement.js';
31
- import { attendantSupport } from './session-attendant-probe.js';
32
- import { codexNativeActivityObserved, hermesQuestionRouteReady, listAttendantReports } from './session-attendant-state.js';
33
- import { CLI_UPDATE_AVAILABLE, SERVICE_UPDATE_IN_PROGRESS } from './cli-contract.js';
34
- // ---------------------------------------------------------------------------
35
- // doctor
36
- // ---------------------------------------------------------------------------
37
- /**
38
- * Server-owned support policy plus every shipped platform document. Artifact
39
- * versions and document integers are structured inventory, never routing or a
40
- * definition of "up to date".
41
- */
42
- async function compatibilityCheck(client, deps) {
43
- try {
44
- const [compatibility, documentResults] = await Promise.all([
45
- client.compatibility(),
46
- Promise.allSettled(PLATFORMS.map((platform) => client.capabilities(platform))),
47
- ]);
48
- const documents = documentResults.flatMap((result) => result.status === 'fulfilled' ? [result.value] : []);
49
- const documentErrors = documentResults.flatMap((result, index) => result.status === 'rejected'
50
- ? [{ platform: PLATFORMS[index], error: String(result.reason) }]
51
- : []);
52
- const serverCapabilities = new Set(compatibility.server_capabilities);
53
- const cliCapabilityIntersection = {
54
- available: SHIPPED_CLI_CAPABILITIES.filter((capability) => serverCapabilities.has(capability)),
55
- missing_on_server: SHIPPED_CLI_CAPABILITIES.filter((capability) => !serverCapabilities.has(capability)),
56
- };
57
- const notificationContract = {
58
- local: NOTIFICATION_CONTRACT_FINGERPRINT,
59
- mismatched_platforms: documents
60
- .filter((document) => document.notification_contract_fingerprint !== NOTIFICATION_CONTRACT_FINGERPRINT)
61
- .map((document) => document.platform),
62
- };
63
- const technical = {
64
- local: {
65
- cli_version: packageVersion(),
66
- capabilities: [...SHIPPED_CLI_CAPABILITIES],
67
- },
68
- server: compatibility,
69
- capability_documents: documents.map((document) => ({
70
- platform: document.platform,
71
- schema_version: document.schema_version,
72
- notification_contract_fingerprint: document.notification_contract_fingerprint,
73
- })),
74
- capability_document_errors: documentErrors,
75
- cli_capability_intersection: cliCapabilityIntersection,
76
- notification_contract: notificationContract,
77
- };
78
- if (compatibility.cli.state === 'must_update') {
79
- return {
80
- id: 'contract',
81
- title: 'Notifai update',
82
- status: 'gap',
83
- detail: "Notifai can't send notifications until you update.",
84
- technical,
85
- remedy: {
86
- by: 'user-here',
87
- summary: 'update Notifai',
88
- command: updateCliCommand(deps),
89
- },
90
- };
91
- }
92
- if (compatibility.cli.state === 'update_available') {
93
- const scheduled = compatibility.cli.reason === 'sunset_scheduled';
94
- return {
95
- id: 'contract',
96
- title: 'Notifai update',
97
- status: 'optional-gap',
98
- detail: scheduled
99
- ? 'Update Notifai soon to keep sending notifications.'
100
- : CLI_UPDATE_AVAILABLE,
101
- technical,
102
- remedy: {
103
- by: 'user-here',
104
- summary: 'update Notifai',
105
- command: updateCliCommand(deps),
106
- },
107
- };
108
- }
109
- if (notificationContract.mismatched_platforms.length > 0) {
110
- return {
111
- id: 'contract',
112
- title: 'Notifai update',
113
- status: 'optional-gap',
114
- detail: SERVICE_UPDATE_IN_PROGRESS,
115
- technical,
116
- remedy: {
117
- by: 'user-here',
118
- summary: 'try again after the service update',
119
- command: 'notifai doctor',
120
- },
121
- };
122
- }
123
- if (documentErrors.length > 0) {
124
- return {
125
- id: 'contract',
126
- title: 'Notifai check',
127
- status: 'optional-gap',
128
- detail: 'Could not finish checking Notifai. Try again.',
129
- technical,
130
- remedy: {
131
- by: 'user-here',
132
- summary: 'retry the Notifai check',
133
- command: 'notifai doctor',
134
- },
135
- };
136
- }
137
- if (cliCapabilityIntersection.missing_on_server.length > 0) {
138
- // Named capabilities are feature-scoped. A service that has not learned
139
- // this CLI's optional feature yet does not make baseline Notification
140
- // Requests unavailable, even when every document carries the same schema
141
- // integer. Structured output names the gap; the human path stays on the
142
- // local service-update sentence and never invents a version diagnosis.
143
- return {
144
- id: 'contract',
145
- title: 'Notifai update',
146
- status: 'optional-gap',
147
- detail: SERVICE_UPDATE_IN_PROGRESS,
148
- technical,
149
- remedy: {
150
- by: 'user-here',
151
- summary: 'try again after the service update',
152
- command: 'notifai doctor',
153
- },
154
- };
155
- }
156
- return {
157
- id: 'contract',
158
- title: 'Notifai update',
159
- status: 'ready',
160
- detail: 'Notifai can send notifications.',
161
- technical,
162
- };
163
- }
164
- catch (err) {
165
- return {
166
- id: 'contract',
167
- title: 'Notifai update',
168
- status: 'optional-gap',
169
- detail: SERVICE_UPDATE_IN_PROGRESS,
170
- technical: {
171
- error: err instanceof ApiCallError
172
- ? { code: err.code, status: err.status }
173
- : String(err),
174
- },
175
- remedy: {
176
- by: 'user-here',
177
- summary: 'try again after the service update',
178
- command: 'notifai doctor',
179
- },
180
- };
181
- }
182
- }
183
- function projectReadiness(deps, config) {
184
- const configured = config.project.value;
185
- if (configured !== null) {
186
- return {
187
- id: 'project',
188
- title: 'Project identity',
189
- status: 'ready',
190
- detail: `"${configured}" (${config.project.source})`,
191
- };
192
- }
193
- const inferred = inferInvocationContext(deps.cwd).project;
194
- if (inferred !== null) {
195
- return {
196
- id: 'project',
197
- title: 'Project identity',
198
- status: 'ready',
199
- detail: `"${inferred}" (inferred from Git or the current directory)`,
200
- };
201
- }
202
- return {
203
- id: 'project',
204
- title: 'Project identity',
205
- status: 'optional-gap',
206
- detail: 'the directory name has no characters a Project identifier can use',
207
- remedy: {
208
- by: 'cli',
209
- summary: 'choose an explicit Project identifier',
210
- command: 'notifai init --project-id my-project',
211
- },
212
- };
213
- }
214
- function projectEnablementReadiness(deps, config) {
215
- const binding = projectBinding(deps.cwd, deps.env, config.project.value);
216
- if (binding === null) {
217
- return {
218
- id: 'project-enablement',
219
- title: 'Project enablement',
220
- status: 'optional-gap',
221
- detail: 'no Project is available here; lifecycle hooks stay silent',
222
- };
223
- }
224
- if (projectEnabled(binding)) {
225
- return {
226
- id: 'project-enablement',
227
- title: 'Project enablement',
228
- status: 'ready',
229
- detail: `enabled by the User for Project "${binding.project}"`,
230
- };
231
- }
232
- return {
233
- id: 'project-enablement',
234
- title: 'Project enablement',
235
- status: 'optional-gap',
236
- detail: `disabled for Project "${binding.project}"; installed lifecycle hooks will emit no model guidance`,
237
- remedy: {
238
- by: 'cli',
239
- summary: 'enable Notifai lifecycle behavior for this Project',
240
- command: 'notifai project enable',
241
- },
242
- };
243
- }
244
- function remoteStatesFrom(previous) {
245
- const pick = (id) => previous.states.find((state) => state.id === id);
246
- const credential = pick('credential');
247
- const server = pick('server');
248
- const contract = pick('contract');
249
- const auth = pick('auth');
250
- const devices = pick('devices');
251
- const proof = pick('proof');
252
- if (credential === undefined ||
253
- server === undefined ||
254
- contract === undefined ||
255
- auth === undefined ||
256
- devices === undefined ||
257
- proof === undefined) {
258
- return null;
259
- }
260
- return { credential, server, contract, auth, devices, proof };
261
- }
262
- function remoteInvalidatedByConfig(previous, config, credential) {
263
- const origin = resolvedBaseUrl(config, credential);
264
- const server = previous.states.find((state) => state.id === 'server');
265
- return server === undefined || !server.detail.includes(origin);
266
- }
267
- function isNoActivePlanError(err) {
268
- return err instanceof ApiCallError && err.code === 'no_active_plan';
269
- }
270
- /**
271
- * Device listing is grant-gated; access status is not. A paired Account whose
272
- * plan lapsed must not be diagnosed as a revoked machine.
273
- */
274
- async function probeAccount(client, credential, baseUrl) {
275
- const [devicesOutcome, accessOutcome, requestOutcome] = await Promise.allSettled([
276
- Promise.resolve().then(() => client.listDevices()),
277
- Promise.resolve().then(() => client.accessStatus()),
278
- Promise.resolve().then(() => client.accessRequest()),
279
- ]);
280
- const access = accessOutcome.status === 'fulfilled' ? accessOutcome.value : null;
281
- const pendingRequest = requestOutcome.status === 'fulfilled' ? requestOutcome.value.request : null;
282
- const devices = devicesOutcome.status === 'fulfilled' ? devicesOutcome.value.devices : null;
283
- const email = access?.email ?? null;
284
- const devicesErr = devicesOutcome.status === 'rejected' ? devicesOutcome.reason : null;
285
- const noActivePlan = access?.status === 'no_active_plan' || isNoActivePlanError(devicesErr);
286
- if (noActivePlan) {
287
- const next = (isNoActivePlanError(devicesErr) ? devicesErr.nextAction : null) ??
288
- `Open ${setupAccessUrl(baseUrl)} to set up access, then retry.`;
289
- const who = email ? ` (${email})` : '';
290
- // Three values, not two. Someone who asked yesterday is waiting on a person,
291
- // not on an action of theirs, and telling them to ask again — every run,
292
- // forever — is the only thing this terminal could get wrong about a wait it
293
- // cannot shorten.
294
- return {
295
- email,
296
- devices: null,
297
- lookupFailed: true,
298
- auth: pendingRequest === null
299
- ? {
300
- id: 'auth',
301
- title: 'Account',
302
- status: 'gap',
303
- detail: `this account does not have access to Notifai yet${who}`,
304
- technical: { access_url: setupAccessUrl(baseUrl), next_action: next },
305
- remedy: { by: 'user-elsewhere', summary: next },
306
- }
307
- : {
308
- id: 'auth',
309
- title: 'Account',
310
- status: 'gap',
311
- detail: `access requested on ${pendingRequest.requested_at.slice(0, 10)}${who} — waiting to be granted`,
312
- remedy: {
313
- by: 'user-elsewhere',
314
- summary: 'nothing is needed from you; this resumes on the next run once access is granted',
315
- },
316
- },
317
- };
318
- }
319
- if (devices !== null) {
320
- return {
321
- email,
322
- devices,
323
- lookupFailed: false,
324
- auth: {
325
- id: 'auth',
326
- title: 'Account',
327
- status: 'ready',
328
- detail: email
329
- ? `machine ${credential.machineId} accepted (${email})`
330
- : `machine ${credential.machineId} accepted`,
331
- },
332
- };
333
- }
334
- const accessErr = accessOutcome.status === 'rejected' ? accessOutcome.reason : null;
335
- const err = [devicesErr, accessErr].find((error) => error instanceof ApiCallError && error.status === 401)
336
- ?? devicesErr ?? accessErr;
337
- const rejectedCredential = err instanceof ApiCallError && err.status === 401;
338
- if (!rejectedCredential) {
339
- return {
340
- email,
341
- devices: null,
342
- lookupFailed: true,
343
- auth: access?.status === 'active'
344
- ? {
345
- id: 'auth',
346
- title: 'Account',
347
- status: 'ready',
348
- detail: email
349
- ? `machine ${credential.machineId} accepted (${email})`
350
- : `machine ${credential.machineId} accepted`,
351
- }
352
- : {
353
- id: 'auth',
354
- title: 'Account',
355
- status: 'gap',
356
- detail: 'could not check Account access; the saved machine approval is unchanged',
357
- remedy: { by: 'user-here', summary: 'try again when the service is reachable', command: SETUP_COMMAND },
358
- },
359
- };
360
- }
361
- return {
362
- email: null,
363
- devices: null,
364
- lookupFailed: true,
365
- auth: {
366
- id: 'auth',
367
- title: 'Account',
368
- status: 'gap',
369
- detail: err instanceof ApiCallError ? `${err.code}: ${err.message}` : String(err),
370
- remedy: {
371
- by: 'user-here',
372
- summary: 'this machine is no longer recognised; pair it again',
373
- command: SETUP_COMMAND,
374
- interactive: true,
375
- },
376
- },
377
- };
378
- }
379
- /**
380
- * Not paired — and, when an approval is already waiting in the browser, said
381
- * as that: the page and the code are the errand, and the same setup command
382
- * resumes the handshake rather than starting another.
383
- */
384
- function credentialGap(deps) {
385
- const pending = readPendingPairing(deps.env, (deps.now ?? Date.now)());
386
- if (pending !== null)
387
- return pendingApprovalBlocker(pending);
388
- return {
389
- id: 'credential',
390
- title: 'This machine',
391
- status: 'gap',
392
- detail: 'not paired with your account',
393
- remedy: {
394
- by: 'user-here',
395
- summary: 'sign in — this opens your browser to approve the machine',
396
- command: SETUP_COMMAND,
397
- interactive: true,
398
- },
399
- };
400
- }
401
- /**
402
- * Read the whole setup once, in dependency order.
403
- *
404
- * Descent stops where a prerequisite is missing: without a credential there is
405
- * nothing to ask the server with, and without a reachable server a contract
406
- * mismatch is unknowable rather than absent. Those downstream states report
407
- * `unknown`, which is the honest answer and keeps a network outage from
408
- * looking like a broken install.
409
- */
410
- export async function assessReadiness(deps, options = {}) {
411
- const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env });
412
- const credential = deps.store.load();
413
- if (credential)
414
- diagnoseIgnoredOriginOverride(deps.io, config, credential);
415
- const previous = options.previous;
416
- const refresh = options.refresh;
417
- if (previous !== undefined && refresh !== undefined && refresh.length === 0)
418
- return previous;
419
- const reuseRemote = previous !== undefined &&
420
- refresh !== undefined &&
421
- !refresh.includes('remote') &&
422
- !remoteInvalidatedByConfig(previous, config, credential);
423
- if (reuseRemote) {
424
- const reused = remoteStatesFrom(previous);
425
- if (reused !== null) {
426
- return {
427
- states: [
428
- cliBinReadiness(deps.env, deps.hookPlatform ?? process.platform),
429
- projectEnablementReadiness(deps, config),
430
- projectReadiness(deps, config),
431
- reused.credential,
432
- reused.server,
433
- reused.contract,
434
- reused.auth,
435
- ...hookStates(deps),
436
- await skillReadiness(deps, options.skillScope),
437
- reused.devices,
438
- reused.proof,
439
- ],
440
- };
441
- }
442
- }
443
- const states = [];
444
- let accountClient = null;
445
- let accountDevices = null;
446
- states.push(cliBinReadiness(deps.env, deps.hookPlatform ?? process.platform));
447
- states.push(projectEnablementReadiness(deps, config));
448
- states.push(projectReadiness(deps, config));
449
- states.push(credential
450
- ? {
451
- id: 'credential',
452
- title: 'This machine',
453
- status: 'ready',
454
- detail: `paired as "${credential.machineName}" (${deps.store.describe()})`,
455
- }
456
- : credentialGap(deps));
457
- const baseUrl = resolvedBaseUrl(config, credential);
458
- const anon = makeClient(deps, baseUrl, null);
459
- // A probe that throws is unreachable, not a crash: this runs against a
460
- // half-configured machine by definition, which is where a client that
461
- // cannot even be constructed properly shows up.
462
- let reachable = false;
463
- try {
464
- reachable = await anon.health();
465
- }
466
- catch {
467
- reachable = false;
468
- }
469
- states.push(reachable
470
- ? { id: 'server', title: 'Service', status: 'ready', detail: `${baseUrl} reachable` }
471
- : {
472
- id: 'server',
473
- title: 'Service',
474
- status: 'gap',
475
- detail: `cannot reach ${baseUrl}`,
476
- remedy: {
477
- by: 'user-here',
478
- summary: 'check your network',
479
- command: 'notifai doctor',
480
- },
481
- });
482
- if (!reachable || !credential) {
483
- states.push({
484
- id: 'contract',
485
- title: 'Notifai update',
486
- status: 'unknown',
487
- detail: !reachable
488
- ? 'Not checked because the service is unreachable.'
489
- : 'Not checked because this machine is not paired.',
490
- });
491
- }
492
- else {
493
- const compatibilityClient = makeClient(deps, baseUrl, `Bearer nfm_${credential.machineId}.${credential.secret}`);
494
- states.push(await compatibilityCheck(compatibilityClient, deps));
495
- }
496
- let accountEmail = null;
497
- let accountLookupFailed = false;
498
- if (!credential || !reachable) {
499
- const why = !credential ? 'this machine is not paired' : 'the server is unreachable';
500
- states.push({ id: 'auth', title: 'Account', status: 'unknown', detail: `not checked — ${why}` });
501
- }
502
- else {
503
- const client = makeClient(deps, baseUrl, `Bearer nfm_${credential.machineId}.${credential.secret}`);
504
- accountClient = client;
505
- const probed = await probeAccount(client, credential, baseUrl);
506
- accountDevices = probed.devices;
507
- accountEmail = probed.email;
508
- accountLookupFailed = probed.lookupFailed;
509
- states.push(probed.auth);
510
- }
511
- // Optional setup that works without a companion device must appear before the
512
- // device gap: init stops at the first user-elsewhere blocker, and hooks/skill
513
- // are reachable without a phone.
514
- states.push(...hookStates(deps));
515
- states.push(await skillReadiness(deps, options.skillScope));
516
- if (!credential || !reachable) {
517
- const why = !credential ? 'this machine is not paired' : 'the server is unreachable';
518
- states.push({ id: 'devices', title: 'Your devices', status: 'unknown', detail: `not checked — ${why}` });
519
- }
520
- else if (accountLookupFailed || accountDevices === null) {
521
- states.push(states.find((state) => state.id === 'auth')?.status === 'ready'
522
- ? {
523
- id: 'devices', title: 'Your devices', status: 'gap',
524
- detail: 'could not check your Companion Apps',
525
- remedy: { by: 'user-here', summary: 'retry the device lookup', command: SETUP_COMMAND },
526
- }
527
- : { id: 'devices', title: 'Your devices', status: 'unknown', detail: 'not checked — Account access is unresolved' });
528
- }
529
- else {
530
- const devices = accountDevices;
531
- const companionDevices = devices.filter((device) => device.platform === 'ios' || device.platform === 'android');
532
- const ready = readyCompanionDevices(companionDevices);
533
- states.push(ready.length > 0
534
- ? {
535
- id: 'devices',
536
- title: 'Your devices',
537
- status: 'ready',
538
- detail: `${ready.map((d) => `${d.display_name} (${d.platform})`).join(', ')} ready to receive`,
539
- }
540
- : {
541
- id: 'devices',
542
- title: 'Your devices',
543
- status: 'gap',
544
- // The one gap that cannot be closed from this terminal, and the
545
- // likeliest place a first setup is abandoned. Naming which of
546
- // the three sub-states it is matters: "install the app" is
547
- // useless advice to someone who installed it and denied the
548
- // permission prompt. The live bridge is /support on the
549
- // dashboard origin — not a placeholder, and not typed by hand.
550
- detail: companionDevices.length === 0
551
- ? 'no active Companion App registered yet'
552
- : `${companionDevices.map((d) => `${d.display_name} (${d.platform}, ${d.permission_status})`).join(', ')} — registered but not able to receive`,
553
- // The pieces an agent's fixed wording needs, separately from the
554
- // sentence a human reads: where the install steps are, which
555
- // email the app must sign in with, and what is registered so far.
556
- technical: {
557
- companion_setup_url: setupCompanionUrl(baseUrl),
558
- account_email: accountEmail,
559
- devices: companionDevices.map((d) => ({
560
- display_name: d.display_name,
561
- platform: d.platform,
562
- permission_status: d.permission_status,
563
- registration_healthy: d.registration_healthy,
564
- })),
565
- },
566
- remedy: {
567
- by: 'user-elsewhere',
568
- summary: deviceInstallRemedy({
569
- baseUrl,
570
- email: accountEmail,
571
- devices: companionDevices,
572
- }),
573
- },
574
- });
575
- }
576
- states.push(await setupProofState(deps, config, accountClient, accountDevices));
577
- return { states };
578
- }
579
- async function applyRegistryRecommendation(deps, states) {
580
- if (!shouldConsultCliRegistry({ env: deps.env })) {
581
- return;
582
- }
583
- const tags = await publishedCliDistTags(deps.fetchImpl);
584
- const newer = newerPublishedCli(thisCliVersion(), tags);
585
- if (newer === null)
586
- return;
587
- const contract = states.find((state) => state.id === 'contract');
588
- if (contract === undefined || contract.status === 'gap' || contract.status === 'optional-gap')
589
- return;
590
- contract.status = 'optional-gap';
591
- contract.detail = CLI_UPDATE_AVAILABLE;
592
- contract.remedy = {
593
- by: 'user-here',
594
- summary: 'update Notifai',
595
- command: updateCliCommand(deps),
596
- };
597
- }
598
- async function setupProofState(deps, config, client, devices) {
599
- if (client === null || devices === null) {
600
- return {
601
- id: 'proof',
602
- title: 'Delivery proof',
603
- status: 'unknown',
604
- detail: 'not checked — account and device readiness must be established first',
605
- };
606
- }
607
- const companions = readyCompanionDevices(devices);
608
- if (companions.length === 0) {
609
- return {
610
- id: 'proof',
611
- title: 'Delivery proof',
612
- status: 'unknown',
613
- detail: 'not checked — no iPhone or Android Companion App is ready',
614
- };
615
- }
616
- const proof = readSetupProof(deps);
617
- const applies = setupProofApplies(proof, companions.map((device) => device.device_id));
618
- if (!applies) {
619
- return {
620
- id: 'proof',
621
- title: 'Delivery proof',
622
- status: 'gap',
623
- detail: "no Companion Receipt (the app's delivery confirmation) has proven this machine yet",
624
- remedy: {
625
- by: 'cli',
626
- summary: "send one notification that this machine is set up and wait for its Companion Receipt (the app's delivery confirmation)",
627
- command: 'notifai init',
628
- },
629
- };
630
- }
631
- const deviceName = companions.find((device) => device.device_id === proof.device_id)?.display_name ?? proof.device_id;
632
- const readyFromObservation = (observedAt, liveEvidence, localPersistence) => ({
633
- id: 'proof',
634
- title: 'Delivery proof',
635
- status: 'ready',
636
- detail: `Companion Receipt (the app's delivery confirmation) observed from ${deviceName} at ` +
637
- `${observedAt} (${proof.request_id})` +
638
- (liveEvidence?.status === 'unavailable'
639
- ? `; current evidence could not be re-read (${liveEvidence.code})`
640
- : liveEvidence?.status === 'unknown'
641
- ? '; the current service evidence no longer includes that retained observation'
642
- : '') +
643
- (localPersistence === undefined
644
- ? ''
645
- : `; the current observation could not be saved locally (${localPersistence.code})`),
646
- technical: liveEvidence === undefined && localPersistence === undefined
647
- ? undefined
648
- : {
649
- ...(liveEvidence === undefined ? {} : { live_evidence: liveEvidence }),
650
- ...(localPersistence === undefined ? {} : { local_persistence: localPersistence }),
651
- },
652
- });
653
- try {
654
- const snapshot = await client.evidence(proof.request_id);
655
- const observed = observedCompanionReceipt(snapshot, proof.device_id);
656
- if (observed) {
657
- let localPersistence;
658
- if (proof.companion_receipt.state !== 'observed') {
659
- if (!writeSetupProof(deps, observedSetupProof(proof, observed.observedAt))) {
660
- localPersistence = { status: 'unavailable', code: 'write_failed' };
661
- log(deps).error('cli.error', {
662
- kind: 'setup_proof_observation_write',
663
- request_id: proof.request_id,
664
- error_code: localPersistence.code,
665
- });
666
- }
667
- }
668
- return readyFromObservation(observed.observedAt, { status: 'observed' }, localPersistence);
669
- }
670
- if (proof.companion_receipt.state === 'observed') {
671
- return readyFromObservation(proof.companion_receipt.observed_at, { status: 'unknown' });
672
- }
673
- const now = (deps.now ?? Date.now)();
674
- if (setupProofIsStale(proof, now)) {
675
- return {
676
- id: 'proof',
677
- title: 'Delivery proof',
678
- status: 'gap',
679
- detail: `${proof.request_id} is older than ${SETUP_PROOF_STALE_MS / 36e5}h with no Companion Receipt; init will send a replacement`,
680
- remedy: {
681
- by: 'cli',
682
- summary: 'replace the stale verification notification',
683
- command: 'notifai init',
684
- },
685
- };
686
- }
687
- return {
688
- id: 'proof',
689
- title: 'Delivery proof',
690
- status: 'gap',
691
- detail: `${proof.request_id} was sent, but its Companion Receipt (the app's delivery confirmation) is still unknown`,
692
- remedy: {
693
- by: 'cli',
694
- summary: 'check the same verification notification again',
695
- command: 'notifai init',
696
- },
697
- };
698
- }
699
- catch (err) {
700
- const failure = setupProofEvidenceFailure(err);
701
- log(deps).error('cli.error', {
702
- kind: 'setup_proof_evidence_read',
703
- request_id: proof.request_id,
704
- error_code: failure.code,
705
- http_status: failure.http_status,
706
- });
707
- if (proof.companion_receipt.state === 'observed') {
708
- return readyFromObservation(proof.companion_receipt.observed_at, failure);
709
- }
710
- return {
711
- id: 'proof',
712
- title: 'Delivery proof',
713
- status: 'gap',
714
- detail: `could not read ${proof.request_id} evidence (${failure.code})`,
715
- technical: { live_evidence: failure },
716
- remedy: {
717
- by: 'cli',
718
- summary: 'retry the existing verification evidence check',
719
- command: 'notifai init',
720
- },
721
- };
722
- }
723
- }
724
- function setupProofEvidenceFailure(err) {
725
- if (err instanceof ApiCallError) {
726
- return { status: 'unavailable', code: err.code, http_status: err.status };
727
- }
728
- if (err instanceof NetworkError) {
729
- return { status: 'unavailable', code: 'network_error', http_status: null };
730
- }
731
- return { status: 'unavailable', code: 'unexpected_error', http_status: null };
732
- }
733
- export async function doctorCommand(deps, flags, options = {}) {
734
- const readiness = options.readiness ?? (await assessReadiness(deps, flags.json === true ? { json: true } : {}));
735
- await applyRegistryRecommendation(deps, readiness.states);
736
- const blocker = firstBlocker(readiness);
737
- const ok = blocker === null;
738
- // A human fallback exists only at a real terminal. Pipes, harnesses and
739
- // non-TTY callers get the structured agent variant even without --json.
740
- if (flags.json || deps.io.interactive !== true) {
741
- deps.io.out(JSON.stringify({ ...readinessJson(readiness), ok, exit_code: ok ? EXIT.ok : EXIT.failed }, null, 2));
742
- return ok ? EXIT.ok : EXIT.failed;
743
- }
744
- const line = (s) => `${s.title}: ${s.detail}`;
745
- await deps.io.intro?.('Notifai doctor');
746
- for (const s of readiness.states) {
747
- // Working software says nothing about versions. Soft/hard update states use
748
- // only the closed sentence and the exact command, never schema/capability
749
- // vocabulary or a server-provided action.
750
- if (s.id === 'contract') {
751
- if (s.status === 'ready' || s.status === 'unknown')
752
- continue;
753
- deps.io.out(s.detail);
754
- if (s.remedy?.by !== 'user-elsewhere' &&
755
- s.remedy?.summary === 'update Notifai' &&
756
- s.remedy.command !== undefined) {
757
- deps.io.out(s.remedy.command);
758
- }
759
- continue;
760
- }
761
- if (deps.io.check) {
762
- await deps.io.check(s.status !== 'gap', line(s), doctorTone(s.status));
763
- }
764
- else {
765
- const mark = s.status === 'gap'
766
- ? 'FAIL'
767
- : s.status === 'unknown'
768
- ? ' ? '
769
- : s.status === 'optional-gap'
770
- ? ' --'
771
- : 'ok ';
772
- deps.io.out(`${mark} ${line(s)}`);
773
- }
774
- }
775
- const surfacedUpdate = readiness.states.some((state) => state.id === 'contract' && (state.status === 'gap' || state.status === 'optional-gap'));
776
- if (blocker !== null && blocker.id !== 'contract') {
777
- await deps.io.outro?.(`Start with: ${remedyLine(blocker)}`);
778
- }
779
- else if (!surfacedUpdate) {
780
- await deps.io.outro?.('Everything looks good');
781
- }
782
- return ok ? EXIT.ok : EXIT.failed;
783
- }
784
- /** Readiness status as a report tone. */
785
- function doctorTone(status) {
786
- switch (status) {
787
- case 'ready':
788
- return 'ok';
789
- case 'gap':
790
- return 'bad';
791
- case 'optional-gap':
792
- return 'warn';
793
- case 'unknown':
794
- return 'pending';
795
- }
796
- }
797
- /** One line telling the reader what to actually do about a state. */
798
- export function remedyLine(state) {
799
- const remedy = state.remedy;
800
- if (!remedy)
801
- return state.detail;
802
- if (remedy.by === 'user-elsewhere')
803
- return remedy.summary;
804
- return remedy.command === undefined
805
- ? remedy.summary
806
- : `${remedy.summary} — run \`${remedy.command}\``;
807
- }
808
- /**
809
- * Whether the hook installation is internally ready, plus evidence that a
810
- * project session has fired it before. This cannot prove future execution or
811
- * end-to-end notification delivery without a live harness and device test.
812
- *
813
- * Every failure mode here was found the expensive way, by spawning a session
814
- * and watching nothing happen: hooks not installed, installed but never fired,
815
- * or left behind by an older build that named events this one does not serve.
816
- */
817
- /**
818
- * Hook diagnostics as readiness states.
819
- *
820
- * A thin adapter over `hookChecks`, whose every branch was found the expensive
821
- * way and is not worth re-deriving. The judgment added here is which failures
822
- * actually stand in the way.
823
- *
824
- * `doctor` is a report and keeps its strict verdict here: a Codex Stop handler
825
- * that would swallow an answer is a real failure and reads as one. `init` is
826
- * the other consumer, and it treats every state in this group as a report line
827
- * rather than a blocker — Question Routing is optional automation, and nothing
828
- * optional may stand between a send-capable setup and its delivery proof.
829
- *
830
- * `reportOnly` marks the checks that no command can close, only the passage of
831
- * a turn. Those are report lines even in `doctor`, because "look again" is not
832
- * a remedy.
833
- */
834
- function hookStates(deps) {
835
- const installations = findInstallations(deps.env, deps.hookAdapterHome, deps.hookPlatform);
836
- const { active, contested } = resolveActiveHarness(deps.env, deps.cwd, (deps.now ?? Date.now)());
837
- const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env });
838
- const settings = {
839
- id: 'question-routing-settings',
840
- title: 'Question routing settings',
841
- status: config.ask_notifications.value ? 'ready' : 'gap',
842
- detail: [
843
- `ask_notifications=${config.ask_notifications.value} (${config.ask_notifications.source})`,
844
- `ask_grace_seconds=${config.ask_grace_seconds.value} (${config.ask_grace_seconds.source})`,
845
- ].join(', '),
846
- ...(config.ask_notifications.value
847
- ? {}
848
- : {
849
- remedy: {
850
- by: 'cli',
851
- summary: 'enable asynchronous question routing',
852
- command: 'notifai config set ask_notifications true',
853
- },
854
- }),
855
- };
856
- if (installations.length === 0) {
857
- if (contested.length > 1) {
858
- return [
859
- {
860
- id: 'hooks',
861
- title: 'Question routing',
862
- status: 'optional-gap',
863
- detail: `Several harness sessions could own this shell (${contested.map((candidate) => candidate.label).join(', ')}); routing readiness is intentionally not attributed to either one`,
864
- },
865
- settings,
866
- ];
867
- }
868
- if (active !== null && !isHookInstallableHarness(active.harness)) {
869
- return [
870
- {
871
- id: 'hooks',
872
- title: 'Question routing',
873
- status: 'optional-gap',
874
- detail: `${active.label}: ${HERMES_QUESTION_ROUTING_UNAVAILABLE.deliveryContract}`,
875
- },
876
- settings,
877
- ];
878
- }
879
- if (active?.harness === 'hermes') {
880
- return [
881
- {
882
- id: 'hooks',
883
- title: 'Hermes activation',
884
- status: 'gap',
885
- detail: 'the Notifai Hermes plugin is not installed; Project Enablement and classic CLI Question Routing need it',
886
- remedy: {
887
- by: 'cli',
888
- summary: 'install the native Hermes plugin for Project activation',
889
- command: 'notifai hooks install --harness hermes',
890
- },
891
- },
892
- settings,
893
- ];
894
- }
895
- return [
896
- {
897
- id: 'hooks',
898
- title: 'Question routing',
899
- status: active === null ? 'optional-gap' : 'gap',
900
- detail: active === null
901
- ? 'hooks not installed, so questions stay in the terminal'
902
- : `active ${active.label} session detected, but ${active.label} hooks are not installed; \`notifai ask\` cannot route this session`,
903
- remedy: {
904
- by: 'cli',
905
- summary: 'install harness hooks so questions reach your devices when you are away',
906
- command: active === null
907
- ? 'notifai hooks install'
908
- : `notifai hooks install --harness ${active.harness}`,
909
- },
910
- },
911
- settings,
912
- ];
913
- }
914
- /**
915
- * A human title per check.
916
- *
917
- * Three checks used to collapse onto "Question routing", so a reader could not
918
- * tell which of them had failed, and the rest fell through to their internal
919
- * name — `hooks (stale)` beside `Delivery proof`. The `id` stays the stable
920
- * thing to branch on; this is only what a person reads.
921
- */
922
- const CHECK_TITLES = {
923
- hooks: 'Question routing',
924
- 'hooks (detected)': 'Harnesses detected',
925
- 'hooks (active harness)': 'Routing for this harness',
926
- 'hooks (active session)': 'Routing for this session',
927
- 'hooks (stale)': 'Hook definitions current',
928
- 'hooks (adapter)': 'Hook adapter',
929
- 'hooks (trust)': 'Codex hook trust',
930
- 'hooks (stop shape)': 'Turn-end hook shape',
931
- 'hooks (legacy project install)': 'Leftover project hook install',
932
- 'hooks (obsolete native plugin)': 'Obsolete native plugin wiring',
933
- 'hooks (codex representation)': 'Codex hook representation',
934
- 'hooks (question admission)': 'Question admission',
935
- 'hooks (fired)': 'Hooks have run here',
936
- 'hooks (answer continuation)': 'How an answer returns',
937
- 'hooks (wake route)': 'Direct wake route',
938
- 'hooks (openclaw plugin)': 'OpenClaw plugin loadability',
939
- 'hooks (hermes plugin)': 'Hermes plugin activation',
940
- };
941
- function checkTitle(name) {
942
- return CHECK_TITLES[name] ?? name;
943
- }
944
- function readinessRemedy(remedy) {
945
- if (remedy === undefined) {
946
- return {
947
- by: 'user-here',
948
- summary: 'the detail above names what to change',
949
- command: 'notifai hooks install',
950
- };
951
- }
952
- if (remedy.by === 'user-elsewhere')
953
- return remedy;
954
- if (remedy.by === 'cli')
955
- return { ...remedy, by: 'cli' };
956
- return { ...remedy, by: 'user-here' };
957
- }
958
- return [
959
- ...hookChecks(deps).map((check) => ({
960
- id: check.name.replace(/[ ()]+/g, '-').replace(/-$/, ''),
961
- title: checkTitle(check.name),
962
- status: check.ok
963
- ? 'ready'
964
- : check.reportOnly === true
965
- ? 'optional-gap'
966
- : 'gap',
967
- detail: check.detail,
968
- ...(check.technical === undefined ? {} : { technical: check.technical }),
969
- ...(check.ok || (check.reportOnly === true && check.remedy === undefined)
970
- ? {}
971
- : {
972
- remedy: readinessRemedy(check.remedy),
973
- }),
974
- })),
975
- settings,
976
- ];
977
- }
978
- function hookChecks(deps) {
979
- const checks = [];
980
- const installations = findInstallations(deps.env, deps.hookAdapterHome, deps.hookPlatform);
981
- // Not having hooks is a setup someone chose, not a fault: `send` works
982
- // without them. A setup that cannot work is what deserves to go red.
983
- if (installations.length === 0) {
984
- checks.push({
985
- name: 'hooks',
986
- ok: true,
987
- detail: 'not installed (optional) — `notifai hooks install` adds supported lifecycle wiring; Question Routing depends on the harness',
988
- });
989
- return checks;
990
- }
991
- checks.push({
992
- name: 'hooks',
993
- ok: true,
994
- detail: installations.map((i) => `${i.harness} (${i.file})`).join(', '),
995
- });
996
- if (installations.some((installation) => installation.harness === 'openclaw')) {
997
- const loadability = inspectOpenclawPluginLoadability(deps);
998
- checks.push({
999
- name: 'hooks (openclaw plugin)',
1000
- ok: loadability.loadable,
1001
- detail: loadability.warning ??
1002
- `OpenClaw permits Notifai to load; plugins.allow and plugins.deny do not block it`,
1003
- technical: {
1004
- openclaw_plugin_loadable: loadability.loadable,
1005
- config_file: loadability.configFile,
1006
- blocking_keys: loadability.blockers.map((blocker) => blocker.key),
1007
- config_valid: loadability.error === null,
1008
- },
1009
- ...(loadability.loadable || loadability.remedy === null
1010
- ? {}
1011
- : { remedy: { by: 'user-elsewhere', summary: loadability.remedy } }),
1012
- });
1013
- }
1014
- if (installations.some((installation) => installation.harness === 'hermes')) {
1015
- let enabled = false;
1016
- try {
1017
- enabled = hermesPluginListed(deps.env);
1018
- }
1019
- catch { /* CLI unavailable is a gap. */ }
1020
- const version = hermesVersionSupported(deps.env);
1021
- checks.push({
1022
- name: 'hooks (hermes plugin)',
1023
- ok: enabled && version,
1024
- detail: !version
1025
- ? 'Hermes v0.21.5 is required for the proven plugin activation path'
1026
- : enabled
1027
- ? 'Hermes lists the Notifai plugin as enabled; an enabled local classic CLI session starts the writer for Questions, Notes, and Answer Edits'
1028
- : 'Hermes does not list the Notifai plugin as enabled',
1029
- ...(enabled && version ? {} : {
1030
- remedy: {
1031
- by: 'user-here',
1032
- summary: 'enable the Notifai plugin through Hermes',
1033
- command: 'hermes plugins enable notifai',
1034
- },
1035
- }),
1036
- });
1037
- }
1038
- const wired = new Set(installations.map((installation) => installation.harness));
1039
- if (wired.has('grok')) {
1040
- checks.push({
1041
- name: 'hooks (Grok activation)',
1042
- ok: false,
1043
- reportOnly: true,
1044
- detail: 'Grok discards SessionStart and allowed UserPromptSubmit stdout, so hook activation context is unsupported; load the Notifai skill directly',
1045
- });
1046
- }
1047
- const unwired = detectedHarnesses(deps.cwd, deps.env).filter((harness) => !wired.has(harness));
1048
- if (unwired.length > 0) {
1049
- checks.push({
1050
- name: 'hooks (detected)',
1051
- ok: false,
1052
- reportOnly: true,
1053
- detail: `${unwired.map((harness) => HARNESS_LABELS[harness]).join(', ')} detected on this machine but not wired`,
1054
- remedy: {
1055
- summary: 'install hooks for every detected harness',
1056
- command: 'notifai hooks install',
1057
- },
1058
- });
1059
- }
1060
- const { active, contested } = resolveActiveHarness(deps.env, deps.cwd, (deps.now ?? Date.now)());
1061
- const activeInstallations = active === null
1062
- ? []
1063
- : installations.filter((installation) => installation.harness === active.harness);
1064
- if (contested.length > 1) {
1065
- checks.push({
1066
- name: 'hooks (active harness)',
1067
- ok: false,
1068
- reportOnly: true,
1069
- detail: `Several harness sessions could own this shell (${contested.map((candidate) => candidate.label).join(', ')}); routing readiness is intentionally not attributed to either one`,
1070
- });
1071
- }
1072
- else if (active !== null && !isHookInstallableHarness(active.harness)) {
1073
- checks.push({
1074
- name: 'hooks (active harness)',
1075
- ok: true,
1076
- reportOnly: true,
1077
- detail: `${active.label}: ${HERMES_QUESTION_ROUTING_UNAVAILABLE.deliveryContract}`,
1078
- });
1079
- }
1080
- else if (active !== null) {
1081
- checks.push({
1082
- name: 'hooks (active harness)',
1083
- ok: activeInstallations.length > 0,
1084
- detail: activeInstallations.length > 0
1085
- ? `active ${active.label} session has a matching hook installation`
1086
- : `active ${active.label} session has no matching hook installation — run \`notifai hooks install --harness ${active.harness}\``,
1087
- ...(activeInstallations.length > 0
1088
- ? {}
1089
- : {
1090
- remedy: {
1091
- summary: `install hooks for the active ${active.label} session`,
1092
- command: `notifai hooks install --harness ${active.harness}`,
1093
- },
1094
- }),
1095
- });
1096
- if (activeInstallations.length > 0) {
1097
- const exactState = active.sessionId === undefined
1098
- ? null
1099
- : readSessionState(active.sessionId, deps.env);
1100
- const activated = exactState?.harness === active.harness &&
1101
- (active.harness === 'hermes' || exactState.last_prompt_at !== undefined);
1102
- if (!activated) {
1103
- // The normal condition of hooks installed moments ago: the pointer
1104
- // appears when the harness next fires a hook, and no command can
1105
- // force that. Informational, so `init` walks on to the states it can
1106
- // actually prove instead of exiting over evidence only time produces.
1107
- checks.push({
1108
- name: 'hooks (active session)',
1109
- ok: false,
1110
- reportOnly: true,
1111
- detail: active.harness === 'hermes'
1112
- ? 'this Hermes Agent Session has not rendered the Notifai prompt section for an enabled Project; start a fresh local classic CLI session after enabling the Project'
1113
- : `active ${active.label} session has not published exact lifecycle state — send one ${active.label} prompt, then check again`,
1114
- remedy: {
1115
- summary: active.harness === 'hermes'
1116
- ? 'start a fresh local classic CLI Agent Session after enabling this Project'
1117
- : `send one ${active.label} prompt — its hook publishes the routing pointer`,
1118
- command: 'notifai doctor',
1119
- },
1120
- });
1121
- }
1122
- else {
1123
- checks.push({
1124
- name: 'hooks (active session)',
1125
- ok: true,
1126
- detail: active.harness === 'hermes'
1127
- ? 'the exact active Hermes Agent Session rendered Notifai activation for this enabled Project'
1128
- : `the exact active ${active.label} session has published lifecycle state`,
1129
- });
1130
- }
1131
- }
1132
- }
1133
- const attendants = sessionAttendantCheck(deps, active);
1134
- if (attendants !== null)
1135
- checks.push(attendants);
1136
- // A handler naming an event this build dropped exits 2 every time the harness
1137
- // fires it, which the harness reports as a hook failure.
1138
- const stale = installations.flatMap((i) => i.handlers
1139
- .filter((h) => {
1140
- const event = handlerEvent(h.command);
1141
- return event !== null && !HOOK_EVENTS.includes(event);
1142
- })
1143
- .map((h) => `${h.event} -> ${handlerEvent(h.command)} in ${i.file}`));
1144
- const missing = installations.flatMap((installation) => {
1145
- const required = requiredHookEvents(installation.harness, deps.hookPlatform);
1146
- const installed = new Set(installation.handlers
1147
- .map((handler) => handlerEvent(handler.command))
1148
- .filter((event) => event !== null));
1149
- const absent = required.filter((event) => !installed.has(event));
1150
- return absent.length === 0
1151
- ? []
1152
- : [`${installation.file} is missing ${absent.join(', ')}`];
1153
- });
1154
- checks.push({
1155
- name: 'hooks (stale)',
1156
- ok: stale.length === 0 && missing.length === 0,
1157
- detail: stale.length === 0 && missing.length === 0
1158
- ? 'every installed handler names an event this build serves, and every required lifecycle handler is present'
1159
- : `${[...stale, ...missing].join('; ')} — rerun \`notifai hooks install\` to refresh the complete lifecycle set`,
1160
- });
1161
- const adapterProblems = installations.flatMap((installation) => (installation.problems ?? []).map((problem) => `${installation.file}: ${problem}`));
1162
- const sharedAdapter = inspectHookAdapter(deps.hookAdapterHome, deps.hookPlatform);
1163
- adapterProblems.push(...sharedAdapter.problems);
1164
- const runningTarget = deps.hookInstallTarget;
1165
- const runningArtifact = runningTarget !== undefined && !isNpxAdapterTarget(runningTarget)
1166
- ? runningTarget.scriptPath
1167
- : process.argv[1];
1168
- const cliInstallations = inspectCliInstallations(deps.env, deps.hookPlatform ?? process.platform, {
1169
- ...(runningArtifact === undefined ? {} : { runningArtifactPath: runningArtifact }),
1170
- currentVersion: packageVersion(),
1171
- });
1172
- const effectiveArtifact = cliInstallations.effective?.artifact_path ?? null;
1173
- const adapterMismatch = sharedAdapter.problems.length === 0 &&
1174
- effectiveArtifact !== null &&
1175
- !hookAdapterTargetsArtifact(sharedAdapter.target, effectiveArtifact);
1176
- if (adapterMismatch) {
1177
- adapterProblems.push('the registered CLI does not match the effective notifai command');
1178
- }
1179
- if (adapterProblems.length > 0) {
1180
- checks.push({
1181
- name: 'hooks (adapter)',
1182
- ok: false,
1183
- // The adapter is one Machine-level file every harness runs through, so
1184
- // a defect in it is always this machine's to fix and always fixable.
1185
- detail: adapterProblems.join('; '),
1186
- ...(adapterMismatch
1187
- ? { technical: { effective_cli_artifact: effectiveArtifact, registered_target: sharedAdapter.target } }
1188
- : {}),
1189
- ...(adapterMismatch && adapterProblems.length === 1
1190
- ? {
1191
- remedy: {
1192
- by: 'cli',
1193
- summary: 'retarget Question Routing to the effective notifai command',
1194
- command: 'notifai init',
1195
- },
1196
- }
1197
- : {}),
1198
- });
1199
- }
1200
- const trustProblems = codexTrustProblems(installations, deps.env);
1201
- checks.push({
1202
- name: 'hooks (trust)',
1203
- ok: trustProblems.length === 0,
1204
- detail: trustProblems.length === 0
1205
- ? 'best-effort check matches current persisted Codex approvals; Notifai never writes the trust store, and `/hooks` is authoritative'
1206
- : `best-effort check only; Notifai never writes the trust store and \`/hooks\` is authoritative. ${trustProblems.join('; ')}`,
1207
- ...(trustProblems.length === 0
1208
- ? {}
1209
- : {
1210
- remedy: {
1211
- summary: 'open `/hooks` in Codex and approve the changed Notifai handlers',
1212
- command: '/hooks',
1213
- user_action: CODEX_HOOK_APPROVAL_USER_ACTION,
1214
- },
1215
- }),
1216
- });
1217
- const shapeProblems = installations.flatMap((installation) => stopShapeProblems(installation, deps.hookPlatform).map((problem) => `${problem} — run \`notifai hooks install --harness ${installation.harness}\``));
1218
- checks.push({
1219
- name: 'hooks (stop shape)',
1220
- ok: shapeProblems.length === 0,
1221
- detail: shapeProblems.length === 0
1222
- ? `every installed Stop handler declares the shape its harness and host need: Codex async everywhere, Claude Code async on POSIX and blocking on Windows with an explicit ${QUESTION_STOP_TIMEOUT_SECONDS}s full-window budget; non-routing blocking hosts ${NON_ROUTING_BLOCKING_STOP_TIMEOUT_SECONDS}s`
1223
- : shapeProblems.join('; '),
1224
- });
1225
- // A Project-scoped definition an older build wrote still fires beside the
1226
- // Machine one, so each event runs twice and the Project copy keeps serving a
1227
- // definition no install refreshes. Notifai no longer creates these and never
1228
- // accepts one as a working route: it is a gap with an exact removal command.
1229
- const legacy = findLegacyProjectInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform);
1230
- if (legacy.length > 0) {
1231
- const harnesses = [...new Set(legacy.map((installation) => installation.harness))];
1232
- const command = harnesses
1233
- .map((harness) => `notifai hooks install --harness ${harness}`)
1234
- .join(' && ');
1235
- checks.push({
1236
- name: 'hooks (legacy project install)',
1237
- ok: false,
1238
- detail: `${legacy
1239
- .map((installation) => `${installation.harness} (${installation.file})`)
1240
- .join(', ')} — Notifai installs for this machine only, and a leftover Project copy fires beside it. Run \`${command}\` to remove it`,
1241
- remedy: {
1242
- by: 'cli',
1243
- summary: 'remove the leftover Project-scoped Notifai hooks',
1244
- command,
1245
- },
1246
- });
1247
- }
1248
- // A leftover native plugin enablement fires the same adapter command as the
1249
- // document handlers. Question Routing is claim-locked, but SessionStart
1250
- // guidance is not: both paths inject, and uninstall that only strips the
1251
- // document half would lie that Notifai is gone.
1252
- const obsoletePlugins = findObsoleteNotifaiPluginWiring(deps.env, deps.hookPlatform);
1253
- if (obsoletePlugins.length > 0) {
1254
- const harnesses = [...new Set(obsoletePlugins.map((entry) => entry.harness))];
1255
- const command = harnesses
1256
- .map((harness) => `notifai hooks install --harness ${harness}`)
1257
- .join(' && ');
1258
- checks.push({
1259
- name: 'hooks (obsolete native plugin)',
1260
- ok: false,
1261
- detail: `${obsoletePlugins
1262
- .map((entry) => `${entry.harness} (${entry.keys.join(', ')} in ${entry.file})`)
1263
- .join(', ')} — a leftover Notifai plugin and document hooks must never both fire. Run \`${command}\` to disable the plugin enablement`,
1264
- remedy: {
1265
- by: 'cli',
1266
- summary: 'disable leftover Notifai native plugin wiring',
1267
- command,
1268
- },
1269
- });
1270
- }
1271
- const representationProblems = codexRepresentationProblems(deps.env, deps.hookPlatform);
1272
- if (representationProblems.length > 0) {
1273
- checks.push({
1274
- name: 'hooks (codex representation)',
1275
- ok: false,
1276
- detail: representationProblems.join('; '),
1277
- });
1278
- }
1279
- else {
1280
- const coexistence = codexCoexistenceNotes(deps.env, deps.hookPlatform);
1281
- if (coexistence.length > 0) {
1282
- checks.push({
1283
- name: 'hooks (codex representation)',
1284
- ok: true,
1285
- detail: coexistence.join('; '),
1286
- });
1287
- }
1288
- }
1289
- const codexHome = codexHomeNote(deps.env, deps.hookPlatform);
1290
- if (codexHome !== null) {
1291
- checks.push({ name: 'hooks (codex home)', ok: true, detail: codexHome });
1292
- }
1293
- if (active !== null && activeInstallations.length > 0) {
1294
- // One Machine installation per harness means the inventory no longer
1295
- // depends on which checkout activated the session, so admission reads the
1296
- // same installations doctor already listed.
1297
- const admissionProblems = activeQuestionRouteProblems(deps, active, installations);
1298
- checks.push({
1299
- name: 'hooks (question admission)',
1300
- ok: admissionProblems.length === 0,
1301
- ...(active.harness === 'hermes' ? { reportOnly: true } : {}),
1302
- detail: admissionProblems.length === 0
1303
- ? `the active ${active.label} route is exact, current, singular, trusted where applicable, and bounded by a live owner`
1304
- : admissionProblems.join('; '),
1305
- ...(admissionProblems.includes(CODEX_STOP_DEFINITION_NOT_SINGULAR_PROBLEM)
1306
- ? {
1307
- remedy: {
1308
- by: 'cli',
1309
- summary: 'restore exactly one current Codex Stop definition',
1310
- command: 'notifai hooks install --harness codex',
1311
- },
1312
- }
1313
- : admissionProblems.includes(CODEX_STALE_STOP_DEFINITION_PROBLEM)
1314
- ? {
1315
- remedy: {
1316
- summary: 'start one fresh Codex session so it loads the current trusted Stop definition',
1317
- command: 'notifai doctor',
1318
- user_action: CODEX_FRESH_SESSION_USER_ACTION,
1319
- },
1320
- }
1321
- : {}),
1322
- });
1323
- }
1324
- // Which route this judges, and which remedy it prints, must both belong to
1325
- // the harness that is actually running here. A machine-global installation
1326
- // matches every directory, so picking whichever installation matched — or
1327
- // whichever session last wrote a pointer — reports on a harness the agent
1328
- // cannot influence: it is told to send a prompt in a harness that is not
1329
- // running, follows the fail-closed rule, and can never clear the check.
1330
- const firedPointer = active === null
1331
- ? readProjectSessionPointer(deps.cwd, deps.env, (deps.now ?? Date.now)())
1332
- : active.sessionId === undefined
1333
- ? (readLiveProjectSessionPointers(deps.cwd, deps.env, (deps.now ?? Date.now)()).find((pointer) => pointer.harness === active.harness) ?? null)
1334
- : readSessionState(active.sessionId, deps.env).harness === active.harness
1335
- ? { sessionId: active.sessionId, harness: active.harness }
1336
- : null;
1337
- const firedState = firedPointer === null ? null : readSessionState(firedPointer.sessionId, deps.env);
1338
- const promptFired = firedState?.last_prompt_at !== undefined;
1339
- if (active?.harness === 'codex') {
1340
- const observed = active.sessionId !== undefined && codexNativeActivityObserved(active.sessionId, deps.env);
1341
- checks.push({
1342
- name: 'hooks (native activity)', ok: observed, reportOnly: true,
1343
- detail: observed
1344
- ? 'the current Codex native lifecycle agrees with this exact session activity record'
1345
- : 'current Codex lifecycle activation is unverified; installed files and historical callbacks do not prove that the running session uses them',
1346
- });
1347
- }
1348
- const stopFired = firedState?.last_stop_at !== undefined;
1349
- const hermesActivated = firedPointer?.harness === 'hermes' && firedState?.harness === 'hermes';
1350
- const fired = firedPointer !== null && (promptFired || hermesActivated);
1351
- // Installations for other harnesses are irrelevant to the active one, and an
1352
- // active harness with none of its own has nothing to activate: say that
1353
- // instead of advising a prompt in some other harness. When the environment
1354
- // is contested and nothing has fired, every candidate is still possible, so
1355
- // the advice covers all of them rather than betting on one.
1356
- const activationHarnesses = new Set(contested.length > 1
1357
- ? contested.map((candidate) => candidate.harness)
1358
- : active === null
1359
- ? installations.map((installation) => installation.harness)
1360
- : [active.harness]);
1361
- const activationInstallations = installations.filter((installation) => activationHarnesses.has(installation.harness));
1362
- const activationAdvice = activationInstallations.length > 0
1363
- ? hookActivationAdvice(activationInstallations)
1364
- : active === null
1365
- ? hookActivationAdvice(installations)
1366
- : `${active.label}: no ${active.label} hook installation matches this project — run \`notifai hooks install --harness ${active.harness}\`.`;
1367
- checks.push({
1368
- name: 'hooks (fired)',
1369
- ok: fired,
1370
- // UserPromptSubmit proves that this exact session reached the installed
1371
- // lifecycle path. A historical Stop is only telemetry: requiring one made
1372
- // a first-turn question impossible even after the current Stop definition,
1373
- // trust, shape, and continuation owner had all been proven above.
1374
- reportOnly: true,
1375
- detail: hermesActivated
1376
- ? 'the exact Hermes Agent Session rendered its native Notifai prompt section for this enabled Project'
1377
- : fired
1378
- ? active === null
1379
- ? `a session in this directory has run UserPromptSubmit and is ready for this turn's Stop${stopFired ? '; an earlier Stop was also observed' : ''}`
1380
- : `the active ${active.label} session has run UserPromptSubmit and is ready for this turn's Stop${stopFired ? '; an earlier Stop was also observed' : ''}`
1381
- : firedPointer === null
1382
- ? `no ${contested.length > 1
1383
- ? `session pointer for any harness whose markers are present here (${contested
1384
- .map((candidate) => candidate.label)
1385
- .join(', ')})`
1386
- : active === null
1387
- ? 'session pointer'
1388
- : `${active.label} session pointer`} from the last 24 hours — ${activationAdvice}`
1389
- : `the routed session has not fired UserPromptSubmit — send one prompt, then check again`,
1390
- ...(fired
1391
- ? {}
1392
- : {
1393
- remedy: {
1394
- summary: firedPointer === null
1395
- ? `send one ${active === null || contested.length > 1 ? '' : `${active.label} `}prompt in a session here, then re-check`
1396
- : 'send one prompt in the routed session, then re-check',
1397
- command: 'notifai doctor',
1398
- },
1399
- }),
1400
- });
1401
- const continuationInstallations = active !== null ? activeInstallations : installations;
1402
- const continuationHarnesses = [
1403
- ...new Set(continuationInstallations.map((installation) => installation.harness)),
1404
- ];
1405
- const openclawContinuationReady = !continuationHarnesses.includes('openclaw') ||
1406
- openclawGatewayReady(deps.env);
1407
- const hermesContinuationReady = !continuationHarnesses.includes('hermes') ||
1408
- (active?.harness === 'hermes' && active.integrationInstance !== undefined &&
1409
- active.sessionId !== undefined && hermesQuestionRouteReady(active.sessionId, deps.env));
1410
- checks.push({
1411
- name: 'hooks (answer continuation)',
1412
- ok: continuationHarnesses.length > 0 &&
1413
- openclawContinuationReady &&
1414
- hermesContinuationReady &&
1415
- continuationHarnesses.every((harness) => questionRoutingCapability(harness, deps.hookPlatform ?? process.platform)
1416
- .stopContinuation !== 'unsupported'),
1417
- reportOnly: active === null || active.harness === 'hermes',
1418
- detail: continuationHarnesses.length === 0
1419
- ? active === null
1420
- ? 'no installed harness route to assess'
1421
- : `the active ${active.label} session has no matching continuation adapter`
1422
- : !openclawContinuationReady
1423
- ? 'openclaw: local Gateway service has not verified its CLI version and process identity'
1424
- : !hermesContinuationReady
1425
- ? 'Hermes: this exact local classic CLI session has no current attending plugin writer'
1426
- : continuationHarnesses
1427
- .map((harness) => `${harness}: ${questionRoutingCapability(harness, deps.hookPlatform ?? process.platform).deliveryContract}`)
1428
- .join('; '),
1429
- });
1430
- const wakeRoute = wakeRouteCheck(deps, active, activeInstallations);
1431
- if (wakeRoute !== null)
1432
- checks.push(wakeRoute);
1433
- return checks;
1434
- }
1435
- /**
1436
- * Whether an answer arriving after this turn's ordinary continuation returns
1437
- * could start another turn in this exact session without a new User prompt.
1438
- * This is a named optional capability, separate from the held Stop
1439
- * continuation that already makes Question Routing usable.
1440
- *
1441
- * Read-only, and deliberately so: nothing here connects to a socket, takes a
1442
- * lock, or sends a message. A diagnostic that wakes the agent it is diagnosing
1443
- * would be its own bug report.
1444
- *
1445
- * Everything it can report negatively is optional rather than a routing
1446
- * failure: a held continuation can still return the answer, or the accepted
1447
- * journal can replay it at the session's next turn. What it buys is that the
1448
- * direct-wake capability and its limits have an explicit name.
1449
- *
1450
- * It never asks for `crossSessionInbound`. The poster is the session's own
1451
- * hook child and takes the privileged own-child path, verified to be delivered
1452
- * even against a `bypassPermissions` receiver while an unrelated process was
1453
- * held. Widening a user's general inbound policy to suit Notifai would be a
1454
- * real change to their machine's posture in exchange for nothing.
1455
- */
1456
- function wakeRouteCheck(deps, active, activeInstallations) {
1457
- if (active === null || activeInstallations.length === 0)
1458
- return null;
1459
- if (active.harness === 'claude-code') {
1460
- const platform = deps.hookPlatform ?? process.platform;
1461
- if (platform === 'win32') {
1462
- return {
1463
- name: 'hooks (wake route)',
1464
- ok: false,
1465
- reportOnly: true,
1466
- technical: { direct_wake_optional: true },
1467
- detail: 'direct inbox wake is unavailable on Windows; the held Stop still returns the answer to this same Agent Session without another User prompt',
1468
- };
1469
- }
1470
- const readiness = inspectClaudeInbox({
1471
- pid: deps.claudeSourcePid ?? claudeSessionPid(deps.env),
1472
- platform,
1473
- readDescriptor: deps.claudeWake?.readDescriptor ?? systemClaudeWakeAdapters(deps.env).readDescriptor,
1474
- socketExists: (socketPath) => existsSync(socketPath),
1475
- });
1476
- return {
1477
- name: 'hooks (wake route)',
1478
- ok: readiness.state === 'ready',
1479
- reportOnly: true,
1480
- detail: readiness.state === 'ready'
1481
- ? `this Claude Code ${readiness.version} session is listening on ${readiness.socketPath}, so an answer can start a turn here without you`
1482
- : `${readiness.reason}. Answers are still delivered, at this session's next turn rather than on their own`,
1483
- };
1484
- }
1485
- if (active.harness === 'codex') {
1486
- const readiness = inspectCodexQueue(active.sessionId, deps.env);
1487
- return {
1488
- name: 'hooks (wake route)',
1489
- ok: readiness.state === 'ready',
1490
- reportOnly: true,
1491
- technical: { direct_wake_optional: true },
1492
- detail: readiness.state === 'ready'
1493
- ? `the Stop hook returns at once and the answer is queued into this thread's own inbox in ${codexQueueHomeDirectory(deps.env)}, so it starts a turn here without you — within seconds when this session is live, and at its next opening when it is not`
1494
- : `${readiness.reason}. Answers are still delivered, at this session's next turn rather than on their own`,
1495
- };
1496
- }
1497
- if (active.harness === 'grok') {
1498
- return {
1499
- name: 'hooks (wake route)',
1500
- ok: false,
1501
- reportOnly: true,
1502
- technical: { direct_wake_optional: true },
1503
- detail: 'Grok has no out-of-band wake route; its held Stop returns the answer to this same Agent Session within the complete answer window',
1504
- };
1505
- }
1506
- return null;
1507
- }
1508
- /**
1509
- * Session Attendants on this machine: which sessions are attended and in what
1510
- * phase. Informational: presence is a convenience, never a routing gate.
1511
- */
1512
- function sessionAttendantCheck(deps, active) {
1513
- const reports = listAttendantReports(deps.env);
1514
- const live = reports.filter((report) => report.alive);
1515
- const describe = (report) => [
1516
- report.phase,
1517
- report.generation === null ? null : `lease generation ${report.generation}`,
1518
- report.activity,
1519
- report.phase === 'exited' ? null : report.accepts_messages ? 'accepts notes' : 'notes unavailable',
1520
- report.reason,
1521
- ]
1522
- .filter((part) => part !== null)
1523
- .join(', ');
1524
- const own = active?.sessionId === undefined
1525
- ? undefined
1526
- : reports.find((report) => report.session_id === active.sessionId);
1527
- const attended = (active?.harness === 'claude-code' || active?.harness === 'codex' ||
1528
- active?.harness === 'hermes' || active?.harness === 'openclaw') &&
1529
- attendantSupport(active.harness, deps.hookPlatform ?? process.platform).supported;
1530
- if (reports.length === 0 && !attended)
1531
- return null;
1532
- const parts = [];
1533
- if (attended && active.sessionId !== undefined) {
1534
- parts.push(own === undefined
1535
- ? active.harness === 'openclaw'
1536
- ? 'this OpenClaw session has no attendant yet (the local Gateway service starts one after its first prompt)'
1537
- : `this ${active.label} session has no attendant yet (one starts with the session or its next prompt)`
1538
- : own.alive
1539
- ? `this session: ${describe(own)}`
1540
- : active.harness === 'openclaw'
1541
- ? `this session's attendant stopped (${describe(own)}); the local Gateway service starts another while the generation remains current`
1542
- : `this session's attendant stopped (${describe(own)}); its next prompt starts another`);
1543
- }
1544
- parts.push(`${live.length} running on this machine`);
1545
- return {
1546
- name: 'session attendant',
1547
- ok: true,
1548
- reportOnly: true,
1549
- detail: parts.join('; '),
1550
- technical: { attendants: live },
1551
- };
1552
- }
1553
- //# sourceMappingURL=commands-doctor.js.map