@raidiant/notifai 10.0.0 → 10.1.1

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 (127) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +4 -1
  3. package/dist/cli-bin.d.ts +21 -1
  4. package/dist/cli-bin.js +141 -10
  5. package/dist/cli-bin.js.map +1 -1
  6. package/dist/cli-update-contract.d.ts +7 -0
  7. package/dist/cli-update-contract.js +10 -0
  8. package/dist/cli-update-contract.js.map +1 -0
  9. package/dist/client.d.ts +6 -1
  10. package/dist/client.js +1 -0
  11. package/dist/client.js.map +1 -1
  12. package/dist/codex-wake.d.ts +2 -0
  13. package/dist/codex-wake.js +2 -1
  14. package/dist/codex-wake.js.map +1 -1
  15. package/dist/commands-auth.d.ts +15 -1
  16. package/dist/commands-auth.js +32 -14
  17. package/dist/commands-auth.js.map +1 -1
  18. package/dist/commands-core.d.ts +10 -1
  19. package/dist/commands-core.js +13 -4
  20. package/dist/commands-core.js.map +1 -1
  21. package/dist/commands-devices.d.ts +10 -1
  22. package/dist/commands-devices.js +17 -13
  23. package/dist/commands-devices.js.map +1 -1
  24. package/dist/commands-doctor.js +158 -71
  25. package/dist/commands-doctor.js.map +1 -1
  26. package/dist/commands-harness-context.js +8 -0
  27. package/dist/commands-harness-context.js.map +1 -1
  28. package/dist/commands-hooks.js +169 -15
  29. package/dist/commands-hooks.js.map +1 -1
  30. package/dist/commands-init.d.ts +16 -0
  31. package/dist/commands-init.js +254 -91
  32. package/dist/commands-init.js.map +1 -1
  33. package/dist/commands-send.d.ts +1 -1
  34. package/dist/commands-send.js +121 -35
  35. package/dist/commands-send.js.map +1 -1
  36. package/dist/commands-update.d.ts +10 -0
  37. package/dist/commands-update.js +141 -0
  38. package/dist/commands-update.js.map +1 -0
  39. package/dist/commands.d.ts +2 -1
  40. package/dist/commands.js +2 -1
  41. package/dist/commands.js.map +1 -1
  42. package/dist/harnesses.d.ts +2 -2
  43. package/dist/harnesses.js +22 -2
  44. package/dist/harnesses.js.map +1 -1
  45. package/dist/hook-adapter.d.ts +2 -0
  46. package/dist/hook-adapter.js +12 -1
  47. package/dist/hook-adapter.js.map +1 -1
  48. package/dist/hooks.d.ts +43 -2
  49. package/dist/hooks.js +163 -21
  50. package/dist/hooks.js.map +1 -1
  51. package/dist/install-hooks.js +15 -9
  52. package/dist/install-hooks.js.map +1 -1
  53. package/dist/interactive.js +20 -23
  54. package/dist/interactive.js.map +1 -1
  55. package/dist/main-run.d.ts +1 -0
  56. package/dist/main-run.js +42 -0
  57. package/dist/main-run.js.map +1 -0
  58. package/dist/main.js +10 -31
  59. package/dist/main.js.map +1 -1
  60. package/dist/node-floor.d.ts +18 -0
  61. package/dist/node-floor.js +35 -0
  62. package/dist/node-floor.js.map +1 -0
  63. package/dist/openclaw-plugin.d.ts +49 -0
  64. package/dist/openclaw-plugin.js +350 -0
  65. package/dist/openclaw-plugin.js.map +1 -0
  66. package/dist/program.d.ts +2 -1
  67. package/dist/program.js +31 -13
  68. package/dist/program.js.map +1 -1
  69. package/dist/question-settlement-process.d.ts +15 -0
  70. package/dist/question-settlement-process.js +35 -0
  71. package/dist/question-settlement-process.js.map +1 -0
  72. package/dist/readiness.d.ts +25 -1
  73. package/dist/readiness.js +32 -2
  74. package/dist/readiness.js.map +1 -1
  75. package/dist/session-activation.js +1 -1
  76. package/dist/session-activation.js.map +1 -1
  77. package/dist/setup-destinations.d.ts +23 -0
  78. package/dist/setup-destinations.js +44 -0
  79. package/dist/setup-destinations.js.map +1 -0
  80. package/dist/ui/help.d.ts +4 -5
  81. package/dist/ui/help.js +5 -6
  82. package/dist/ui/help.js.map +1 -1
  83. package/package.json +2 -2
  84. package/src/cli-bin.test.ts +145 -7
  85. package/src/cli-bin.ts +176 -10
  86. package/src/cli-update-contract.ts +9 -0
  87. package/src/cli-update.test.ts +125 -0
  88. package/src/client.ts +7 -0
  89. package/src/codex-wake.ts +5 -1
  90. package/src/commands-auth.ts +47 -13
  91. package/src/commands-core.ts +21 -4
  92. package/src/commands-devices.ts +23 -16
  93. package/src/commands-doctor.ts +197 -75
  94. package/src/commands-harness-context.ts +8 -0
  95. package/src/commands-hooks.ts +210 -18
  96. package/src/commands-init.ts +275 -92
  97. package/src/commands-send.ts +145 -39
  98. package/src/commands-update.ts +180 -0
  99. package/src/commands.test.ts +701 -53
  100. package/src/commands.ts +2 -0
  101. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +57 -0
  102. package/src/harness-integration.test.ts +82 -0
  103. package/src/harnesses.test.ts +9 -1
  104. package/src/harnesses.ts +24 -2
  105. package/src/hook-adapter.ts +14 -0
  106. package/src/hooks.test.ts +275 -0
  107. package/src/hooks.ts +270 -18
  108. package/src/install-hooks.test.ts +208 -0
  109. package/src/install-hooks.ts +19 -9
  110. package/src/interactive.test.ts +31 -0
  111. package/src/interactive.ts +20 -26
  112. package/src/main-run.ts +45 -0
  113. package/src/main.ts +9 -33
  114. package/src/node-floor.test.ts +27 -0
  115. package/src/node-floor.ts +37 -0
  116. package/src/openclaw-plugin.ts +409 -0
  117. package/src/program.test.ts +35 -0
  118. package/src/program.ts +37 -12
  119. package/src/question-settlement-process.ts +47 -0
  120. package/src/readiness.ts +35 -2
  121. package/src/session-activation.ts +1 -1
  122. package/src/setup-destinations.test.ts +41 -0
  123. package/src/setup-destinations.ts +50 -0
  124. package/src/skill.test.ts +13 -2
  125. package/src/ui/help.test.ts +16 -0
  126. package/src/ui/help.ts +6 -7
  127. package/src/commands-devices.test.ts +0 -15
