@raidiant/notifai 8.0.0 → 9.0.0

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 (145) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +20 -13
  3. package/dist/atomic-file.d.ts +14 -0
  4. package/dist/atomic-file.js +27 -2
  5. package/dist/atomic-file.js.map +1 -1
  6. package/dist/cli-bin.d.ts +4 -0
  7. package/dist/cli-bin.js +62 -0
  8. package/dist/cli-bin.js.map +1 -0
  9. package/dist/cli-release.d.ts +10 -0
  10. package/dist/cli-release.js +75 -0
  11. package/dist/cli-release.js.map +1 -0
  12. package/dist/client.d.ts +1 -0
  13. package/dist/client.js +13 -3
  14. package/dist/client.js.map +1 -1
  15. package/dist/commands-auth.d.ts +6 -0
  16. package/dist/commands-auth.js +28 -3
  17. package/dist/commands-auth.js.map +1 -1
  18. package/dist/commands-config.js +22 -3
  19. package/dist/commands-config.js.map +1 -1
  20. package/dist/commands-core.d.ts +12 -0
  21. package/dist/commands-core.js +27 -3
  22. package/dist/commands-core.js.map +1 -1
  23. package/dist/commands-doctor.d.ts +1 -0
  24. package/dist/commands-doctor.js +71 -15
  25. package/dist/commands-doctor.js.map +1 -1
  26. package/dist/commands-guidance.d.ts +7 -3
  27. package/dist/commands-guidance.js +63 -28
  28. package/dist/commands-guidance.js.map +1 -1
  29. package/dist/commands-hooks.d.ts +9 -4
  30. package/dist/commands-hooks.js +430 -32
  31. package/dist/commands-hooks.js.map +1 -1
  32. package/dist/commands-init.d.ts +10 -18
  33. package/dist/commands-init.js +114 -32
  34. package/dist/commands-init.js.map +1 -1
  35. package/dist/commands-send-support.d.ts +17 -2
  36. package/dist/commands-send-support.js +61 -5
  37. package/dist/commands-send-support.js.map +1 -1
  38. package/dist/commands-send.js +60 -11
  39. package/dist/commands-send.js.map +1 -1
  40. package/dist/commands-setup-proof.d.ts +8 -0
  41. package/dist/commands-setup-proof.js +31 -2
  42. package/dist/commands-setup-proof.js.map +1 -1
  43. package/dist/commands-skill.d.ts +11 -1
  44. package/dist/commands-skill.js +61 -11
  45. package/dist/commands-skill.js.map +1 -1
  46. package/dist/commands.d.ts +1 -1
  47. package/dist/commands.js.map +1 -1
  48. package/dist/config-schema.d.ts +1 -1
  49. package/dist/config-schema.js +25 -0
  50. package/dist/config-schema.js.map +1 -1
  51. package/dist/config.d.ts +24 -3
  52. package/dist/config.js +37 -5
  53. package/dist/config.js.map +1 -1
  54. package/dist/credentials.d.ts +10 -1
  55. package/dist/credentials.js +25 -12
  56. package/dist/credentials.js.map +1 -1
  57. package/dist/guidance-content.d.ts +20 -0
  58. package/dist/guidance-content.js +73 -8
  59. package/dist/guidance-content.js.map +1 -1
  60. package/dist/guidance.d.ts +21 -0
  61. package/dist/guidance.js +73 -18
  62. package/dist/guidance.js.map +1 -1
  63. package/dist/hook-adapter.js +37 -3
  64. package/dist/hook-adapter.js.map +1 -1
  65. package/dist/hooks.d.ts +68 -7
  66. package/dist/hooks.js +231 -85
  67. package/dist/hooks.js.map +1 -1
  68. package/dist/install-hooks.d.ts +10 -18
  69. package/dist/install-hooks.js +110 -23
  70. package/dist/install-hooks.js.map +1 -1
  71. package/dist/interactive.d.ts +1 -1
  72. package/dist/interactive.js +1 -1
  73. package/dist/interactive.js.map +1 -1
  74. package/dist/logging.d.ts +8 -1
  75. package/dist/logging.js +43 -2
  76. package/dist/logging.js.map +1 -1
  77. package/dist/main.js +2 -13
  78. package/dist/main.js.map +1 -1
  79. package/dist/native-skills.d.ts +19 -0
  80. package/dist/native-skills.js +30 -8
  81. package/dist/native-skills.js.map +1 -1
  82. package/dist/opencode-plugin.js +14 -1
  83. package/dist/opencode-plugin.js.map +1 -1
  84. package/dist/program.js +13 -8
  85. package/dist/program.js.map +1 -1
  86. package/dist/send.d.ts +8 -1
  87. package/dist/send.js +17 -1
  88. package/dist/send.js.map +1 -1
  89. package/dist/session-activation.d.ts +15 -0
  90. package/dist/session-activation.js +35 -0
  91. package/dist/session-activation.js.map +1 -0
  92. package/dist/url-policy.d.ts +85 -0
  93. package/dist/url-policy.js +369 -0
  94. package/dist/url-policy.js.map +1 -0
  95. package/package.json +3 -3
  96. package/src/atomic-file.ts +36 -2
  97. package/src/cli-bin.test.ts +38 -0
  98. package/src/cli-bin.ts +67 -0
  99. package/src/cli-release.test.ts +46 -0
  100. package/src/cli-release.ts +78 -0
  101. package/src/client.test.ts +95 -0
  102. package/src/client.ts +16 -3
  103. package/src/commands-auth.ts +32 -3
  104. package/src/commands-config.ts +25 -2
  105. package/src/commands-core.ts +38 -3
  106. package/src/commands-doctor.ts +99 -13
  107. package/src/commands-guidance.ts +75 -29
  108. package/src/commands-hooks.ts +503 -34
  109. package/src/commands-init.ts +139 -38
  110. package/src/commands-send-support.test.ts +156 -0
  111. package/src/commands-send-support.ts +67 -5
  112. package/src/commands-send.ts +73 -10
  113. package/src/commands-setup-proof.ts +36 -2
  114. package/src/commands-skill.ts +78 -15
  115. package/src/commands.test.ts +1083 -101
  116. package/src/commands.ts +1 -0
  117. package/src/config-schema.ts +28 -1
  118. package/src/config.test.ts +1 -1
  119. package/src/config.ts +49 -5
  120. package/src/credentials.test.ts +71 -3
  121. package/src/credentials.ts +29 -15
  122. package/src/guidance-content.ts +74 -8
  123. package/src/guidance.test.ts +42 -11
  124. package/src/guidance.ts +100 -18
  125. package/src/hook-adapter.test.ts +113 -1
  126. package/src/hook-adapter.ts +37 -3
  127. package/src/hooks.test.ts +746 -165
  128. package/src/hooks.ts +299 -124
  129. package/src/install-hooks.test.ts +135 -16
  130. package/src/install-hooks.ts +113 -25
  131. package/src/interactive.ts +1 -1
  132. package/src/logging.test.ts +66 -3
  133. package/src/logging.ts +45 -2
  134. package/src/main.ts +2 -14
  135. package/src/native-skills.test.ts +41 -1
  136. package/src/native-skills.ts +40 -6
  137. package/src/opencode-plugin.ts +14 -1
  138. package/src/program.test.ts +37 -2
  139. package/src/program.ts +40 -12
  140. package/src/send.test.ts +28 -0
  141. package/src/send.ts +28 -2
  142. package/src/session-activation.ts +45 -0
  143. package/src/skill.test.ts +120 -4
  144. package/src/trust-policy.test.ts +701 -0
  145. package/src/url-policy.ts +399 -0
