@raidiant/notifai 7.0.2 → 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 (157) hide show
  1. package/CHANGELOG.md +75 -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-devices.d.ts +2 -1
  24. package/dist/commands-devices.js +20 -8
  25. package/dist/commands-devices.js.map +1 -1
  26. package/dist/commands-doctor.d.ts +1 -0
  27. package/dist/commands-doctor.js +85 -27
  28. package/dist/commands-doctor.js.map +1 -1
  29. package/dist/commands-guidance.d.ts +27 -0
  30. package/dist/commands-guidance.js +163 -0
  31. package/dist/commands-guidance.js.map +1 -0
  32. package/dist/commands-hooks.d.ts +16 -5
  33. package/dist/commands-hooks.js +433 -38
  34. package/dist/commands-hooks.js.map +1 -1
  35. package/dist/commands-init.d.ts +10 -18
  36. package/dist/commands-init.js +139 -54
  37. package/dist/commands-init.js.map +1 -1
  38. package/dist/commands-send-support.d.ts +17 -2
  39. package/dist/commands-send-support.js +66 -8
  40. package/dist/commands-send-support.js.map +1 -1
  41. package/dist/commands-send.d.ts +0 -1
  42. package/dist/commands-send.js +105 -39
  43. package/dist/commands-send.js.map +1 -1
  44. package/dist/commands-setup-proof.d.ts +8 -0
  45. package/dist/commands-setup-proof.js +31 -2
  46. package/dist/commands-setup-proof.js.map +1 -1
  47. package/dist/commands-skill.d.ts +11 -1
  48. package/dist/commands-skill.js +61 -11
  49. package/dist/commands-skill.js.map +1 -1
  50. package/dist/commands.d.ts +2 -1
  51. package/dist/commands.js +1 -0
  52. package/dist/commands.js.map +1 -1
  53. package/dist/config-schema.d.ts +1 -1
  54. package/dist/config-schema.js +30 -14
  55. package/dist/config-schema.js.map +1 -1
  56. package/dist/config.d.ts +24 -5
  57. package/dist/config.js +37 -8
  58. package/dist/config.js.map +1 -1
  59. package/dist/credentials.d.ts +10 -1
  60. package/dist/credentials.js +25 -12
  61. package/dist/credentials.js.map +1 -1
  62. package/dist/guidance-content.d.ts +50 -0
  63. package/dist/guidance-content.js +240 -0
  64. package/dist/guidance-content.js.map +1 -0
  65. package/dist/guidance.d.ts +69 -0
  66. package/dist/guidance.js +173 -0
  67. package/dist/guidance.js.map +1 -0
  68. package/dist/hook-adapter.js +37 -3
  69. package/dist/hook-adapter.js.map +1 -1
  70. package/dist/hooks.d.ts +68 -7
  71. package/dist/hooks.js +238 -92
  72. package/dist/hooks.js.map +1 -1
  73. package/dist/install-hooks.d.ts +10 -18
  74. package/dist/install-hooks.js +110 -23
  75. package/dist/install-hooks.js.map +1 -1
  76. package/dist/interactive.d.ts +1 -1
  77. package/dist/interactive.js +14 -9
  78. package/dist/interactive.js.map +1 -1
  79. package/dist/logging.d.ts +8 -1
  80. package/dist/logging.js +43 -2
  81. package/dist/logging.js.map +1 -1
  82. package/dist/main.js +5 -487
  83. package/dist/main.js.map +1 -1
  84. package/dist/native-skills.d.ts +19 -0
  85. package/dist/native-skills.js +30 -8
  86. package/dist/native-skills.js.map +1 -1
  87. package/dist/opencode-plugin.js +14 -1
  88. package/dist/opencode-plugin.js.map +1 -1
  89. package/dist/program.d.ts +49 -0
  90. package/dist/program.js +576 -0
  91. package/dist/program.js.map +1 -0
  92. package/dist/send.d.ts +12 -10
  93. package/dist/send.js +65 -29
  94. package/dist/send.js.map +1 -1
  95. package/dist/session-activation.d.ts +15 -0
  96. package/dist/session-activation.js +35 -0
  97. package/dist/session-activation.js.map +1 -0
  98. package/dist/session-labels.d.ts +9 -3
  99. package/dist/session-labels.js +38 -7
  100. package/dist/session-labels.js.map +1 -1
  101. package/dist/url-policy.d.ts +85 -0
  102. package/dist/url-policy.js +369 -0
  103. package/dist/url-policy.js.map +1 -0
  104. package/package.json +6 -4
  105. package/src/atomic-file.ts +36 -2
  106. package/src/cli-bin.test.ts +38 -0
  107. package/src/cli-bin.ts +67 -0
  108. package/src/cli-release.test.ts +46 -0
  109. package/src/cli-release.ts +78 -0
  110. package/src/client.test.ts +95 -0
  111. package/src/client.ts +16 -3
  112. package/src/commands-auth.ts +32 -3
  113. package/src/commands-config.ts +25 -2
  114. package/src/commands-core.ts +38 -3
  115. package/src/commands-devices.ts +28 -9
  116. package/src/commands-doctor.ts +116 -25
  117. package/src/commands-guidance.ts +216 -0
  118. package/src/commands-hooks.ts +513 -42
  119. package/src/commands-init.ts +170 -60
  120. package/src/commands-send-support.test.ts +156 -0
  121. package/src/commands-send-support.ts +72 -8
  122. package/src/commands-send.ts +123 -38
  123. package/src/commands-setup-proof.ts +36 -2
  124. package/src/commands-skill.ts +78 -15
  125. package/src/commands.test.ts +1490 -197
  126. package/src/commands.ts +6 -0
  127. package/src/config-schema.ts +32 -16
  128. package/src/config.test.ts +57 -2
  129. package/src/config.ts +49 -10
  130. package/src/credentials.test.ts +71 -3
  131. package/src/credentials.ts +29 -15
  132. package/src/guidance-content.ts +256 -0
  133. package/src/guidance.test.ts +299 -0
  134. package/src/guidance.ts +233 -0
  135. package/src/hook-adapter.test.ts +113 -1
  136. package/src/hook-adapter.ts +37 -3
  137. package/src/hooks.test.ts +783 -170
  138. package/src/hooks.ts +306 -131
  139. package/src/install-hooks.test.ts +135 -16
  140. package/src/install-hooks.ts +113 -25
  141. package/src/interactive.ts +14 -9
  142. package/src/logging.test.ts +66 -3
  143. package/src/logging.ts +45 -2
  144. package/src/main.ts +6 -614
  145. package/src/native-skills.test.ts +41 -1
  146. package/src/native-skills.ts +40 -6
  147. package/src/opencode-plugin.ts +14 -1
  148. package/src/program.test.ts +237 -0
  149. package/src/program.ts +751 -0
  150. package/src/send.test.ts +28 -0
  151. package/src/send.ts +81 -34
  152. package/src/session-activation.ts +45 -0
  153. package/src/session-labels.test.ts +120 -3
  154. package/src/session-labels.ts +38 -7
  155. package/src/skill.test.ts +153 -12
  156. package/src/trust-policy.test.ts +701 -0
  157. 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.