@@ -11,7 +11,11 @@ import {
11
11
  HERMES_QUESTION_ROUTING_UNAVAILABLE,
12
12
  isHookInstallableHarness,
13
13
  } from './harnesses.js'
14
- import { inspectHookAdapter } from './hook-adapter.js'
14
+ import {
15
+ hookAdapterTargetsArtifact,
16
+ inspectHookAdapter,
17
+ isNpxAdapterTarget,
18
+ } from './hook-adapter.js'
15
19
  import {
16
20
  readLiveProjectSessionPointers,
17
21
  readProjectSessionPointer,
@@ -48,14 +52,16 @@ import type { Tone } from './ui/theme.js'
48
52
  import type { MachineCredential } from './credentials.js'
49
53
  import {
50
54
  EXIT,
51
- UPDATE_CLI_COMMAND,
55
+ SETUP_COMMAND,
52
56
  diagnoseIgnoredOriginOverride,
53
57
  loadLoggedConfig,
54
58
  makeClient,
55
59
  resolvedBaseUrl,
60
+ updateCliCommand,
56
61
  type CommandDeps,
57
62
  } from './commands-core.js'
58
- import { deviceInstallRemedy, readyCompanionDevices, supportPageUrl } from './commands-devices.js'
63
+ import { deviceInstallRemedy, readyCompanionDevices } from './commands-devices.js'
64
+ import { setupAccessUrl } from './setup-destinations.js'
59
65
  import {
60
66
  claudeSessionPid,
61
67
  resolveActiveHarness,
@@ -68,7 +74,7 @@ import {
68
74
  hookActivationAdvice,
69
75
  requiredHookEvents,
70
76
  } from './commands-hooks.js'
71
- import { cliBinReadiness } from './cli-bin.js'
77
+ import { cliBinReadiness, inspectCliInstallations } from './cli-bin.js'
72
78
  import {
73
79
  latestPublishedCliVersion,
74
80
  newerPublishedCli,
@@ -95,7 +101,7 @@ import { projectBinding, projectEnabled } from './project-enablement.js'
95
101
  * versions and document integers are structured inventory, never routing or a
96
102
  * definition of "up to date".
97
103
  */
98
- async function compatibilityCheck(client: ApiClient): Promise<ReadinessState> {
104
+ async function compatibilityCheck(client: ApiClient, deps: CommandDeps): Promise<ReadinessState> {
99
105
  try {
100
106
  const [compatibility, documents] = await Promise.all([
101
107
  client.compatibility(),
@@ -130,7 +136,7 @@ async function compatibilityCheck(client: ApiClient): Promise<ReadinessState> {
130
136
  remedy: {
131
137
  by: 'user-here',
132
138
  summary: 'update Notifai',
133
- command: UPDATE_CLI_COMMAND,
139
+ command: updateCliCommand(deps),
134
140
  },
135
141
  }
136
142
  }
@@ -147,7 +153,7 @@ async function compatibilityCheck(client: ApiClient): Promise<ReadinessState> {
147
153
  remedy: {
148
154
  by: 'user-here',
149
155
  summary: 'update Notifai',
150
- command: UPDATE_CLI_COMMAND,
156
+ command: updateCliCommand(deps),
151
157
  },
152
158
  }
153
159
  }
@@ -303,6 +309,110 @@ function remoteInvalidatedByConfig(
303
309
  return server === undefined || !server.detail.includes(origin)
304
310
  }
305
311
 
312
+ function isNoActivePlanError(err: unknown): err is ApiCallError {
313
+ return err instanceof ApiCallError && err.code === 'no_active_plan'
314
+ }
315
+
316
+ /**
317
+ * Device listing is grant-gated; access status is not. A paired Account whose
318
+ * plan lapsed must not be diagnosed as a revoked machine.
319
+ */
320
+ async function probeAccount(
321
+ client: ApiClient,
322
+ credential: MachineCredential,
323
+ baseUrl: string,
324
+ ): Promise<{
325
+ email: string | null
326
+ devices: RoutableDevice[] | null
327
+ lookupFailed: boolean
328
+ auth: ReadinessState
329
+ }> {
330
+ const [devicesOutcome, accessOutcome, requestOutcome] = await Promise.allSettled([
331
+ Promise.resolve().then(() => client.listDevices()),
332
+ Promise.resolve().then(() => client.accessStatus()),
333
+ Promise.resolve().then(() => client.accessRequest()),
334
+ ])
335
+ const access = accessOutcome.status === 'fulfilled' ? accessOutcome.value : null
336
+ const pendingRequest =
337
+ requestOutcome.status === 'fulfilled' ? requestOutcome.value.request : null
338
+ const devices = devicesOutcome.status === 'fulfilled' ? devicesOutcome.value.devices : null
339
+ const email = access?.email ?? null
340
+ const devicesErr = devicesOutcome.status === 'rejected' ? devicesOutcome.reason : null
341
+ const noActivePlan =
342
+ access?.status === 'no_active_plan' || isNoActivePlanError(devicesErr)
343
+
344
+ if (noActivePlan) {
345
+ const next =
346
+ (isNoActivePlanError(devicesErr) ? devicesErr.nextAction : null) ??
347
+ `Open ${setupAccessUrl(baseUrl)} to set up access, then retry.`
348
+ const who = email ? ` (${email})` : ''
349
+ // Three values, not two. Someone who asked yesterday is waiting on a person,
350
+ // not on an action of theirs, and telling them to ask again — every run,
351
+ // forever — is the only thing this terminal could get wrong about a wait it
352
+ // cannot shorten.
353
+ return {
354
+ email,
355
+ devices: null,
356
+ lookupFailed: true,
357
+ auth:
358
+ pendingRequest === null
359
+ ? {
360
+ id: 'auth',
361
+ title: 'Account',
362
+ status: 'gap',
363
+ detail: `this account does not have access to Notifai yet${who}`,
364
+ remedy: { by: 'user-elsewhere', summary: next },
365
+ }
366
+ : {
367
+ id: 'auth',
368
+ title: 'Account',
369
+ status: 'gap',
370
+ detail: `access requested on ${pendingRequest.requested_at.slice(0, 10)}${who} — waiting to be granted`,
371
+ remedy: {
372
+ by: 'user-elsewhere',
373
+ summary:
374
+ 'nothing is needed from you; this resumes on the next run once access is granted',
375
+ },
376
+ },
377
+ }
378
+ }
379
+
380
+ if (devices !== null) {
381
+ return {
382
+ email,
383
+ devices,
384
+ lookupFailed: false,
385
+ auth: {
386
+ id: 'auth',
387
+ title: 'Account',
388
+ status: 'ready',
389
+ detail: email
390
+ ? `machine ${credential.machineId} accepted (${email})`
391
+ : `machine ${credential.machineId} accepted`,
392
+ },
393
+ }
394
+ }
395
+
396
+ const err = devicesErr ?? (accessOutcome.status === 'rejected' ? accessOutcome.reason : null)
397
+ return {
398
+ email: null,
399
+ devices: null,
400
+ lookupFailed: true,
401
+ auth: {
402
+ id: 'auth',
403
+ title: 'Account',
404
+ status: 'gap',
405
+ detail: err instanceof ApiCallError ? `${err.code}: ${err.message}` : String(err),
406
+ remedy: {
407
+ by: 'user-here',
408
+ summary: 'this machine is no longer recognised; pair it again',
409
+ command: SETUP_COMMAND,
410
+ interactive: true,
411
+ },
412
+ },
413
+ }
414
+ }
415
+
306
416
  /**
307
417
  * Read the whole setup once, in dependency order.
308
418
  *
@@ -378,7 +488,7 @@ export async function assessReadiness(
378
488
  remedy: {
379
489
  by: 'user-here',
380
490
  summary: 'sign in — this opens your browser to approve the machine',
381
- command: 'notifai login',
491
+ command: SETUP_COMMAND,
382
492
  interactive: true,
383
493
  },
384
494
  },
@@ -426,7 +536,7 @@ export async function assessReadiness(
426
536
  baseUrl,
427
537
  `Bearer nfm_${credential.machineId}.${credential.secret}`,
428
538
  )
429
- states.push(await compatibilityCheck(compatibilityClient))
539
+ states.push(await compatibilityCheck(compatibilityClient, deps))
430
540
  }
431
541
 
432
542
  let accountEmail: string | null = null
@@ -437,39 +547,11 @@ export async function assessReadiness(
437
547
  } else {
438
548
  const client = makeClient(deps, baseUrl, `Bearer nfm_${credential.machineId}.${credential.secret}`)
439
549
  accountClient = client
440
- try {
441
- const [{ devices }, email] = await Promise.all([
442
- client.listDevices(),
443
- Promise.resolve()
444
- .then(async () => (await client.accessStatus()).email)
445
- .catch(() => null as string | null),
446
- ])
447
- accountDevices = devices
448
- accountEmail = email
449
- states.push({
450
- id: 'auth',
451
- title: 'Account',
452
- status: 'ready',
453
- detail: accountEmail
454
- ? `machine ${credential.machineId} accepted (${accountEmail})`
455
- : `machine ${credential.machineId} accepted`,
456
- })
457
- } catch (err) {
458
- // A credential the server rejects is revocation, not absence, and the
459
- // remedy is the same sign-in either way.
460
- accountLookupFailed = true
461
- states.push({
462
- id: 'auth',
463
- title: 'Account',
464
- status: 'gap',
465
- detail: err instanceof ApiCallError ? `${err.code}: ${err.message}` : String(err),
466
- remedy: {
467
- by: 'user-here',
468
- summary: 'this machine is no longer recognised; pair it again',
469
- command: 'notifai login',
470
- },
471
- })
472
- }
550
+ const probed = await probeAccount(client, credential, baseUrl)
551
+ accountDevices = probed.devices
552
+ accountEmail = probed.email
553
+ accountLookupFailed = probed.lookupFailed
554
+ states.push(probed.auth)
473
555
  }
474
556
 
475
557
  // Optional setup that works without a companion device must appear before the
@@ -509,7 +591,7 @@ export async function assessReadiness(
509
591
  // dashboard origin — not a placeholder, and not typed by hand.
510
592
  detail:
511
593
  companionDevices.length === 0
512
- ? `no active Companion device registered yet; install Notifai via ${supportPageUrl(baseUrl)}`
594
+ ? 'no active Companion App registered yet'
513
595
  : `${companionDevices.map((d) => `${d.display_name} (${d.platform}, ${d.permission_status})`).join(', ')} — registered but not able to receive`,
514
596
  remedy: {
515
597
  by: 'user-elsewhere',
@@ -553,7 +635,7 @@ async function applyRegistryRecommendation(
553
635
  contract.remedy = {
554
636
  by: 'user-here',
555
637
  summary: 'update Notifai',
556
- command: UPDATE_CLI_COMMAND,
638
+ command: updateCliCommand(deps),
557
639
  }
558
640
  }
559
641
 
@@ -684,8 +766,12 @@ export async function doctorCommand(
684
766
  if (s.id === 'contract') {
685
767
  if (s.status === 'ready' || s.status === 'unknown') continue
686
768
  deps.io.out(s.detail)
687
- if (s.remedy?.by !== 'user-elsewhere' && s.remedy?.command === UPDATE_CLI_COMMAND) {
688
- deps.io.out(UPDATE_CLI_COMMAND)
769
+ if (
770
+ s.remedy?.by !== 'user-elsewhere' &&
771
+ s.remedy?.summary === 'update Notifai' &&
772
+ s.remedy.command !== undefined
773
+ ) {
774
+ deps.io.out(s.remedy.command)
689
775
  }
690
776
  continue
691
777
  }
@@ -754,14 +840,15 @@ export function remedyLine(state: ReadinessState): string {
754
840
  * way and is not worth re-deriving. The judgment added here is which failures
755
841
  * actually stand in the way.
756
842
  *
757
- * Not everything failed is in the way. A pointer that has never been
758
- * published is the normal condition of an install thirty seconds old — the
759
- * next prompt fixes it and no command can. Treating that as blocking would
760
- * mean `init` could only finish after a session had already run — so it
761
- * reports as something worth knowing rather than something to
762
- * fix, and `init` walks on to the states it can actually close, delivery
763
- * proof included. Which failures are informational, and what the true remedy
764
- * is, is each check's own call (`HookCheck`).
843
+ * `doctor` is a report and keeps its strict verdict here: a Codex Stop handler
844
+ * that would swallow an answer is a real failure and reads as one. `init` is
845
+ * the other consumer, and it treats every state in this group as a report line
846
+ * rather than a blocker — Question Routing is optional automation, and nothing
847
+ * optional may stand between a send-capable setup and its delivery proof.
848
+ *
849
+ * `reportOnly` marks the checks that no command can close, only the passage of
850
+ * a turn. Those are report lines even in `doctor`, because "look again" is not
851
+ * a remedy.
765
852
  */
766
853
  function hookStates(deps: CommandDeps): ReadinessState[] {
767
854
  const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform)
@@ -863,23 +950,21 @@ function checkTitle(name: string): string {
863
950
  return CHECK_TITLES[name] ?? name
864
951
  }
865
952
 
866
- /** Real but not in the way; see the note above. */
867
- const informational = new Set<string>()
868
953
  return [
869
954
  ...hookChecks(deps).map((check) => ({
870
955
  id: check.name.replace(/[ ()]+/g, '-').replace(/-$/, ''),
871
956
  title: checkTitle(check.name),
872
957
  status: check.ok
873
- ? 'ready' as const
874
- : check.informational === true || informational.has(check.name)
875
- ? 'optional-gap' as const
876
- : 'gap' as const,
958
+ ? ('ready' as const)
959
+ : check.reportOnly === true
960
+ ? ('optional-gap' as const)
961
+ : ('gap' as const),
877
962
  detail: check.detail,
963
+ ...(check.technical === undefined ? {} : { technical: check.technical }),
878
964
  ...(check.ok
879
965
  ? {}
880
966
  : {
881
967
  remedy: {
882
- by: 'user-here' as const,
883
968
  // The check's own remedy when it has one; the generic reinstall
884
969
  // line was wrong exactly where it mattered (an unfired pointer
885
970
  // needs a prompt, not `hooks install`).
@@ -887,6 +972,7 @@ function checkTitle(name: string): string {
887
972
  summary: 'the detail above names what to change',
888
973
  command: 'notifai hooks install',
889
974
  }),
975
+ by: check.remedy?.by ?? ('user-here' as const),
890
976
  },
891
977
  }),
892
978
  })),
@@ -896,13 +982,15 @@ function checkTitle(name: string): string {
896
982
 
897
983
 
898
984
  interface HookCheck {
985
+ /** No command closes this; only a turn that has not happened yet. */
986
+ reportOnly?: boolean
899
987
  name: string
900
988
  ok: boolean
901
989
  detail: string
902
- /** Real but not in the way: worth a line, never a blocker. */
903
- informational?: boolean
990
+ technical?: unknown
904
991
  /** A remedy truer than the generic `notifai hooks install`. */
905
992
  remedy?: {
993
+ by?: 'cli' | 'user-here'
906
994
  summary: string
907
995
  command: string
908
996
  user_action?: { code: string; harness: string; action: string; message: string }
@@ -937,7 +1025,7 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
937
1025
  checks.push({
938
1026
  name: 'hooks (detected)',
939
1027
  ok: false,
940
- informational: true,
1028
+ reportOnly: true,
941
1029
  detail: `${unwired.map((harness) => HARNESS_LABELS[harness]).join(', ')} detected on this machine but not wired`,
942
1030
  remedy: {
943
1031
  summary: 'install hooks for every detected harness',
@@ -959,14 +1047,14 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
959
1047
  checks.push({
960
1048
  name: 'hooks (active harness)',
961
1049
  ok: false,
962
- informational: true,
1050
+ reportOnly: true,
963
1051
  detail: `Several harness sessions could own this shell (${contested.map((candidate) => candidate.label).join(', ')}); routing readiness is intentionally not attributed to either one`,
964
1052
  })
965
1053
  } else if (active !== null && !isHookInstallableHarness(active.harness)) {
966
1054
  checks.push({
967
1055
  name: 'hooks (active harness)',
968
1056
  ok: true,
969
- informational: true,
1057
+ reportOnly: true,
970
1058
  detail: `${active.label}: ${HERMES_QUESTION_ROUTING_UNAVAILABLE.deliveryContract}`,
971
1059
  })
972
1060
  } else if (active !== null) {
@@ -999,7 +1087,7 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
999
1087
  checks.push({
1000
1088
  name: 'hooks (active session)',
1001
1089
  ok: false,
1002
- informational: true,
1090
+ reportOnly: true,
1003
1091
  detail: `active ${active.label} session has not published exact lifecycle state — send one ${active.label} prompt, then check again`,
1004
1092
  remedy: {
1005
1093
  summary: `send one ${active.label} prompt — its hook publishes the routing pointer`,
@@ -1052,14 +1140,47 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
1052
1140
  )
1053
1141
  const sharedAdapter = inspectHookAdapter(deps.hookAdapterHome, deps.hookPlatform)
1054
1142
  adapterProblems.push(...sharedAdapter.problems)
1143
+ const runningTarget = deps.hookInstallTarget
1144
+ const runningArtifact =
1145
+ runningTarget !== undefined && !isNpxAdapterTarget(runningTarget)
1146
+ ? runningTarget.scriptPath
1147
+ : process.argv[1]
1148
+ const cliInstallations = inspectCliInstallations(
1149
+ deps.env,
1150
+ deps.hookPlatform ?? process.platform,
1151
+ {
1152
+ ...(runningArtifact === undefined ? {} : { runningArtifactPath: runningArtifact }),
1153
+ currentVersion: packageVersion(),
1154
+ },
1155
+ )
1156
+ const effectiveArtifact = cliInstallations.effective?.artifact_path ?? null
1157
+ const adapterMismatch =
1158
+ sharedAdapter.problems.length === 0 &&
1159
+ effectiveArtifact !== null &&
1160
+ !hookAdapterTargetsArtifact(sharedAdapter.target, effectiveArtifact)
1161
+ if (adapterMismatch) {
1162
+ adapterProblems.push('the registered CLI does not match the effective notifai command')
1163
+ }
1055
1164
  if (adapterProblems.length > 0) {
1056
1165
  checks.push({
1057
1166
  name: 'hooks (adapter)',
1058
1167
  ok: false,
1059
1168
  // A machine-global install for a harness that is not active in this
1060
- // project is useful diagnosis, but it must not block unrelated init.
1061
- informational: active === null && installations.every((installation) => installation.global),
1169
+ // project is useful diagnosis, but it is not this project's failure.
1170
+ reportOnly: active === null && installations.every((installation) => installation.global),
1062
1171
  detail: adapterProblems.join('; '),
1172
+ ...(adapterMismatch
1173
+ ? { technical: { effective_cli_artifact: effectiveArtifact, registered_target: sharedAdapter.target } }
1174
+ : {}),
1175
+ ...(adapterMismatch && adapterProblems.length === 1
1176
+ ? {
1177
+ remedy: {
1178
+ by: 'cli' as const,
1179
+ summary: 'retarget Question Routing to the effective notifai command',
1180
+ command: 'notifai init',
1181
+ },
1182
+ }
1183
+ : {}),
1063
1184
  })
1064
1185
  }
1065
1186
 
@@ -1215,9 +1336,10 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
1215
1336
  checks.push({
1216
1337
  name: 'hooks (fired)',
1217
1338
  ok: fired,
1218
- // A wholly fresh install is informational. Once UserPromptSubmit has fired,
1219
- // a missing Stop is a broken route, not missing historical evidence.
1220
- informational: firedPointer === null,
1339
+ // Nothing here is a command. Whether the pointer is absent or the turn is
1340
+ // half-finished, the only thing that closes this is a turn ending — which
1341
+ // an agent running `init` inside that very turn cannot supply.
1342
+ reportOnly: true,
1221
1343
  detail: fired
1222
1344
  ? active === null
1223
1345
  ? 'a session in this directory has run UserPromptSubmit and Stop'
@@ -1258,7 +1380,7 @@ function hookChecks(deps: CommandDeps): HookCheck[] {
1258
1380
  continuationHarnesses.every(
1259
1381
  (harness) => HARNESS_CAPABILITIES[harness].stopContinuation !== 'unsupported',
1260
1382
  ),
1261
- informational: active === null,
1383
+ reportOnly: active === null,
1262
1384
  detail:
1263
1385
  continuationHarnesses.length === 0
1264
1386
  ? active === null
@@ -1315,7 +1437,7 @@ function wakeRouteCheck(
1315
1437
  return {
1316
1438
  name: 'hooks (wake route)',
1317
1439
  ok: readiness.state === 'ready',
1318
- informational: true,
1440
+ reportOnly: true,
1319
1441
  detail:
1320
1442
  readiness.state === 'ready'
1321
1443
  ? `this Claude Code ${readiness.version} session is listening on ${readiness.socketPath}, so an answer can start a turn here without you`
@@ -1330,7 +1452,7 @@ function wakeRouteCheck(
1330
1452
  return {
1331
1453
  name: 'hooks (wake route)',
1332
1454
  ok: readiness.state === 'ready',
1333
- informational: true,
1455
+ reportOnly: true,
1334
1456
  detail:
1335
1457
  readiness.state === 'ready'
1336
1458
  ? `the held Codex turn continues from its own hook, and after it returns ${readiness.lockDirectory} can prove a stopped thread unowned before resuming it`
@@ -51,6 +51,14 @@ function harnessEnvCandidates(env: NodeJS.ProcessEnv): ActiveHarnessSession[] {
51
51
  ...(sessionLabelPending ? { sessionLabelPending: true } : {}),
52
52
  })
53
53
  }
54
+ if (env['NOTIFAI_ACTIVE_HARNESS'] === 'openclaw') {
55
+ const sessionId = env['NOTIFAI_ACTIVE_SESSION_ID']
56
+ candidates.push({
57
+ harness: 'openclaw',
58
+ label: 'OpenClaw',
59
+ ...(sessionId === undefined || sessionId === '' ? {} : { sessionId }),
60
+ })
61
+ }
54
62
  if (env['CLAUDECODE'] === '1') {
55
63
  const sessionId = env['CLAUDE_CODE_SESSION_ID']
56
64
  candidates.push({