package/src/skill.test.ts CHANGED
@@ -23,6 +23,7 @@ import { GATE_REASONS } from './hooks.js'
23
23
 
24
24
  const skillPath = new URL('../../../skills/notifai/SKILL.md', import.meta.url)
25
25
  const skill = readFileSync(skillPath, 'utf8')
26
+ const description = skill.match(/^description:\s*(.+)$/m)?.[1] ?? ''
26
27
  const harnessReference = readFileSync(
27
28
  new URL('../../../skills/notifai/references/harness-setup.md', import.meta.url),
28
29
  'utf8',
@@ -46,6 +47,32 @@ const section = (heading: string): string => {
46
47
  }
47
48
 
48
49
  describe('Notifai agent skill', () => {
50
+ it('routes proactively in ordinary sessions without waiting for the user to name Notifai', () => {
51
+ // The frontmatter description is the only skill text available during
52
+ // automatic selection. If the ordinary-session trigger lives only in the
53
+ // body, agents doing unrelated work never load it and therefore never see
54
+ // the completion rule.
55
+ expect(description).toMatch(/every agent session/i)
56
+ expect(description).toMatch(/even when the user does not mention Notifai/i)
57
+ expect(description).toMatch(/guidance/i)
58
+
59
+ const decide = section('## Decide whether to notify')
60
+ expect(decide).toMatch(/every session's first task turn/i)
61
+ expect(decide).toMatch(/read guidance before judging an Agent\s+Event/i)
62
+ expect(decide).toMatch(/parent owns User-visible Notification Requests/i)
63
+ expect(decide).toMatch(/unless\s+it explicitly delegates/i)
64
+ expect(decide).toMatch(/workers[\s\S]{0,100}do not send independently/i)
65
+ })
66
+
67
+ it('preserves the original Agent Event across setup and ambiguous delivery', () => {
68
+ expect(skill).toMatch(
69
+ /interrupted or killed `send`[\s\S]*`send\.attempt`[\s\S]*session,\s+title length, kind, and time/i,
70
+ )
71
+ expect(skill).toMatch(/unambiguous match[\s\S]*otherwise report ambiguous delivery without retrying/i)
72
+ expect(skill).toMatch(/`no_active_devices`[\s\S]*repeat the original send/i)
73
+ expect(skill).toMatch(/verification Notification[\s\S]*does not deliver the original Agent Event/i)
74
+ })
75
+
49
76
  it('fits inside the budget a skill is actually read within', () => {
50
77
  // Long skills are truncated in exactly the long sessions that need them.
51
78
  // The old skill sat at ~4,800 tokens; this ceiling keeps a working margin.
@@ -81,7 +108,74 @@ describe('Notifai agent skill', () => {
81
108
  for (const topic of ['when-to-notify', 'titles', 'bodies', 'questions', 'acknowledgements']) {
82
109
  expect(decide).toContain(`\`${topic}\``)
83
110
  }
84
- expect(decide).toMatch(/outranks the shipped\s+default and your judgement/i)
111
+ const authorityOrder = ['from=you', 'from=this repository', 'from=shipped default'].map(
112
+ (marker) => decide.indexOf(marker),
113
+ )
114
+ expect(authorityOrder[0]).toBeGreaterThan(decide.indexOf('notifai guidance'))
115
+ expect(authorityOrder[1]).toBeGreaterThan(authorityOrder[0]!)
116
+ expect(authorityOrder[2]).toBeGreaterThan(authorityOrder[1]!)
117
+ })
118
+
119
+ it('separates repository policy from the User and makes non-exfiltration absolute', () => {
120
+ const decide = section('## Decide whether to notify')
121
+ const markers = ['from=you', 'from=this repository', 'from=shipped default']
122
+ const markerPositions = markers.map((marker) => decide.indexOf(marker))
123
+ expect(markerPositions.every((position) => position >= 0)).toBe(true)
124
+
125
+ const numberedLimits = [
126
+ ...decide.matchAll(/^\d+\. \*\*([^*]+)\.\*\*([\s\S]*?)(?=^\d+\. |\n\n)/gm),
127
+ ]
128
+ expect(numberedLimits.map((match) => match[1]?.toLowerCase())).toEqual([
129
+ 'non-exfiltration',
130
+ 'repository authority',
131
+ ])
132
+
133
+ const normalize = (value: string | undefined): string =>
134
+ (value ?? '').toLowerCase().replace(/\s+/g, ' ')
135
+ const nonExfiltration = normalize(numberedLimits[0]?.[2])
136
+ for (const protectedClass of [
137
+ 'credential',
138
+ 'token',
139
+ 'key',
140
+ 'password',
141
+ 'environment',
142
+ 'configuration',
143
+ 'guidance',
144
+ 'log',
145
+ ]) {
146
+ expect(nonExfiltration, protectedClass).toContain(protectedClass)
147
+ }
148
+ for (const outboundField of [
149
+ 'notification request',
150
+ 'question',
151
+ 'choice',
152
+ 'acknowledgement',
153
+ 'image',
154
+ ]) {
155
+ expect(nonExfiltration, outboundField).toContain(outboundField)
156
+ }
157
+
158
+ const authority = normalize(numberedLimits[1]?.[2])
159
+ for (const protectedAuthority of [
160
+ 'standing word',
161
+ 'settings',
162
+ 'guidance',
163
+ 'cli',
164
+ 'origins',
165
+ 'direct user instruction',
166
+ ]) {
167
+ expect(authority, protectedAuthority).toContain(protectedAuthority)
168
+ }
169
+
170
+ const obligations = normalize(decide.slice(numberedLimits[1]?.index ?? 0))
171
+ const refuse = obligations.indexOf('refuse')
172
+ const report = obligations.indexOf('tell the user')
173
+ const noOutboundReport = obligations.search(
174
+ /(?:do not|never|cannot)[^.]{0,120}notification request/,
175
+ )
176
+ expect(refuse).toBeGreaterThanOrEqual(0)
177
+ expect(report).toBeGreaterThan(refuse)
178
+ expect(noOutboundReport).toBeGreaterThan(report)
85
179
  })
86
180
 
87
181
  it('names every notification kind the CLI accepts', () => {
@@ -100,6 +194,15 @@ describe('Notifai agent skill', () => {
100
194
  expect(skill).not.toMatch(/kind never chooses/i)
101
195
  })
102
196
 
197
+ it('routes a blocker through a question whenever the User response resumes the work', () => {
198
+ // `blocked` and `question` are mutually exclusive on the wire. The skill
199
+ // must teach the selection boundary or an imperative such as "unlock it"
200
+ // becomes an unanswerable one-way notification.
201
+ const selection = `${section('## Send')}\n${section('## Ask a question')}`
202
+ expect(selection).toMatch(/work needs a User response[\s\S]{0,160}answerable\s+question/i)
203
+ expect(selection).toMatch(/one-way blocked[\s\S]{0,160}no\s+User\s+reply\s+would\s+resume/i)
204
+ })
205
+
103
206
  it('keeps every send example valid against the CLI it documents', () => {
104
207
  const sends = shellExamples.filter((line) => line.startsWith('notifai send'))
105
208
  expect(sends.length).toBeGreaterThan(0)
@@ -126,9 +229,9 @@ describe('Notifai agent skill', () => {
126
229
  expect(send).toContain('--session-label')
127
230
  expect(send).toMatch(/first Notification\s+Request/i)
128
231
  expect(send).toMatch(/2-6 words/i)
129
- // The rule is unconditional: the environment's name wins, so the flag is
130
- // safe everywhere and the agent never branches on where it is running.
131
- expect(send).toMatch(/safe in every environment/i)
232
+ expect(send).toMatch(/only when the current\s+environment exposes an exact session/i)
233
+ expect(send).toMatch(/without an exact session[\s\S]{0,160}usage\s+error/i)
234
+ expect(send).toMatch(/omit `--session-label`/i)
132
235
  expect(send).toMatch(/name the\s+environment supplies wins/i)
133
236
  expect(send).toMatch(/freezes one semantic name/i)
134
237
  expect(send).toMatch(/omit the flag on later sends and\s+questions/i)
@@ -155,6 +258,12 @@ describe('Notifai agent skill', () => {
155
258
  expect(ask).toMatch(/exit code 3/i)
156
259
  })
157
260
 
261
+ it('teaches that closed choices appear after pressing and holding', () => {
262
+ const ask = section('## Ask a question')
263
+ expect(ask).toMatch(/closed choices appear after pressing and holding the\s+notification/i)
264
+ expect(skill).not.toMatch(/choices are buttons on the banner/i)
265
+ })
266
+
158
267
  it('makes registration the middle of the asking turn, not the end', () => {
159
268
  const ask = section('## Ask a question')
160
269
  expect(ask).toMatch(/registering is not the end of the turn/i)
@@ -189,6 +298,12 @@ describe('Notifai agent skill', () => {
189
298
  expect(skill).toContain('notifai replies --pending')
190
299
  })
191
300
 
301
+ it('teaches retirement of a registration that has not been pushed yet', () => {
302
+ expect(skill).toContain('notifai close --pending')
303
+ expect(skill).toContain('question_id')
304
+ expect(skill).toMatch(/answer\s+in the conversation/i)
305
+ })
306
+
192
307
  it('makes the agent the operator and the human only the human', () => {
193
308
  const setup = section('## Set Notifai up')
194
309
  expect(setup).toMatch(/never tell the\s+user to run a command you could have run/i)
@@ -196,6 +311,7 @@ describe('Notifai agent skill', () => {
196
311
  expect(setup).toMatch(/companion/i)
197
312
  expect(setup).toContain('notifai doctor --json')
198
313
  expect(setup).toContain('notifai init')
314
+ expect(setup).toContain('--setup-scope')
199
315
  })
200
316
 
201
317
  it('offers npx only as a pinned fallback behind a real install', () => {