@@ -60,11 +87,11 @@ describe('Notifai agent skill', () => {
60
87
  expect(decide).toBeLessThan(compose)
61
88
  })
62
89
 
63
- it('keeps session instructions out of config and agent paraphrase out of criteria', () => {
90
+ it('keeps session instructions out of config and agent paraphrase out of guidance', () => {
64
91
  // The rule this pins: a conversational instruction is followed, not
65
- // persisted, and criteria are stored in the user's words verbatim. An
66
- // agent once turned "guide me when you need me" into a persisted
67
- // notify_criteria in its own wording, silently via --yes.
92
+ // persisted, and persisted guidance holds the user's words verbatim. An
93
+ // agent once turned "guide me when you need me" into a persisted standing
94
+ // instruction in its own wording, silently via --yes.
68
95
  const decide = section('## Decide whether to notify')
69
96
  expect(decide).toMatch(/never touches config/i)
70
97
  expect(decide).toMatch(/words\s+verbatim/i)
@@ -72,6 +99,85 @@ describe('Notifai agent skill', () => {
72
99
  expect(decide).toMatch(/`--yes` skips the\s+CLI's confirmation/i)
73
100
  })
74
101
 
102
+ it('sends the agent to the resolved guidance before it writes anything', () => {
103
+ // The writing guidance itself lives in `notifai guidance` (shipped topics,
104
+ // user-overridable per layer); the skill's job is to make reading it the
105
+ // first step and user layers authoritative.
106
+ const decide = section('## Decide whether to notify')
107
+ expect(decide).toContain('notifai guidance')
108
+ for (const topic of ['when-to-notify', 'titles', 'bodies', 'questions', 'acknowledgements']) {
109
+ expect(decide).toContain(`\`${topic}\``)
110
+ }
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)
179
+ })
180
+
75
181
  it('names every notification kind the CLI accepts', () => {
76
182
  for (const kind of NOTIFICATION_KINDS) {
77
183
  expect(skill).toContain(`\`${kind}\``)
@@ -88,6 +194,15 @@ describe('Notifai agent skill', () => {
88
194
  expect(skill).not.toMatch(/kind never chooses/i)
89
195
  })
90
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
+
91
206
  it('keeps every send example valid against the CLI it documents', () => {
92
207
  const sends = shellExamples.filter((line) => line.startsWith('notifai send'))
93
208
  expect(sends.length).toBeGreaterThan(0)
@@ -106,7 +221,7 @@ describe('Notifai agent skill', () => {
106
221
  expect(send).toMatch(/one canonical Markdown body/i)
107
222
  expect(send).toContain('media:1')
108
223
  // Type and project are structured fields, never title text.
109
- expect(send).toMatch(/never\s+put the kind or the project in it/i)
224
+ expect(send).toMatch(/kind and the project travel as their own\s+fields, never in it/i)
110
225
  })
111
226
 
112
227
  it('names each immutable session once without asking the agent for its id', () => {
@@ -114,14 +229,26 @@ describe('Notifai agent skill', () => {
114
229
  expect(send).toContain('--session-label')
115
230
  expect(send).toMatch(/first Notification\s+Request/i)
116
231
  expect(send).toMatch(/2-6 words/i)
117
- expect(send).toMatch(/Managed OpenCode and Orca-managed Claude Code/i)
118
- expect(send).toMatch(/omit `--session-label` there/i)
119
- expect(send).toMatch(/Outside those managed sources/i)
120
- expect(send).toMatch(/Claude Code, Codex,\s+and Cursor/i)
121
- expect(send).toMatch(/freezes the first accepted name/i)
122
- expect(send).toMatch(/omit the\s+flag on later sends and questions/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)
235
+ expect(send).toMatch(/name the\s+environment supplies wins/i)
236
+ expect(send).toMatch(/freezes one semantic name/i)
237
+ expect(send).toMatch(/omit the flag on later sends and\s+questions/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)
123
240
  expect(send).toMatch(/never pass\s+`--session-id`/i)
124
- expect(send).toMatch(/identifier, hash, or filesystem path/i)
241
+ expect(send).toMatch(/identifier, hash, or\s+filesystem path/i)
242
+ })
243
+
244
+ it('never teaches by enumerating environments the CLI can detect itself', () => {
245
+ // "What if we had 24 different plugins, are we gonna list them all?" —
246
+ // guidance states one rule; the mechanism owns environment detection.
247
+ // Harness names may appear only in setup/routing material, where the
248
+ // harness IS the subject.
249
+ for (const heading of ['## Decide whether to notify', '## Send', '## Ask a question', '## When the answer arrives']) {
250
+ expect(section(heading), heading).not.toMatch(/OpenCode|Orca|Cursor|Codex/)
251
+ }
125
252
  })
126
253
 
127
254
  it('separates how long the command waits from how long an answer is accepted', () => {
@@ -131,6 +258,12 @@ describe('Notifai agent skill', () => {
131
258
  expect(ask).toMatch(/exit code 3/i)
132
259
  })
133
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
+
134
267
  it('makes registration the middle of the asking turn, not the end', () => {
135
268
  const ask = section('## Ask a question')
136
269
  expect(ask).toMatch(/registering is not the end of the turn/i)
@@ -165,6 +298,12 @@ describe('Notifai agent skill', () => {
165
298
  expect(skill).toContain('notifai replies --pending')
166
299
  })
167
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
+
168
307
  it('makes the agent the operator and the human only the human', () => {
169
308
  const setup = section('## Set Notifai up')
170
309
  expect(setup).toMatch(/never tell the\s+user to run a command you could have run/i)
@@ -172,6 +311,7 @@ describe('Notifai agent skill', () => {
172
311
  expect(setup).toMatch(/companion/i)
173
312
  expect(setup).toContain('notifai doctor --json')
174
313
  expect(setup).toContain('notifai init')
314
+ expect(setup).toContain('--setup-scope')
175
315
  })
176
316
 
177
317
  it('offers npx only as a pinned fallback behind a real install', () => {
@@ -234,6 +374,7 @@ describe('Notifai agent skill', () => {
234
374
  'notifai doctor',
235
375
  'notifai init',
236
376
  'notifai config show',
377
+ 'notifai guidance',
237
378
  'notifai logs',
238
379
  'notifai status',
239
380
  ]) {