@raidiant/notifai 10.1.0 → 10.1.2

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 (204) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +3 -0
  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 +29 -11
  17. package/dist/commands-auth.js.map +1 -1
  18. package/dist/commands-core.d.ts +9 -2
  19. package/dist/commands-core.js +11 -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 +93 -49
  25. package/dist/commands-doctor.js.map +1 -1
  26. package/dist/commands-hooks.js +47 -10
  27. package/dist/commands-hooks.js.map +1 -1
  28. package/dist/commands-init.d.ts +16 -0
  29. package/dist/commands-init.js +254 -93
  30. package/dist/commands-init.js.map +1 -1
  31. package/dist/commands-send-support.js +3 -0
  32. package/dist/commands-send-support.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-skill.js +72 -54
  37. package/dist/commands-skill.js.map +1 -1
  38. package/dist/commands-update.d.ts +10 -0
  39. package/dist/commands-update.js +141 -0
  40. package/dist/commands-update.js.map +1 -0
  41. package/dist/commands.d.ts +2 -1
  42. package/dist/commands.js +2 -1
  43. package/dist/commands.js.map +1 -1
  44. package/dist/hook-adapter.d.ts +2 -0
  45. package/dist/hook-adapter.js +12 -1
  46. package/dist/hook-adapter.js.map +1 -1
  47. package/dist/hooks.d.ts +43 -2
  48. package/dist/hooks.js +163 -21
  49. package/dist/hooks.js.map +1 -1
  50. package/dist/invocation-context.d.ts +1 -0
  51. package/dist/invocation-context.js +9 -1
  52. package/dist/invocation-context.js.map +1 -1
  53. package/dist/main-run.d.ts +1 -0
  54. package/dist/main-run.js +42 -0
  55. package/dist/main-run.js.map +1 -0
  56. package/dist/main.js +10 -31
  57. package/dist/main.js.map +1 -1
  58. package/dist/native-skills.d.ts +13 -3
  59. package/dist/native-skills.js +51 -12
  60. package/dist/native-skills.js.map +1 -1
  61. package/dist/node-floor.d.ts +19 -0
  62. package/dist/node-floor.js +39 -0
  63. package/dist/node-floor.js.map +1 -0
  64. package/dist/orca-session-title.d.ts +5 -4
  65. package/dist/orca-session-title.js +32 -19
  66. package/dist/orca-session-title.js.map +1 -1
  67. package/dist/platform.d.ts +11 -17
  68. package/dist/platform.js +20 -23
  69. package/dist/platform.js.map +1 -1
  70. package/dist/program.d.ts +2 -1
  71. package/dist/program.js +22 -5
  72. package/dist/program.js.map +1 -1
  73. package/dist/question-settlement-process.d.ts +15 -0
  74. package/dist/question-settlement-process.js +35 -0
  75. package/dist/question-settlement-process.js.map +1 -0
  76. package/dist/readiness.d.ts +25 -1
  77. package/dist/readiness.js +32 -2
  78. package/dist/readiness.js.map +1 -1
  79. package/dist/release.d.ts +5 -5
  80. package/dist/release.js +5 -5
  81. package/dist/setup-destinations.d.ts +23 -0
  82. package/dist/setup-destinations.js +44 -0
  83. package/dist/setup-destinations.js.map +1 -0
  84. package/dist/skill-integrity.d.ts +49 -0
  85. package/dist/skill-integrity.js +154 -0
  86. package/dist/skill-integrity.js.map +1 -0
  87. package/dist/skill-source/manifest.json +16 -0
  88. package/dist/skill-source/notifai/SKILL.md +390 -0
  89. package/dist/skill-source/notifai/references/harness-setup.md +220 -0
  90. package/package.json +11 -14
  91. package/src/atomic-file.ts +0 -198
  92. package/src/claude-wake.test.ts +0 -321
  93. package/src/claude-wake.ts +0 -499
  94. package/src/cli-bin.test.ts +0 -38
  95. package/src/cli-bin.ts +0 -67
  96. package/src/cli-release.test.ts +0 -46
  97. package/src/cli-release.ts +0 -78
  98. package/src/client.test.ts +0 -390
  99. package/src/client.ts +0 -345
  100. package/src/codex-wake.test.ts +0 -254
  101. package/src/codex-wake.ts +0 -297
  102. package/src/command-session.ts +0 -38
  103. package/src/commands-agent-sessions.ts +0 -72
  104. package/src/commands-auth.ts +0 -205
  105. package/src/commands-config.ts +0 -336
  106. package/src/commands-core.ts +0 -298
  107. package/src/commands-devices.test.ts +0 -15
  108. package/src/commands-devices.ts +0 -173
  109. package/src/commands-doctor.ts +0 -1440
  110. package/src/commands-guidance.ts +0 -181
  111. package/src/commands-harness-context.ts +0 -207
  112. package/src/commands-hook-shape.ts +0 -69
  113. package/src/commands-hooks.ts +0 -2268
  114. package/src/commands-init.ts +0 -878
  115. package/src/commands-io.ts +0 -108
  116. package/src/commands-logs.ts +0 -243
  117. package/src/commands-project.ts +0 -47
  118. package/src/commands-send-support.test.ts +0 -156
  119. package/src/commands-send-support.ts +0 -311
  120. package/src/commands-send.ts +0 -769
  121. package/src/commands-setup-proof.test.ts +0 -72
  122. package/src/commands-setup-proof.ts +0 -124
  123. package/src/commands-skill.test.ts +0 -44
  124. package/src/commands-skill.ts +0 -204
  125. package/src/commands-sounds.test.ts +0 -106
  126. package/src/commands-sounds.ts +0 -49
  127. package/src/commands.test.ts +0 -7828
  128. package/src/commands.ts +0 -75
  129. package/src/config-schema.test.ts +0 -153
  130. package/src/config-schema.ts +0 -349
  131. package/src/config.test.ts +0 -581
  132. package/src/config.ts +0 -475
  133. package/src/credentials.test.ts +0 -403
  134. package/src/credentials.ts +0 -403
  135. package/src/file-lock-process-worker.test.ts +0 -64
  136. package/src/file-lock.test.ts +0 -320
  137. package/src/file-lock.ts +0 -393
  138. package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
  139. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
  140. package/src/generated-session-label.ts +0 -155
  141. package/src/guidance-content.ts +0 -278
  142. package/src/guidance-render.ts +0 -56
  143. package/src/guidance.test.ts +0 -327
  144. package/src/guidance.ts +0 -233
  145. package/src/harness-integration.test.ts +0 -294
  146. package/src/harnesses.test.ts +0 -149
  147. package/src/harnesses.ts +0 -164
  148. package/src/hook-adapter.test.ts +0 -496
  149. package/src/hook-adapter.ts +0 -410
  150. package/src/hook-input.test.ts +0 -39
  151. package/src/hook-input.ts +0 -72
  152. package/src/hooks.test.ts +0 -5549
  153. package/src/hooks.ts +0 -3759
  154. package/src/install-hooks.test.ts +0 -1614
  155. package/src/install-hooks.ts +0 -1501
  156. package/src/interactive.test.ts +0 -387
  157. package/src/interactive.ts +0 -858
  158. package/src/invocation-context.test.ts +0 -352
  159. package/src/invocation-context.ts +0 -197
  160. package/src/logging-process-worker.test.ts +0 -45
  161. package/src/logging.test.ts +0 -544
  162. package/src/logging.ts +0 -698
  163. package/src/main.ts +0 -36
  164. package/src/native-skills.test.ts +0 -115
  165. package/src/native-skills.ts +0 -162
  166. package/src/onboarding-docs.test.ts +0 -25
  167. package/src/openclaw-plugin.ts +0 -409
  168. package/src/opencode-plugin.ts +0 -298
  169. package/src/orca-session-title.test.ts +0 -54
  170. package/src/orca-session-title.ts +0 -103
  171. package/src/pack-feedback-slice.test.ts +0 -212
  172. package/src/pack-feedback-slice.ts +0 -187
  173. package/src/platform.test.ts +0 -152
  174. package/src/platform.ts +0 -173
  175. package/src/program.test.ts +0 -282
  176. package/src/program.ts +0 -818
  177. package/src/project-enablement.test.ts +0 -55
  178. package/src/project-enablement.ts +0 -99
  179. package/src/question-caps.test.ts +0 -49
  180. package/src/question-timing.ts +0 -56
  181. package/src/readiness.test.ts +0 -25
  182. package/src/readiness.ts +0 -179
  183. package/src/release.ts +0 -51
  184. package/src/send-attempts.test.ts +0 -143
  185. package/src/send-attempts.ts +0 -188
  186. package/src/send.test.ts +0 -28
  187. package/src/send.ts +0 -436
  188. package/src/session-activation.ts +0 -61
  189. package/src/session-end-log-lock-worker.test.ts +0 -35
  190. package/src/session-labels.test.ts +0 -490
  191. package/src/session-labels.ts +0 -474
  192. package/src/skill.test.ts +0 -415
  193. package/src/sound-ref.test.ts +0 -31
  194. package/src/sound-ref.ts +0 -34
  195. package/src/trust-policy.test.ts +0 -701
  196. package/src/ui/banner.ts +0 -95
  197. package/src/ui/config-view.test.ts +0 -90
  198. package/src/ui/config-view.ts +0 -138
  199. package/src/ui/help.test.ts +0 -16
  200. package/src/ui/help.ts +0 -128
  201. package/src/ui/theme.test.ts +0 -92
  202. package/src/ui/theme.ts +0 -265
  203. package/src/url-policy.ts +0 -399
  204. package/tsconfig.json +0 -10
