@raidiant/notifai 10.1.1 → 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 (156) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/commands-core.d.ts +1 -1
  3. package/dist/commands-init.js +9 -6
  4. package/dist/commands-init.js.map +1 -1
  5. package/dist/commands-send-support.js +3 -0
  6. package/dist/commands-send-support.js.map +1 -1
  7. package/dist/commands-skill.js +72 -54
  8. package/dist/commands-skill.js.map +1 -1
  9. package/dist/invocation-context.d.ts +1 -0
  10. package/dist/invocation-context.js +9 -1
  11. package/dist/invocation-context.js.map +1 -1
  12. package/dist/native-skills.d.ts +13 -3
  13. package/dist/native-skills.js +51 -12
  14. package/dist/native-skills.js.map +1 -1
  15. package/dist/node-floor.d.ts +2 -1
  16. package/dist/node-floor.js +9 -5
  17. package/dist/node-floor.js.map +1 -1
  18. package/dist/orca-session-title.d.ts +5 -4
  19. package/dist/orca-session-title.js +32 -19
  20. package/dist/orca-session-title.js.map +1 -1
  21. package/dist/platform.d.ts +11 -17
  22. package/dist/platform.js +20 -23
  23. package/dist/platform.js.map +1 -1
  24. package/dist/release.d.ts +5 -5
  25. package/dist/release.js +5 -5
  26. package/dist/setup-destinations.js +1 -1
  27. package/dist/setup-destinations.js.map +1 -1
  28. package/dist/skill-integrity.d.ts +49 -0
  29. package/dist/skill-integrity.js +154 -0
  30. package/dist/skill-integrity.js.map +1 -0
  31. package/dist/skill-source/manifest.json +16 -0
  32. package/dist/skill-source/notifai/SKILL.md +390 -0
  33. package/dist/skill-source/notifai/references/harness-setup.md +220 -0
  34. package/package.json +11 -14
  35. package/src/atomic-file.ts +0 -198
  36. package/src/claude-wake.test.ts +0 -321
  37. package/src/claude-wake.ts +0 -499
  38. package/src/cli-bin.test.ts +0 -176
  39. package/src/cli-bin.ts +0 -233
  40. package/src/cli-release.test.ts +0 -46
  41. package/src/cli-release.ts +0 -78
  42. package/src/cli-update-contract.ts +0 -9
  43. package/src/cli-update.test.ts +0 -125
  44. package/src/client.test.ts +0 -390
  45. package/src/client.ts +0 -352
  46. package/src/codex-wake.test.ts +0 -254
  47. package/src/codex-wake.ts +0 -301
  48. package/src/command-session.ts +0 -38
  49. package/src/commands-agent-sessions.ts +0 -72
  50. package/src/commands-auth.ts +0 -238
  51. package/src/commands-config.ts +0 -336
  52. package/src/commands-core.ts +0 -312
  53. package/src/commands-devices.ts +0 -180
  54. package/src/commands-doctor.ts +0 -1501
  55. package/src/commands-guidance.ts +0 -181
  56. package/src/commands-harness-context.ts +0 -207
  57. package/src/commands-hook-shape.ts +0 -69
  58. package/src/commands-hooks.ts +0 -2318
  59. package/src/commands-init.ts +0 -1055
  60. package/src/commands-io.ts +0 -108
  61. package/src/commands-logs.ts +0 -243
  62. package/src/commands-project.ts +0 -47
  63. package/src/commands-send-support.test.ts +0 -156
  64. package/src/commands-send-support.ts +0 -311
  65. package/src/commands-send.ts +0 -875
  66. package/src/commands-setup-proof.test.ts +0 -72
  67. package/src/commands-setup-proof.ts +0 -124
  68. package/src/commands-skill.test.ts +0 -44
  69. package/src/commands-skill.ts +0 -204
  70. package/src/commands-sounds.test.ts +0 -106
  71. package/src/commands-sounds.ts +0 -49
  72. package/src/commands-update.ts +0 -180
  73. package/src/commands.test.ts +0 -8297
  74. package/src/commands.ts +0 -77
  75. package/src/config-schema.test.ts +0 -153
  76. package/src/config-schema.ts +0 -349
  77. package/src/config.test.ts +0 -581
  78. package/src/config.ts +0 -475
  79. package/src/credentials.test.ts +0 -403
  80. package/src/credentials.ts +0 -403
  81. package/src/file-lock-process-worker.test.ts +0 -64
  82. package/src/file-lock.test.ts +0 -320
  83. package/src/file-lock.ts +0 -393
  84. package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
  85. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
  86. package/src/generated-session-label.ts +0 -155
  87. package/src/guidance-content.ts +0 -278
  88. package/src/guidance-render.ts +0 -56
  89. package/src/guidance.test.ts +0 -327
  90. package/src/guidance.ts +0 -233
  91. package/src/harness-integration.test.ts +0 -294
  92. package/src/harnesses.test.ts +0 -149
  93. package/src/harnesses.ts +0 -164
  94. package/src/hook-adapter.test.ts +0 -496
  95. package/src/hook-adapter.ts +0 -424
  96. package/src/hook-input.test.ts +0 -39
  97. package/src/hook-input.ts +0 -72
  98. package/src/hooks.test.ts +0 -5824
  99. package/src/hooks.ts +0 -4011
  100. package/src/install-hooks.test.ts +0 -1614
  101. package/src/install-hooks.ts +0 -1501
  102. package/src/interactive.test.ts +0 -387
  103. package/src/interactive.ts +0 -858
  104. package/src/invocation-context.test.ts +0 -352
  105. package/src/invocation-context.ts +0 -197
  106. package/src/logging-process-worker.test.ts +0 -45
  107. package/src/logging.test.ts +0 -544
  108. package/src/logging.ts +0 -698
  109. package/src/main-run.ts +0 -45
  110. package/src/main.ts +0 -12
  111. package/src/native-skills.test.ts +0 -115
  112. package/src/native-skills.ts +0 -162
  113. package/src/node-floor.test.ts +0 -27
  114. package/src/node-floor.ts +0 -37
  115. package/src/onboarding-docs.test.ts +0 -25
  116. package/src/openclaw-plugin.ts +0 -409
  117. package/src/opencode-plugin.ts +0 -298
  118. package/src/orca-session-title.test.ts +0 -54
  119. package/src/orca-session-title.ts +0 -103
  120. package/src/pack-feedback-slice.test.ts +0 -212
  121. package/src/pack-feedback-slice.ts +0 -187
  122. package/src/platform.test.ts +0 -152
  123. package/src/platform.ts +0 -173
  124. package/src/program.test.ts +0 -317
  125. package/src/program.ts +0 -842
  126. package/src/project-enablement.test.ts +0 -55
  127. package/src/project-enablement.ts +0 -99
  128. package/src/question-caps.test.ts +0 -49
  129. package/src/question-settlement-process.ts +0 -47
  130. package/src/question-timing.ts +0 -56
  131. package/src/readiness.test.ts +0 -25
  132. package/src/readiness.ts +0 -212
  133. package/src/release.ts +0 -51
  134. package/src/send-attempts.test.ts +0 -143
  135. package/src/send-attempts.ts +0 -188
  136. package/src/send.test.ts +0 -28
  137. package/src/send.ts +0 -436
  138. package/src/session-activation.ts +0 -61
  139. package/src/session-end-log-lock-worker.test.ts +0 -35
  140. package/src/session-labels.test.ts +0 -490
  141. package/src/session-labels.ts +0 -474
  142. package/src/setup-destinations.test.ts +0 -41
  143. package/src/setup-destinations.ts +0 -50
  144. package/src/skill.test.ts +0 -426
  145. package/src/sound-ref.test.ts +0 -31
  146. package/src/sound-ref.ts +0 -34
  147. package/src/trust-policy.test.ts +0 -701
  148. package/src/ui/banner.ts +0 -95
  149. package/src/ui/config-view.test.ts +0 -90
  150. package/src/ui/config-view.ts +0 -138
  151. package/src/ui/help.test.ts +0 -16
  152. package/src/ui/help.ts +0 -128
  153. package/src/ui/theme.test.ts +0 -92
  154. package/src/ui/theme.ts +0 -265
  155. package/src/url-policy.ts +0 -399
  156. package/tsconfig.json +0 -10
