@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.
- package/CHANGELOG.md +41 -0
- package/README.md +20 -13
- package/dist/atomic-file.d.ts +14 -0
- package/dist/atomic-file.js +27 -2
- package/dist/atomic-file.js.map +1 -1
- package/dist/cli-bin.d.ts +4 -0
- package/dist/cli-bin.js +62 -0
- package/dist/cli-bin.js.map +1 -0
- package/dist/cli-release.d.ts +10 -0
- package/dist/cli-release.js +75 -0
- package/dist/cli-release.js.map +1 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +13 -3
- package/dist/client.js.map +1 -1
- package/dist/commands-auth.d.ts +6 -0
- package/dist/commands-auth.js +28 -3
- package/dist/commands-auth.js.map +1 -1
- package/dist/commands-config.js +22 -3
- package/dist/commands-config.js.map +1 -1
- package/dist/commands-core.d.ts +12 -0
- package/dist/commands-core.js +27 -3
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-doctor.d.ts +1 -0
- package/dist/commands-doctor.js +71 -15
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-guidance.d.ts +7 -3
- package/dist/commands-guidance.js +63 -28
- package/dist/commands-guidance.js.map +1 -1
- package/dist/commands-hooks.d.ts +9 -4
- package/dist/commands-hooks.js +430 -32
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +10 -18
- package/dist/commands-init.js +114 -32
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.d.ts +17 -2
- package/dist/commands-send-support.js +61 -5
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-send.js +60 -11
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-setup-proof.d.ts +8 -0
- package/dist/commands-setup-proof.js +31 -2
- package/dist/commands-setup-proof.js.map +1 -1
- package/dist/commands-skill.d.ts +11 -1
- package/dist/commands-skill.js +61 -11
- package/dist/commands-skill.js.map +1 -1
- package/dist/commands.d.ts +1 -1
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +25 -0
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +24 -3
- package/dist/config.js +37 -5
- package/dist/config.js.map +1 -1
- package/dist/credentials.d.ts +10 -1
- package/dist/credentials.js +25 -12
- package/dist/credentials.js.map +1 -1
- package/dist/guidance-content.d.ts +20 -0
- package/dist/guidance-content.js +73 -8
- package/dist/guidance-content.js.map +1 -1
- package/dist/guidance.d.ts +21 -0
- package/dist/guidance.js +73 -18
- package/dist/guidance.js.map +1 -1
- package/dist/hook-adapter.js +37 -3
- package/dist/hook-adapter.js.map +1 -1
- package/dist/hooks.d.ts +68 -7
- package/dist/hooks.js +231 -85
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.d.ts +10 -18
- package/dist/install-hooks.js +110 -23
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.d.ts +1 -1
- package/dist/interactive.js +1 -1
- package/dist/interactive.js.map +1 -1
- package/dist/logging.d.ts +8 -1
- package/dist/logging.js +43 -2
- package/dist/logging.js.map +1 -1
- package/dist/main.js +2 -13
- package/dist/main.js.map +1 -1
- package/dist/native-skills.d.ts +19 -0
- package/dist/native-skills.js +30 -8
- package/dist/native-skills.js.map +1 -1
- package/dist/opencode-plugin.js +14 -1
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/program.js +13 -8
- package/dist/program.js.map +1 -1
- package/dist/send.d.ts +8 -1
- package/dist/send.js +17 -1
- package/dist/send.js.map +1 -1
- package/dist/session-activation.d.ts +15 -0
- package/dist/session-activation.js +35 -0
- package/dist/session-activation.js.map +1 -0
- package/dist/url-policy.d.ts +85 -0
- package/dist/url-policy.js +369 -0
- package/dist/url-policy.js.map +1 -0
- package/package.json +3 -3
- package/src/atomic-file.ts +36 -2
- package/src/cli-bin.test.ts +38 -0
- package/src/cli-bin.ts +67 -0
- package/src/cli-release.test.ts +46 -0
- package/src/cli-release.ts +78 -0
- package/src/client.test.ts +95 -0
- package/src/client.ts +16 -3
- package/src/commands-auth.ts +32 -3
- package/src/commands-config.ts +25 -2
- package/src/commands-core.ts +38 -3
- package/src/commands-doctor.ts +99 -13
- package/src/commands-guidance.ts +75 -29
- package/src/commands-hooks.ts +503 -34
- package/src/commands-init.ts +139 -38
- package/src/commands-send-support.test.ts +156 -0
- package/src/commands-send-support.ts +67 -5
- package/src/commands-send.ts +73 -10
- package/src/commands-setup-proof.ts +36 -2
- package/src/commands-skill.ts +78 -15
- package/src/commands.test.ts +1083 -101
- package/src/commands.ts +1 -0
- package/src/config-schema.ts +28 -1
- package/src/config.test.ts +1 -1
- package/src/config.ts +49 -5
- package/src/credentials.test.ts +71 -3
- package/src/credentials.ts +29 -15
- package/src/guidance-content.ts +74 -8
- package/src/guidance.test.ts +42 -11
- package/src/guidance.ts +100 -18
- package/src/hook-adapter.test.ts +113 -1
- package/src/hook-adapter.ts +37 -3
- package/src/hooks.test.ts +746 -165
- package/src/hooks.ts +299 -124
- package/src/install-hooks.test.ts +135 -16
- package/src/install-hooks.ts +113 -25
- package/src/interactive.ts +1 -1
- package/src/logging.test.ts +66 -3
- package/src/logging.ts +45 -2
- package/src/main.ts +2 -14
- package/src/native-skills.test.ts +41 -1
- package/src/native-skills.ts +40 -6
- package/src/opencode-plugin.ts +14 -1
- package/src/program.test.ts +37 -2
- package/src/program.ts +40 -12
- package/src/send.test.ts +28 -0
- package/src/send.ts +28 -2
- package/src/session-activation.ts +45 -0
- package/src/skill.test.ts +120 -4
- package/src/trust-policy.test.ts +701 -0
- 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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
expect(send).toMatch(/
|
|
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', () => {
|