package/src/skill.test.ts DELETED
@@ -1,415 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import { describe, expect, it } from 'vitest'
3
- import { NOTIFICATION_KINDS } from '@raidiant/notifai-protocol'
4
- import { GATE_REASONS } from './hooks.js'
5
-
6
- /**
7
- * These tests assert what the skill has to teach, not the sentences it teaches
8
- * it in — as far as that is achievable. Pinning wording makes every
9
- * improvement to the guidance look like a regression, which is how guidance
10
- * goes stale: the cheapest way to keep a suite green becomes leaving the prose
11
- * alone.
12
- *
13
- * What is genuinely structural: section order, the vocabularies the CLI can
14
- * actually emit (`NOTIFICATION_KINDS`, `GATE_REASONS`), the token budget, that
15
- * every example would pass the CLI's own validation, and that every command an
16
- * agent needs is reachable.
17
- *
18
- * Some assertions do match on phrases, and honestly so: a rule like "never say
19
- * where the answer must arrive" has no structural signature, and the cost of
20
- * it silently disappearing is higher than the cost of rewording a regex. Those
21
- * are matched case-insensitively and loosely on purpose.
22
- */
23
-
24
- const skillPath = new URL('../../../skills/notifai/SKILL.md', import.meta.url)
25
- const skill = readFileSync(skillPath, 'utf8')
26
- const description = skill.match(/^description:\s*(.+)$/m)?.[1] ?? ''
27
- const harnessReference = readFileSync(
28
- new URL('../../../skills/notifai/references/harness-setup.md', import.meta.url),
29
- 'utf8',
30
- )
31
-
32
- /** Fenced blocks, so examples can be checked as commands rather than as text. */
33
- const examples = [...skill.matchAll(/```(?:bash|json)?\n([\s\S]*?)```/g)].map((match) => match[1]!)
34
- const shellExamples = examples.flatMap((block) =>
35
- block
36
- .replace(/\\\n\s*/g, ' ')
37
- .split('\n')
38
- .map((line) => line.trim())
39
- .filter((line) => line.startsWith('notifai ')),
40
- )
41
-
42
- const section = (heading: string): string => {
43
- const start = skill.indexOf(heading)
44
- if (start < 0) return ''
45
- const next = skill.indexOf('\n## ', start + heading.length)
46
- return skill.slice(start, next < 0 ? undefined : next)
47
- }
48
-
49
- describe('Notifai agent skill', () => {
50
- it('routes proactively in owner sessions without turning ordinary workers into senders', () => {
51
- // The frontmatter description is the only skill text available during
52
- // automatic selection. It must select parent owners and workers given
53
- // explicit delegation without selecting every ordinary worker.
54
- expect(description).toMatch(/every agent session that owns User-visible Notification Requests/i)
55
- expect(description).toMatch(/parent owns by default/i)
56
- expect(description).toMatch(/worker only when ownership is explicitly delegated/i)
57
- expect(description).toMatch(/even when the user does not mention (?:Notifai|it)/i)
58
- expect(description).toMatch(/guidance/i)
59
-
60
- const decide = section('## Decide whether to notify')
61
- expect(decide).toMatch(/owner session lifecycle context normally includes.*effective\s+guidance/is)
62
- expect(decide).toMatch(/context is absent.*read it once before judging an Agent\s+Event/is)
63
- expect(decide).toMatch(/parent owns by default/i)
64
- expect(decide).toMatch(/explicit textual\s+delegation makes a worker the owner/i)
65
- expect(decide).toMatch(/ordinary workers[\s\S]{0,100}do not load or send/i)
66
- })
67
-
68
- it('preserves the original Agent Event across setup and ambiguous delivery', () => {
69
- expect(skill).toMatch(
70
- /interrupted or killed `send`[\s\S]*do not correlate redacted logs by title[\s\S]*re-run the exact semantic send with\s+`--retry`/i,
71
- )
72
- expect(skill).toMatch(/opaque matching attempt[\s\S]*refuse ambiguity/i)
73
- expect(skill).toMatch(/`no_active_devices`[\s\S]*repeat the\s+(?:exact\s+)?original send/i)
74
- expect(skill).toMatch(/verification Notification[\s\S]*does not deliver\s+the original Agent Event/i)
75
- })
76
-
77
- it('fits inside the budget a skill is actually read within', () => {
78
- // Long skills are truncated in exactly the long sessions that need them.
79
- // The old skill sat at ~4,800 tokens; this ceiling keeps a working margin.
80
- const approximateTokens = skill.length / 4
81
- expect(approximateTokens).toBeLessThan(4_400)
82
- })
83
-
84
- it('decides whether to notify before it composes anything', () => {
85
- const decide = skill.search(/^## .*notify/im)
86
- const compose = skill.search(/^## Send/im)
87
- expect(decide).toBeGreaterThan(0)
88
- expect(decide).toBeLessThan(compose)
89
- })
90
-
91
- it('keeps session instructions out of config and agent paraphrase out of guidance', () => {
92
- // The rule this pins: a conversational instruction is followed, not
93
- // persisted, and persisted guidance holds the user's words verbatim. An
94
- // agent once turned "guide me when you need me" into a persisted standing
95
- // instruction in its own wording, silently via --yes.
96
- const decide = section('## Decide whether to notify')
97
- expect(decide).toMatch(/never touches config/i)
98
- expect(decide).toMatch(/words\s+verbatim/i)
99
- expect(decide).toMatch(/paraphrase must never masquerade/i)
100
- expect(decide).toMatch(/`--yes` skips the\s+CLI's confirmation/i)
101
- })
102
-
103
- it('sends the agent to the resolved guidance before it writes anything', () => {
104
- // The writing guidance itself lives in `notifai guidance` (shipped topics,
105
- // user-overridable per layer); the skill's job is to make reading it the
106
- // first step and user layers authoritative.
107
- const decide = section('## Decide whether to notify')
108
- expect(decide).toContain('notifai guidance')
109
- for (const topic of ['when-to-notify', 'titles', 'bodies', 'questions', 'acknowledgements']) {
110
- expect(decide).toContain(`\`${topic}\``)
111
- }
112
- const authorityOrder = ['from=you', 'from=this repository', 'from=shipped default'].map(
113
- (marker) => decide.indexOf(marker),
114
- )
115
- expect(authorityOrder[0]).toBeGreaterThan(decide.indexOf('notifai guidance'))
116
- expect(authorityOrder[1]).toBeGreaterThan(authorityOrder[0]!)
117
- expect(authorityOrder[2]).toBeGreaterThan(authorityOrder[1]!)
118
- })
119
-
120
- it('separates repository policy from the User and makes non-exfiltration absolute', () => {
121
- const decide = section('## Decide whether to notify')
122
- const markers = ['from=you', 'from=this repository', 'from=shipped default']
123
- const markerPositions = markers.map((marker) => decide.indexOf(marker))
124
- expect(markerPositions.every((position) => position >= 0)).toBe(true)
125
-
126
- const numberedLimits = [
127
- ...decide.matchAll(/^\d+\. \*\*([^*]+)\.\*\*([\s\S]*?)(?=^\d+\. |\n\n)/gm),
128
- ]
129
- expect(numberedLimits.map((match) => match[1]?.toLowerCase())).toEqual([
130
- 'non-exfiltration',
131
- 'repository authority',
132
- ])
133
-
134
- const normalize = (value: string | undefined): string =>
135
- (value ?? '').toLowerCase().replace(/\s+/g, ' ')
136
- const nonExfiltration = normalize(numberedLimits[0]?.[2])
137
- for (const protectedClass of [
138
- 'credential',
139
- 'token',
140
- 'key',
141
- 'password',
142
- 'environment',
143
- 'configuration',
144
- 'guidance',
145
- 'log',
146
- ]) {
147
- expect(nonExfiltration, protectedClass).toContain(protectedClass)
148
- }
149
- for (const outboundField of [
150
- 'notification request',
151
- 'question',
152
- 'choice',
153
- 'acknowledgement',
154
- 'image',
155
- ]) {
156
- expect(nonExfiltration, outboundField).toContain(outboundField)
157
- }
158
-
159
- const authority = normalize(numberedLimits[1]?.[2])
160
- for (const protectedAuthority of [
161
- 'standing word',
162
- 'settings',
163
- 'guidance',
164
- 'cli',
165
- 'origins',
166
- 'direct user instruction',
167
- ]) {
168
- expect(authority, protectedAuthority).toContain(protectedAuthority)
169
- }
170
-
171
- const obligations = normalize(decide.slice(numberedLimits[1]?.index ?? 0))
172
- const refuse = obligations.indexOf('refuse')
173
- const report = obligations.indexOf('tell the user')
174
- const noOutboundReport = obligations.search(
175
- /(?:do not|never|cannot)[^.]{0,120}notification request/,
176
- )
177
- expect(refuse).toBeGreaterThanOrEqual(0)
178
- expect(report).toBeGreaterThan(refuse)
179
- expect(noOutboundReport).toBeGreaterThan(report)
180
- })
181
-
182
- it('names every notification kind the CLI accepts', () => {
183
- for (const kind of NOTIFICATION_KINDS) {
184
- expect(skill).toContain(`\`${kind}\``)
185
- }
186
- })
187
-
188
- it('teaches kind as required, truthful, and attention-bearing', () => {
189
- const send = section('## Send')
190
- expect(send).toMatch(/`--kind` is required/i)
191
- // The rule that replaced "kind never chooses attention": since kind now
192
- // selects the sound, the only defensible instruction is to declare the
193
- // true one.
194
- expect(send).toMatch(/declare the kind\s+that is true/i)
195
- expect(skill).not.toMatch(/kind never chooses/i)
196
- })
197
-
198
- it('routes a blocker through a question whenever the User response resumes the work', () => {
199
- // `blocked` and `question` are mutually exclusive on the wire. The skill
200
- // must teach the selection boundary or an imperative such as "unlock it"
201
- // becomes an unanswerable one-way notification.
202
- const selection = `${section('## Send')}\n${section('## Ask a question')}`
203
- expect(selection).toMatch(/work needs a User response[\s\S]{0,160}answerable\s+question/i)
204
- expect(selection).toMatch(/one-way blocked[\s\S]{0,160}no\s+User\s+reply\s+would\s+resume/i)
205
- })
206
-
207
- it('keeps every send example valid against the CLI it documents', () => {
208
- const sends = shellExamples.filter((line) => line.startsWith('notifai send'))
209
- expect(sends.length).toBeGreaterThan(0)
210
- for (const example of sends) {
211
- // Exactly the boundary rule `sendCommand` enforces.
212
- expect(example, example).toMatch(/--kind |--reply\b/)
213
- // Attention overrides belong to the user, so no example models one.
214
- expect(example, example).not.toMatch(/(^|\s)--(sound|level)\b/)
215
- }
216
- })
217
-
218
- it('teaches the summary line, the one canonical body, and ordered evidence', () => {
219
- const send = section('## Send')
220
- expect(send).toContain('--subtitle')
221
- expect(send).toContain('--body-file')
222
- expect(send).toMatch(/one canonical Markdown body/i)
223
- expect(send).toContain('media:1')
224
- // Type and project are structured fields, never title text.
225
- expect(send).toMatch(/kind and the project travel as their own\s+fields, never in it/i)
226
- })
227
-
228
- it('keeps initial labels stable and teaches the narrow exact-session rename exception', () => {
229
- const send = section('## Send')
230
- expect(send).toContain('--session-label')
231
- expect(send).toMatch(/2-6 words/i)
232
- expect(send).toMatch(/when the current environment exposes its exact\s+identifier/i)
233
- expect(send).toMatch(/without an exact Agent Session[\s\S]{0,160}usage\s+error/i)
234
- expect(send).toMatch(/omit `--session-label`/i)
235
- expect(send).toMatch(/name the\s+environment supplies wins/i)
236
- expect(send).toMatch(/freezes one semantic name/i)
237
- expect(send).toMatch(/safe to repeat the same `--session-label` on every send and ask/i)
238
- // The one exception to permanence: garbage names are recoverable.
239
- expect(send).toMatch(/generated fallback name[\s\S]*replaced by a later\s+semantic name/i)
240
- expect(send).toMatch(/never pass\s+`--session-id`/i)
241
- expect(send).toMatch(/identifier, hash, or\s+filesystem path/i)
242
- expect(send).toContain('notifai session rename')
243
- expect(send).toMatch(/job changed completely[\s\S]{0,100}current\s+name would now mislead/i)
244
- expect(send).toMatch(/never rename for milestones, ordinary progress/i)
245
- expect(send).toMatch(/accepts no Agent Session id/i)
246
- expect(send).toMatch(/active harness proves the exact current Agent Session/i)
247
- })
248
-
249
- it('never teaches by enumerating environments the CLI can detect itself', () => {
250
- // "What if we had 24 different plugins, are we gonna list them all?" —
251
- // guidance states one rule; the mechanism owns environment detection.
252
- // Harness names may appear only in setup/routing material, where the
253
- // harness IS the subject.
254
- for (const heading of ['## Decide whether to notify', '## Send', '## Ask a question', '## When the answer arrives']) {
255
- expect(section(heading), heading).not.toMatch(/OpenCode|OpenClaw|Orca|Cursor|Codex|Hermes/)
256
- }
257
- })
258
-
259
- it('separates how long the command waits from how long an answer is accepted', () => {
260
- const ask = section('## Ask a question')
261
- expect(ask).toContain('--reply-timeout')
262
- expect(ask).toContain('--reply-window')
263
- expect(ask).toMatch(/exit code 3/i)
264
- })
265
-
266
- it('teaches that closed choices appear after pressing and holding', () => {
267
- const ask = section('## Ask a question')
268
- expect(ask).toMatch(/closed choices appear after pressing and holding the\s+notification/i)
269
- expect(skill).not.toMatch(/choices are buttons on the banner/i)
270
- })
271
-
272
- it('makes registration the middle of the asking turn, not the end', () => {
273
- const ask = section('## Ask a question')
274
- expect(ask).toMatch(/registering is not the end of the turn/i)
275
- // The pre-commitment, and the route-neutrality that makes it survive.
276
- expect(ask).toMatch(/what each answer will make you do/i)
277
- expect(ask).toMatch(/never say where the answer must arrive/i)
278
- expect(ask).toMatch(/tell me here/i)
279
- })
280
-
281
- it('requires an acknowledgement before any resumed work', () => {
282
- const answers = section('## When the answer arrives')
283
- const acknowledge = answers.indexOf('notifai acknowledge')
284
- const resume = answers.search(/then resume/i)
285
- expect(acknowledge).toBeGreaterThanOrEqual(0)
286
- expect(resume).toBeGreaterThan(acknowledge)
287
- expect(answers).toMatch(/acknowledge before you resume/i)
288
- // Unconditional: no "if required" escape hatch survives the decision that
289
- // the user always learns their reply was read.
290
- expect(answers).not.toMatch(/if .{0,24}acknowledgement is required/i)
291
- expect(answers).toMatch(/concrete/i)
292
- })
293
-
294
- it('keeps the relay honest about transport, authorship, and its limits', () => {
295
- const answers = section('## When the answer arrives')
296
- expect(answers).toMatch(/latest reply/i)
297
- expect(answers).toMatch(/another session/i)
298
- expect(answers).toMatch(/permission prompt/i)
299
- expect(answers).toMatch(/without asking them to confirm again/i)
300
- })
301
-
302
- it('recovers an answer it was never handed', () => {
303
- expect(skill).toContain('notifai replies --pending')
304
- })
305
-
306
- it('teaches retirement of a registration that has not been pushed yet', () => {
307
- expect(skill).toContain('notifai close --pending')
308
- expect(skill).toContain('question_id')
309
- expect(skill).toMatch(/answer\s+in the conversation/i)
310
- })
311
-
312
- it('makes the agent the operator and the human only the human', () => {
313
- const setup = section('## Set Notifai up')
314
- expect(setup).toMatch(/never tell the\s+user to run a command you could have run/i)
315
- expect(setup).toMatch(/browser/i)
316
- expect(setup).toMatch(/companion/i)
317
- const decisions = setup.search(/both user decisions/i)
318
- const init = setup.indexOf('notifai init --setup-scope')
319
- expect(decisions).toBeGreaterThan(0)
320
- expect(init).toBeGreaterThan(decisions)
321
- expect(setup).toMatch(/setup scope[\s\S]*this project[\s\S]*every project here/i)
322
- expect(setup).toMatch(/Question Routing[\s\S]*devices[\s\S]*terminal/i)
323
- expect(setup).toContain('<--hooks|--no-hooks>')
324
- expect(setup).not.toMatch(/```bash\s+notifai init --json/m)
325
- expect(setup).toMatch(/do not follow a successful structured init\s+with doctor/i)
326
- expect(setup).toContain('--setup-scope')
327
- })
328
-
329
- it('offers npx only as a pinned fallback behind a real install', () => {
330
- const global = skill.indexOf('npm install -g @raidiant/notifai')
331
- const npx = skill.indexOf('npx --yes @raidiant/notifai@')
332
- expect(global).toBeGreaterThan(0)
333
- expect(global).toBeLessThan(npx)
334
- expect(skill).toMatch(/never as the first suggestion/i)
335
- })
336
-
337
- it('gives the agent an exit-status branch for every documented outcome', () => {
338
- for (const code of ['| 0 |', '| 1 |', '| 2 |', '| 3 |', '| 4 |', '| 5 |']) {
339
- expect(skill).toContain(code)
340
- }
341
- expect(skill).toMatch(/`notifai <command> --help` is the authoritative list/i)
342
- })
343
-
344
- it('points at the record when something did not happen, and calls it private', () => {
345
- const check = section('## Check what happened')
346
- expect(check).toContain('notifai logs')
347
- expect(check).toContain('hook.gate')
348
- expect(check).toMatch(/never leaves the machine/i)
349
- })
350
-
351
- it('advertises exactly the gate reasons the code can emit', () => {
352
- // The drift this replaces: the skill advertised `already-asked` and
353
- // `no-devices` for months, neither of which was ever emitted, while
354
- // `answered`, `acknowledgement-required` and `proceeding` went undocumented.
355
- // Reading the vocabulary from the code makes that impossible to repeat.
356
- const check = section('## Check what happened')
357
- const documented = [...check.matchAll(/`([a-z]+(?:-[a-z]+)*)`/g)].map((m) => m[1]!)
358
- const advertised = new Set(documented.filter((word) => GATE_REASONS.includes(word as never)))
359
- for (const reason of GATE_REASONS) {
360
- // `elapsed` is debug-only bookkeeping, not a routing verdict an agent acts on.
361
- if (reason === 'elapsed') continue
362
- expect(advertised, `gate reason ${reason} is not documented`).toContain(reason)
363
- }
364
- for (const word of documented) {
365
- if (!word.includes('-')) continue
366
- const looksLikeAReason = /^(no|already|continuation|delivery|claimed|notifications|acknowledgement)-/.test(word)
367
- if (!looksLikeAReason) continue
368
- expect(GATE_REASONS, `the skill advertises "${word}", which nothing emits`).toContain(word)
369
- }
370
- })
371
-
372
- it('distinguishes acceptance, receipt, and unknown when reporting delivery', () => {
373
- const check = section('## Check what happened')
374
- expect(check).toMatch(/provider acceptance/i)
375
- expect(check).toMatch(/companion receipt/i)
376
- expect(check).toMatch(/unknown/i)
377
- })
378
-
379
- it('covers every command an agent needs to reach the user and recover', () => {
380
- for (const command of [
381
- 'notifai send',
382
- 'notifai ask',
383
- 'notifai replies',
384
- 'notifai acknowledge',
385
- 'notifai close',
386
- 'notifai init',
387
- 'notifai config show',
388
- 'notifai guidance',
389
- 'notifai logs',
390
- 'notifai status',
391
- 'notifai sounds',
392
- ]) {
393
- expect(skill, `${command} is unreachable from the skill`).toContain(command)
394
- }
395
- })
396
-
397
- it('moves per-harness mechanics into progressive disclosure', () => {
398
- expect(skill).toContain('[Harness setup and recovery](references/harness-setup.md)')
399
- expect(skill).toMatch(/read it when you are\s+installing hooks or diagnosing routing/i)
400
- for (const harness of ['Claude Code', 'Codex', 'Cursor', 'OpenCode', 'OpenClaw', 'Hermes']) {
401
- expect(harnessReference).toContain(`**${harness}:**`)
402
- }
403
- expect(harnessReference).toMatch(/exact `HERMES_SESSION_ID`/i)
404
- expect(harnessReference).toMatch(/Do not run `notifai hooks install --harness hermes`/i)
405
- expect(harnessReference).toMatch(/blocking `notifai send --reply`/i)
406
- expect(harnessReference).toMatch(/npx skills/i)
407
- expect(harnessReference).toMatch(/no Hermes plugin may vendor/i)
408
- // The agent runs what a process can run; the human only does the human part.
409
- expect(harnessReference).toMatch(/let init install and assess\s+the hooks/i)
410
- expect(harnessReference).toContain('--setup-scope <project|global>')
411
- expect(harnessReference).not.toContain('--setup-scope project')
412
- expect(harnessReference).toMatch(/run `notifai login` yourself/i)
413
- expect(harnessReference).toMatch(/only the user can approve/i)
414
- })
415
- })
@@ -1,31 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { SOUND_NAME_MAX_LENGTH, SOUND_REF_MAX_LENGTH } from '@raidiant/notifai-protocol'
3
- import { isCliSoundRef, unknownSoundMessage } from './sound-ref.js'
4
-
5
- describe('isCliSoundRef', () => {
6
- it('accepts shipped names and the silent spelling', () => {
7
- for (const ref of ['default', 'done', 'attention', 'alert', 'none']) {
8
- expect(isCliSoundRef(ref), ref).toBe(true)
9
- }
10
- })
11
-
12
- it('accepts an Account custom sound id and a display name', () => {
13
- expect(isCliSoundRef('snd_01HQTEST')).toBe(true)
14
- expect(isCliSoundRef('Kitchen timer')).toBe(true)
15
- expect(isCliSoundRef('airhorn')).toBe(true)
16
- })
17
-
18
- it('rejects empty, blank, and over-long values', () => {
19
- expect(isCliSoundRef('')).toBe(false)
20
- expect(isCliSoundRef(' ')).toBe(false)
21
- expect(isCliSoundRef('n'.repeat(SOUND_NAME_MAX_LENGTH + 1))).toBe(false)
22
- expect(isCliSoundRef(`snd_${'a'.repeat(SOUND_REF_MAX_LENGTH)}`)).toBe(false)
23
- })
24
- })
25
-
26
- describe('unknownSoundMessage', () => {
27
- it('points at the list command rather than only the shipped set', () => {
28
- expect(unknownSoundMessage('nope')).toContain('notifai sounds')
29
- expect(unknownSoundMessage('nope')).toContain('custom sound')
30
- })
31
- })
package/src/sound-ref.ts DELETED
@@ -1,34 +0,0 @@
1
- import {
2
- CLI_SOUNDS,
3
- SOUND_NAME_MAX_LENGTH,
4
- SOUND_REF_MAX_LENGTH,
5
- isCustomSoundId,
6
- } from '@raidiant/notifai-protocol'
7
-
8
- /**
9
- * What `--sound` and the saved `sound` key accept: shipped semantic names,
10
- * the CLI silent spelling, an Account custom sound id, or a custom display name.
11
- */
12
- export function isCliSoundRef(value: string): boolean {
13
- if ((CLI_SOUNDS as readonly string[]).includes(value)) return true
14
- if (value.length < 1 || value.length > SOUND_REF_MAX_LENGTH) return false
15
- if (isCustomSoundId(value)) return true
16
- const trimmed = value.trim()
17
- return trimmed.length >= 1 && trimmed.length <= SOUND_NAME_MAX_LENGTH
18
- }
19
-
20
- export function unknownSoundMessage(sound: string): string {
21
- return (
22
- `Unknown sound "${sound}" — use a shipped name (${CLI_SOUNDS.join(', ')}), ` +
23
- 'a custom sound id, or a custom sound name. Run `notifai sounds` to list what this Account can play.'
24
- )
25
- }
26
-
27
- /** Shipped names plus the CLI silent spelling, with User-facing labels. */
28
- export const SHIPPED_SOUND_LISTING = [
29
- { ref: 'default', name: 'Device default' },
30
- { ref: 'done', name: 'completion chime' },
31
- { ref: 'attention', name: 'attention tone' },
32
- { ref: 'alert', name: 'most insistent tone' },
33
- { ref: 'none', name: 'silent' },
34
- ] as const