@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.
- package/CHANGELOG.md +75 -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-devices.d.ts +2 -1
- package/dist/commands-devices.js +20 -8
- package/dist/commands-devices.js.map +1 -1
- package/dist/commands-doctor.d.ts +1 -0
- package/dist/commands-doctor.js +85 -27
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-guidance.d.ts +27 -0
- package/dist/commands-guidance.js +163 -0
- package/dist/commands-guidance.js.map +1 -0
- package/dist/commands-hooks.d.ts +16 -5
- package/dist/commands-hooks.js +433 -38
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +10 -18
- package/dist/commands-init.js +139 -54
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.d.ts +17 -2
- package/dist/commands-send-support.js +66 -8
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-send.d.ts +0 -1
- package/dist/commands-send.js +105 -39
- 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 +2 -1
- package/dist/commands.js +1 -0
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +30 -14
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +24 -5
- package/dist/config.js +37 -8
- 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 +50 -0
- package/dist/guidance-content.js +240 -0
- package/dist/guidance-content.js.map +1 -0
- package/dist/guidance.d.ts +69 -0
- package/dist/guidance.js +173 -0
- package/dist/guidance.js.map +1 -0
- 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 +238 -92
- 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 +14 -9
- 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 +5 -487
- 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.d.ts +49 -0
- package/dist/program.js +576 -0
- package/dist/program.js.map +1 -0
- package/dist/send.d.ts +12 -10
- package/dist/send.js +65 -29
- 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/session-labels.d.ts +9 -3
- package/dist/session-labels.js +38 -7
- package/dist/session-labels.js.map +1 -1
- 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 +6 -4
- 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-devices.ts +28 -9
- package/src/commands-doctor.ts +116 -25
- package/src/commands-guidance.ts +216 -0
- package/src/commands-hooks.ts +513 -42
- package/src/commands-init.ts +170 -60
- package/src/commands-send-support.test.ts +156 -0
- package/src/commands-send-support.ts +72 -8
- package/src/commands-send.ts +123 -38
- package/src/commands-setup-proof.ts +36 -2
- package/src/commands-skill.ts +78 -15
- package/src/commands.test.ts +1490 -197
- package/src/commands.ts +6 -0
- package/src/config-schema.ts +32 -16
- package/src/config.test.ts +57 -2
- package/src/config.ts +49 -10
- package/src/credentials.test.ts +71 -3
- package/src/credentials.ts +29 -15
- package/src/guidance-content.ts +256 -0
- package/src/guidance.test.ts +299 -0
- package/src/guidance.ts +233 -0
- package/src/hook-adapter.test.ts +113 -1
- package/src/hook-adapter.ts +37 -3
- package/src/hooks.test.ts +783 -170
- package/src/hooks.ts +306 -131
- package/src/install-hooks.test.ts +135 -16
- package/src/install-hooks.ts +113 -25
- package/src/interactive.ts +14 -9
- package/src/logging.test.ts +66 -3
- package/src/logging.ts +45 -2
- package/src/main.ts +6 -614
- 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 +237 -0
- package/src/program.ts +751 -0
- package/src/send.test.ts +28 -0
- package/src/send.ts +81 -34
- package/src/session-activation.ts +45 -0
- package/src/session-labels.test.ts +120 -3
- package/src/session-labels.ts +38 -7
- package/src/skill.test.ts +153 -12
- 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.
|
|
@@ -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
|
|
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
|
|
66
|
-
// agent once turned "guide me when you need me" into a persisted
|
|
67
|
-
//
|
|
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(/
|
|
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(/
|
|
118
|
-
expect(send).toMatch(/
|
|
119
|
-
expect(send).toMatch(/
|
|
120
|
-
expect(send).toMatch(/
|
|
121
|
-
expect(send).toMatch(/freezes
|
|
122
|
-
expect(send).toMatch(/omit the
|
|
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
|
|
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
|
]) {
|