package/src/skill.test.ts DELETED
@@ -1,426 +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('keeps local registration distinct from submission and delivery evidence', () => {
282
- const ask = section('## Ask a question')
283
- expect(ask).toMatch(/`registered: true`[\s\S]{0,180}local/i)
284
- expect(ask).toMatch(/not yet been submitted as a\s+Notification\s+Request/i)
285
- expect(ask).toMatch(/no Provider Acceptance/i)
286
- expect(ask).toContain('notifai status <question_id> --json')
287
- expect(ask).toMatch(/local.*frozen.*live.*answered.*withdrawn.*retired/is)
288
- expect(ask).toMatch(/never call a question\s+sent\s+or\s+delivered\s+from registration alone/i)
289
- expect(ask).toMatch(/inspect or close the original[\s\S]{0,160}do not register/i)
290
- })
291
-
292
- it('requires an acknowledgement before any resumed work', () => {
293
- const answers = section('## When the answer arrives')
294
- const acknowledge = answers.indexOf('notifai acknowledge')
295
- const resume = answers.search(/then resume/i)
296
- expect(acknowledge).toBeGreaterThanOrEqual(0)
297
- expect(resume).toBeGreaterThan(acknowledge)
298
- expect(answers).toMatch(/acknowledge before you resume/i)
299
- // Unconditional: no "if required" escape hatch survives the decision that
300
- // the user always learns their reply was read.
301
- expect(answers).not.toMatch(/if .{0,24}acknowledgement is required/i)
302
- expect(answers).toMatch(/concrete/i)
303
- })
304
-
305
- it('keeps the relay honest about transport, authorship, and its limits', () => {
306
- const answers = section('## When the answer arrives')
307
- expect(answers).toMatch(/latest reply/i)
308
- expect(answers).toMatch(/another session/i)
309
- expect(answers).toMatch(/permission prompt/i)
310
- expect(answers).toMatch(/without asking them to confirm again/i)
311
- })
312
-
313
- it('recovers an answer it was never handed', () => {
314
- expect(skill).toContain('notifai replies --pending')
315
- })
316
-
317
- it('teaches retirement of a registration that has not been pushed yet', () => {
318
- expect(skill).toContain('notifai close --pending')
319
- expect(skill).toContain('question_id')
320
- expect(skill).toMatch(/answer\s+in the conversation/i)
321
- })
322
-
323
- it('makes the agent the operator and the human only the human', () => {
324
- const setup = section('## Set Notifai up')
325
- expect(setup).toMatch(/never tell the\s+user to run a command you could have run/i)
326
- expect(setup).toMatch(/browser/i)
327
- expect(setup).toMatch(/companion/i)
328
- const decisions = setup.search(/both user decisions/i)
329
- const init = setup.indexOf('notifai init --setup-scope')
330
- expect(decisions).toBeGreaterThan(0)
331
- expect(init).toBeGreaterThan(decisions)
332
- expect(setup).toMatch(/setup scope[\s\S]*this project[\s\S]*every project here/i)
333
- expect(setup).toMatch(/Question Routing[\s\S]*devices[\s\S]*terminal/i)
334
- expect(setup).toContain('<--hooks|--no-hooks>')
335
- expect(setup).not.toMatch(/```bash\s+notifai init --json/m)
336
- expect(setup).toMatch(/do not follow a successful structured init\s+with doctor/i)
337
- expect(setup).toContain('--setup-scope')
338
- })
339
-
340
- it('offers npx only as a pinned fallback behind a real install', () => {
341
- const global = skill.indexOf('npm install -g @raidiant/notifai')
342
- const npx = skill.indexOf('npx --yes @raidiant/notifai@')
343
- expect(global).toBeGreaterThan(0)
344
- expect(global).toBeLessThan(npx)
345
- expect(skill).toMatch(/never as the first suggestion/i)
346
- })
347
-
348
- it('gives the agent an exit-status branch for every documented outcome', () => {
349
- for (const code of ['| 0 |', '| 1 |', '| 2 |', '| 3 |', '| 4 |', '| 5 |']) {
350
- expect(skill).toContain(code)
351
- }
352
- expect(skill).toMatch(/`notifai <command> --help` is the authoritative list/i)
353
- })
354
-
355
- it('points at the record when something did not happen, and calls it private', () => {
356
- const check = section('## Check what happened')
357
- expect(check).toContain('notifai logs')
358
- expect(check).toContain('hook.gate')
359
- expect(check).toMatch(/never leaves the machine/i)
360
- })
361
-
362
- it('advertises exactly the gate reasons the code can emit', () => {
363
- // The drift this replaces: the skill advertised `already-asked` and
364
- // `no-devices` for months, neither of which was ever emitted, while
365
- // `answered`, `acknowledgement-required` and `proceeding` went undocumented.
366
- // Reading the vocabulary from the code makes that impossible to repeat.
367
- const check = section('## Check what happened')
368
- const documented = [...check.matchAll(/`([a-z]+(?:-[a-z]+)*)`/g)].map((m) => m[1]!)
369
- const advertised = new Set(documented.filter((word) => GATE_REASONS.includes(word as never)))
370
- for (const reason of GATE_REASONS) {
371
- // `elapsed` is debug-only bookkeeping, not a routing verdict an agent acts on.
372
- if (reason === 'elapsed') continue
373
- expect(advertised, `gate reason ${reason} is not documented`).toContain(reason)
374
- }
375
- for (const word of documented) {
376
- if (!word.includes('-')) continue
377
- const looksLikeAReason = /^(no|already|continuation|delivery|claimed|notifications|acknowledgement)-/.test(word)
378
- if (!looksLikeAReason) continue
379
- expect(GATE_REASONS, `the skill advertises "${word}", which nothing emits`).toContain(word)
380
- }
381
- })
382
-
383
- it('distinguishes acceptance, receipt, and unknown when reporting delivery', () => {
384
- const check = section('## Check what happened')
385
- expect(check).toMatch(/provider acceptance/i)
386
- expect(check).toMatch(/companion receipt/i)
387
- expect(check).toMatch(/unknown/i)
388
- })
389
-
390
- it('covers every command an agent needs to reach the user and recover', () => {
391
- for (const command of [
392
- 'notifai send',
393
- 'notifai ask',
394
- 'notifai replies',
395
- 'notifai acknowledge',
396
- 'notifai close',
397
- 'notifai init',
398
- 'notifai config show',
399
- 'notifai guidance',
400
- 'notifai logs',
401
- 'notifai status',
402
- 'notifai sounds',
403
- ]) {
404
- expect(skill, `${command} is unreachable from the skill`).toContain(command)
405
- }
406
- })
407
-
408
- it('moves per-harness mechanics into progressive disclosure', () => {
409
- expect(skill).toContain('[Harness setup and recovery](references/harness-setup.md)')
410
- expect(skill).toMatch(/read it when you are\s+installing hooks or diagnosing routing/i)
411
- for (const harness of ['Claude Code', 'Codex', 'Cursor', 'OpenCode', 'OpenClaw', 'Hermes']) {
412
- expect(harnessReference).toContain(`**${harness}:**`)
413
- }
414
- expect(harnessReference).toMatch(/exact `HERMES_SESSION_ID`/i)
415
- expect(harnessReference).toMatch(/Do not run `notifai hooks install --harness hermes`/i)
416
- expect(harnessReference).toMatch(/blocking `notifai send --reply`/i)
417
- expect(harnessReference).toMatch(/npx skills/i)
418
- expect(harnessReference).toMatch(/no Hermes plugin may vendor/i)
419
- // The agent runs what a process can run; the human only does the human part.
420
- expect(harnessReference).toMatch(/let init install and assess\s+the hooks/i)
421
- expect(harnessReference).toContain('--setup-scope <project|global>')
422
- expect(harnessReference).not.toContain('--setup-scope project')
423
- expect(harnessReference).toMatch(/run `notifai login` yourself/i)
424
- expect(harnessReference).toMatch(/only the user can approve/i)
425
- })
426
- })
@@ -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