@raidiant/notifai 6.0.0 → 7.0.1
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 +34 -0
- package/dist/atomic-file.js +13 -0
- package/dist/atomic-file.js.map +1 -1
- package/dist/commands-core.d.ts +3 -0
- package/dist/commands-core.js +23 -4
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-doctor.js +7 -5
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-harness-context.d.ts +4 -0
- package/dist/commands-harness-context.js +4 -0
- package/dist/commands-harness-context.js.map +1 -1
- package/dist/commands-init.js +0 -1
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.js +22 -0
- package/dist/commands-send-support.js.map +1 -1
- package/dist/config.d.ts +3 -3
- package/dist/config.js +3 -3
- package/dist/generated-session-label.d.ts +2 -0
- package/dist/generated-session-label.js +152 -0
- package/dist/generated-session-label.js.map +1 -0
- package/dist/invocation-context.d.ts +3 -0
- package/dist/invocation-context.js +23 -6
- package/dist/invocation-context.js.map +1 -1
- package/dist/main.js +2 -2
- package/dist/main.js.map +1 -1
- package/dist/opencode-plugin.js +18 -3
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/orca-session-title.d.ts +10 -0
- package/dist/orca-session-title.js +99 -0
- package/dist/orca-session-title.js.map +1 -0
- package/dist/session-labels.d.ts +30 -0
- package/dist/session-labels.js +278 -0
- package/dist/session-labels.js.map +1 -0
- package/package.json +2 -2
- package/src/atomic-file.ts +12 -0
- package/src/commands-core.ts +24 -4
- package/src/commands-doctor.ts +7 -6
- package/src/commands-harness-context.ts +8 -0
- package/src/commands-init.ts +0 -1
- package/src/commands-send-support.ts +27 -0
- package/src/commands.test.ts +203 -6
- package/src/config.ts +3 -3
- package/src/generated-session-label.ts +155 -0
- package/src/hooks.test.ts +4 -5
- package/src/install-hooks.test.ts +84 -1
- package/src/invocation-context.test.ts +115 -16
- package/src/invocation-context.ts +32 -11
- package/src/main.ts +8 -2
- package/src/opencode-plugin.ts +18 -3
- package/src/orca-session-title.test.ts +95 -0
- package/src/orca-session-title.ts +105 -0
- package/src/session-labels.test.ts +324 -0
- package/src/session-labels.ts +360 -0
- package/src/skill.test.ts +15 -0
package/src/commands.test.ts
CHANGED
|
@@ -180,6 +180,14 @@ class PlainInteractiveIo extends CapturedIo {
|
|
|
180
180
|
interactive = true
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
class OutroCapturedIo extends CapturedIo {
|
|
184
|
+
outros: string[] = []
|
|
185
|
+
|
|
186
|
+
async outro(message: string) {
|
|
187
|
+
this.outros.push(message)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
183
191
|
class InteractiveIo extends CapturedIo {
|
|
184
192
|
interactive = true
|
|
185
193
|
selectAnswer: string | null = 'global'
|
|
@@ -500,11 +508,162 @@ describe('command contracts', () => {
|
|
|
500
508
|
session_id: 'opaque-claude-session-42',
|
|
501
509
|
harness: 'claude-code',
|
|
502
510
|
})
|
|
503
|
-
expect(submitted?.draft.source?.session_label).
|
|
511
|
+
expect(submitted?.draft.source?.session_label).toBe('Rapid Antelope')
|
|
504
512
|
expect(io.outLines.join('\n')).not.toContain('opaque-claude-session-42')
|
|
505
513
|
expect(io.errLines.join('\n')).not.toContain('opaque-claude-session-42')
|
|
506
514
|
})
|
|
507
515
|
|
|
516
|
+
it('uses the exact Orca worktree title for an active Claude session send', async () => {
|
|
517
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-orca-claude-title-'))
|
|
518
|
+
const io = new CapturedIo()
|
|
519
|
+
let submitted: SubmitNotificationRequestT | undefined
|
|
520
|
+
const client = {
|
|
521
|
+
submit: async (body: SubmitNotificationRequestT) => {
|
|
522
|
+
submitted = body
|
|
523
|
+
return receipt
|
|
524
|
+
},
|
|
525
|
+
} as unknown as ApiClient
|
|
526
|
+
const worktreeId = `repo-123::${cwd}`
|
|
527
|
+
const env = {
|
|
528
|
+
XDG_CONFIG_HOME: path.join(cwd, 'config'),
|
|
529
|
+
XDG_STATE_HOME: path.join(cwd, 'state'),
|
|
530
|
+
CLAUDECODE: '1',
|
|
531
|
+
CLAUDE_CODE_SESSION_ID: 'orca-claude-session',
|
|
532
|
+
TERM_PROGRAM: 'Orca',
|
|
533
|
+
ORCA_WORKTREE_ID: worktreeId,
|
|
534
|
+
}
|
|
535
|
+
const deps = {
|
|
536
|
+
...makeDeps(io, client),
|
|
537
|
+
cwd,
|
|
538
|
+
env,
|
|
539
|
+
orcaSessionTitle: (lookupEnv: NodeJS.ProcessEnv) => {
|
|
540
|
+
expect(lookupEnv['ORCA_WORKTREE_ID']).toBe(worktreeId)
|
|
541
|
+
return 'Worker - semantic session implementation'
|
|
542
|
+
},
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
expect(
|
|
546
|
+
await sendCommand(deps, {
|
|
547
|
+
title: 'Resolver implemented',
|
|
548
|
+
body: 'The Orca worktree title was frozen locally.',
|
|
549
|
+
kind: 'done',
|
|
550
|
+
}),
|
|
551
|
+
).toBe(EXIT.ok)
|
|
552
|
+
|
|
553
|
+
expect(submitted?.draft.source).toMatchObject({
|
|
554
|
+
session_id: 'orca-claude-session',
|
|
555
|
+
session_label: 'Worker - semantic session implementation',
|
|
556
|
+
harness: 'claude-code',
|
|
557
|
+
})
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
it('falls back safely when Orca returns a private path as its title', async () => {
|
|
561
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-orca-private-title-'))
|
|
562
|
+
const io = new CapturedIo()
|
|
563
|
+
let submitted: SubmitNotificationRequestT | undefined
|
|
564
|
+
const client = {
|
|
565
|
+
submit: async (body: SubmitNotificationRequestT) => {
|
|
566
|
+
submitted = body
|
|
567
|
+
return receipt
|
|
568
|
+
},
|
|
569
|
+
} as unknown as ApiClient
|
|
570
|
+
const deps = {
|
|
571
|
+
...makeDeps(io, client),
|
|
572
|
+
cwd,
|
|
573
|
+
env: {
|
|
574
|
+
XDG_CONFIG_HOME: path.join(cwd, 'config'),
|
|
575
|
+
XDG_STATE_HOME: path.join(cwd, 'state'),
|
|
576
|
+
CLAUDECODE: '1',
|
|
577
|
+
CLAUDE_CODE_SESSION_ID: 'orca-private-title-session',
|
|
578
|
+
TERM_PROGRAM: 'Orca',
|
|
579
|
+
ORCA_WORKTREE_ID: `repo-123::${cwd}`,
|
|
580
|
+
},
|
|
581
|
+
orcaSessionTitle: () => '/private/untrusted/worktree',
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
expect(
|
|
585
|
+
await sendCommand(deps, {
|
|
586
|
+
title: 'Resolver implemented',
|
|
587
|
+
body: 'Unsafe metadata must never become a visible name.',
|
|
588
|
+
kind: 'done',
|
|
589
|
+
}),
|
|
590
|
+
).toBe(EXIT.ok)
|
|
591
|
+
|
|
592
|
+
expect(submitted?.draft.source?.session_label).toMatch(/^[A-Z][a-z]+ [A-Z][a-z]+$/)
|
|
593
|
+
expect(submitted?.draft.source?.session_label).not.toContain('/private/')
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
it('uses the trusted OpenCode session title published by its managed adapter', async () => {
|
|
597
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-opencode-title-'))
|
|
598
|
+
const io = new CapturedIo()
|
|
599
|
+
let submitted: SubmitNotificationRequestT | undefined
|
|
600
|
+
const client = {
|
|
601
|
+
submit: async (body: SubmitNotificationRequestT) => {
|
|
602
|
+
submitted = body
|
|
603
|
+
return receipt
|
|
604
|
+
},
|
|
605
|
+
} as unknown as ApiClient
|
|
606
|
+
const deps = {
|
|
607
|
+
...makeDeps(io, client),
|
|
608
|
+
cwd,
|
|
609
|
+
env: {
|
|
610
|
+
XDG_CONFIG_HOME: path.join(cwd, 'config'),
|
|
611
|
+
XDG_STATE_HOME: path.join(cwd, 'state'),
|
|
612
|
+
NOTIFAI_ACTIVE_HARNESS: 'opencode',
|
|
613
|
+
NOTIFAI_ACTIVE_SESSION_ID: 'opencode-session',
|
|
614
|
+
NOTIFAI_ACTIVE_SESSION_LABEL: 'Semantic session names',
|
|
615
|
+
},
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
expect(
|
|
619
|
+
await sendCommand(deps, {
|
|
620
|
+
title: 'Resolver implemented',
|
|
621
|
+
body: 'The first-party title was frozen locally.',
|
|
622
|
+
kind: 'done',
|
|
623
|
+
}),
|
|
624
|
+
).toBe(EXIT.ok)
|
|
625
|
+
|
|
626
|
+
expect(submitted?.draft.source).toMatchObject({
|
|
627
|
+
session_id: 'opencode-session',
|
|
628
|
+
session_label: 'Semantic session names',
|
|
629
|
+
harness: 'opencode',
|
|
630
|
+
})
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
it('does not freeze or submit an OpenCode placeholder title', async () => {
|
|
634
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-opencode-pending-'))
|
|
635
|
+
const io = new CapturedIo()
|
|
636
|
+
let submitted = false
|
|
637
|
+
const client = {
|
|
638
|
+
submit: async () => {
|
|
639
|
+
submitted = true
|
|
640
|
+
return receipt
|
|
641
|
+
},
|
|
642
|
+
} as unknown as ApiClient
|
|
643
|
+
const deps = {
|
|
644
|
+
...makeDeps(io, client),
|
|
645
|
+
cwd,
|
|
646
|
+
env: {
|
|
647
|
+
XDG_CONFIG_HOME: path.join(cwd, 'config'),
|
|
648
|
+
XDG_STATE_HOME: path.join(cwd, 'state'),
|
|
649
|
+
NOTIFAI_ACTIVE_HARNESS: 'opencode',
|
|
650
|
+
NOTIFAI_ACTIVE_SESSION_ID: 'opencode-session',
|
|
651
|
+
NOTIFAI_ACTIVE_SESSION_LABEL_PENDING: '1',
|
|
652
|
+
},
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
expect(
|
|
656
|
+
await sendCommand(deps, {
|
|
657
|
+
title: 'Resolver implemented',
|
|
658
|
+
body: 'Wait for the semantic title before freezing a name.',
|
|
659
|
+
kind: 'done',
|
|
660
|
+
}),
|
|
661
|
+
).toBe(EXIT.usage)
|
|
662
|
+
expect(submitted).toBe(false)
|
|
663
|
+
expect(io.errLines.join('\n')).toContain('still generating this session')
|
|
664
|
+
expect(existsSync(path.join(cwd, 'state', 'notifai', 'session-labels.json'))).toBe(false)
|
|
665
|
+
})
|
|
666
|
+
|
|
508
667
|
it('uploads repeatable images in order and sends only canonical media references', async () => {
|
|
509
668
|
const root = mkdtempSync(path.join(os.tmpdir(), 'notifai-media-order-'))
|
|
510
669
|
const first = path.join(root, 'first.png')
|
|
@@ -653,7 +812,7 @@ describe('command contracts', () => {
|
|
|
653
812
|
expect(grammar).not.toContain(".option('--session <")
|
|
654
813
|
expect(grammar).toContain(".option('--body-file")
|
|
655
814
|
expect(grammar).toContain(".option('--session-id")
|
|
656
|
-
expect(grammar).toContain("
|
|
815
|
+
expect(grammar).toContain("'--session-label <text>'")
|
|
657
816
|
expect(grammar).toContain("'--image <path|url|media_id>'")
|
|
658
817
|
expect(grammar).toContain("'--image-alt <text>'")
|
|
659
818
|
}
|
|
@@ -2867,7 +3026,7 @@ describe('compatibility-first update guidance', () => {
|
|
|
2867
3026
|
listDevices: async () => ({ devices: [] }),
|
|
2868
3027
|
accessStatus: async () => ({ email: 'user@example.test' }),
|
|
2869
3028
|
} as unknown as ApiClient
|
|
2870
|
-
const humanIo = new
|
|
3029
|
+
const humanIo = new InteractiveIo()
|
|
2871
3030
|
const deps = {
|
|
2872
3031
|
...makeDeps(humanIo, client),
|
|
2873
3032
|
cwd,
|
|
@@ -2882,6 +3041,9 @@ describe('compatibility-first update guidance', () => {
|
|
|
2882
3041
|
).toBe(exit)
|
|
2883
3042
|
expect(humanIo.outLines).toEqual(humanLines)
|
|
2884
3043
|
expect(humanIo.errLines).toEqual([])
|
|
3044
|
+
expect(humanIo.outros).toEqual(
|
|
3045
|
+
status === 'ready' ? ['Everything looks good'] : [],
|
|
3046
|
+
)
|
|
2885
3047
|
|
|
2886
3048
|
const jsonIo = new CapturedIo()
|
|
2887
3049
|
expect(
|
|
@@ -3140,7 +3302,7 @@ describe('init', () => {
|
|
|
3140
3302
|
|
|
3141
3303
|
it('resumes the same init after a required CLI update becomes current', async () => {
|
|
3142
3304
|
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-init-update-resume-'))
|
|
3143
|
-
const io = new
|
|
3305
|
+
const io = new OutroCapturedIo()
|
|
3144
3306
|
let cliSupport = compatibilityWithCli({
|
|
3145
3307
|
state: 'must_update',
|
|
3146
3308
|
reason: 'minimum_not_met',
|
|
@@ -3175,11 +3337,13 @@ describe('init', () => {
|
|
|
3175
3337
|
expect(await initCommand(deps, { hooks: false, skills: false })).toBe(EXIT.failed)
|
|
3176
3338
|
expect(io.outLines).toContain("Notifai can't send notifications until you update.")
|
|
3177
3339
|
expect(io.outLines).toContain('npm install -g @raidiant/notifai')
|
|
3340
|
+
expect(io.outros).toEqual([])
|
|
3178
3341
|
expect(submissions).toBe(0)
|
|
3179
3342
|
|
|
3180
3343
|
cliSupport = currentCompatibility
|
|
3181
3344
|
io.outLines = []
|
|
3182
3345
|
io.errLines = []
|
|
3346
|
+
io.outros = []
|
|
3183
3347
|
|
|
3184
3348
|
expect(await initCommand(deps, { hooks: false, skills: false })).toBe(EXIT.ok)
|
|
3185
3349
|
expect(submissions).toBe(1)
|
|
@@ -4352,7 +4516,7 @@ describe('asking before the hooks have ever run', () => {
|
|
|
4352
4516
|
})
|
|
4353
4517
|
expect(
|
|
4354
4518
|
readSessionState('codex-current-thread', env).pending?.[0]?.source?.session_label,
|
|
4355
|
-
).
|
|
4519
|
+
).toBe('Gentle Salmon')
|
|
4356
4520
|
})
|
|
4357
4521
|
|
|
4358
4522
|
it('uploads ask images before registration and freezes canonical body media', async () => {
|
|
@@ -4484,6 +4648,39 @@ describe('asking before the hooks have ever run', () => {
|
|
|
4484
4648
|
expect(readSessionState('codex-orchestrator', env).pending).toBeUndefined()
|
|
4485
4649
|
})
|
|
4486
4650
|
|
|
4651
|
+
it('registers an Orca-managed Claude question under the semantic worktree title', () => {
|
|
4652
|
+
const cwd = scratchDir('notifai-orca-claude-ask-')
|
|
4653
|
+
const io = new CapturedIo()
|
|
4654
|
+
const env = {
|
|
4655
|
+
...isolatedEnv(cwd),
|
|
4656
|
+
CLAUDECODE: '1',
|
|
4657
|
+
CLAUDE_CODE_SESSION_ID: 'orca-claude-question',
|
|
4658
|
+
TERM_PROGRAM: 'Orca',
|
|
4659
|
+
ORCA_WORKTREE_ID: `repo-123::${cwd}`,
|
|
4660
|
+
}
|
|
4661
|
+
const deps = {
|
|
4662
|
+
...makeDeps(io, {} as ApiClient),
|
|
4663
|
+
cwd,
|
|
4664
|
+
env,
|
|
4665
|
+
now: () => 42,
|
|
4666
|
+
orcaSessionTitle: () => 'Worker - semantic session implementation',
|
|
4667
|
+
}
|
|
4668
|
+
|
|
4669
|
+
expect(hooksInstallCommand(deps, { harness: 'claude-code', execPath, scriptPath })).toBe(
|
|
4670
|
+
EXIT.ok,
|
|
4671
|
+
)
|
|
4672
|
+
writeSessionState('orca-claude-question', env, { last_prompt_at: 42, last_stop_at: 41 })
|
|
4673
|
+
writeProjectSession(cwd, env, 'orca-claude-question', 42, 'claude-code')
|
|
4674
|
+
io.outLines = []
|
|
4675
|
+
|
|
4676
|
+
expect(askCommand(deps, 'Ship the semantic resolver?', {})).toBe(EXIT.ok)
|
|
4677
|
+
expect(readSessionState('orca-claude-question', env).pending?.[0]?.source).toMatchObject({
|
|
4678
|
+
session_id: 'orca-claude-question',
|
|
4679
|
+
session_label: 'Worker - semantic session implementation',
|
|
4680
|
+
harness: 'claude-code',
|
|
4681
|
+
})
|
|
4682
|
+
})
|
|
4683
|
+
|
|
4487
4684
|
it('judges the fired check against the active harness, not a global installation of another', async () => {
|
|
4488
4685
|
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-fired-active-harness-'))
|
|
4489
4686
|
const io = new CapturedIo()
|
|
@@ -5386,7 +5583,7 @@ describe('command failures carrying server details', () => {
|
|
|
5386
5583
|
|
|
5387
5584
|
expect(io.errLines).toEqual([
|
|
5388
5585
|
"feature_unavailable: The selected device can't answer questions.",
|
|
5389
|
-
'next: Update Notifai on
|
|
5586
|
+
'next: Update Notifai on Old phone.',
|
|
5390
5587
|
])
|
|
5391
5588
|
expect(io.errLines.join(' ')).not.toContain('untrusted')
|
|
5392
5589
|
expect(io.errLines.join(' ')).not.toContain('disagree about the contract')
|
package/src/config.ts
CHANGED
|
@@ -188,9 +188,9 @@ export function globalConfigPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
/**
|
|
191
|
-
* Machine-local mutable state: presence markers, pending questions,
|
|
192
|
-
* session config overrides. Separate from the config dir
|
|
193
|
-
*
|
|
191
|
+
* Machine-local mutable state: presence markers, pending questions, frozen
|
|
192
|
+
* session names, and session config overrides. Separate from the config dir
|
|
193
|
+
* because none of it should ever be synced or committed.
|
|
194
194
|
*/
|
|
195
195
|
export function stateDir(
|
|
196
196
|
env: NodeJS.ProcessEnv = process.env,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const FNV1A_64_OFFSET = 0xcbf29ce484222325n
|
|
2
|
+
const FNV1A_64_PRIME = 0x100000001b3n
|
|
3
|
+
const WORD_COUNT = 64n
|
|
4
|
+
|
|
5
|
+
/** Frozen word lists keep generated fallbacks stable for a given opaque id. */
|
|
6
|
+
const ADJECTIVES = [
|
|
7
|
+
'Amber',
|
|
8
|
+
'Arctic',
|
|
9
|
+
'Ash',
|
|
10
|
+
'Azure',
|
|
11
|
+
'Blue',
|
|
12
|
+
'Bold',
|
|
13
|
+
'Bright',
|
|
14
|
+
'Bronze',
|
|
15
|
+
'Calm',
|
|
16
|
+
'Cedar',
|
|
17
|
+
'Clear',
|
|
18
|
+
'Copper',
|
|
19
|
+
'Coral',
|
|
20
|
+
'Crisp',
|
|
21
|
+
'Dawn',
|
|
22
|
+
'Deep',
|
|
23
|
+
'Ember',
|
|
24
|
+
'Fern',
|
|
25
|
+
'Frost',
|
|
26
|
+
'Gentle',
|
|
27
|
+
'Golden',
|
|
28
|
+
'Green',
|
|
29
|
+
'Harbor',
|
|
30
|
+
'Hazel',
|
|
31
|
+
'Indigo',
|
|
32
|
+
'Ivory',
|
|
33
|
+
'Jade',
|
|
34
|
+
'Keen',
|
|
35
|
+
'Lively',
|
|
36
|
+
'Lunar',
|
|
37
|
+
'Maple',
|
|
38
|
+
'Meadow',
|
|
39
|
+
'Mellow',
|
|
40
|
+
'Misty',
|
|
41
|
+
'Noble',
|
|
42
|
+
'Ocean',
|
|
43
|
+
'Olive',
|
|
44
|
+
'Onyx',
|
|
45
|
+
'Quiet',
|
|
46
|
+
'Rapid',
|
|
47
|
+
'Red',
|
|
48
|
+
'River',
|
|
49
|
+
'Royal',
|
|
50
|
+
'Sage',
|
|
51
|
+
'Silver',
|
|
52
|
+
'Solar',
|
|
53
|
+
'Steady',
|
|
54
|
+
'Stone',
|
|
55
|
+
'Sunny',
|
|
56
|
+
'Swift',
|
|
57
|
+
'Teal',
|
|
58
|
+
'Timber',
|
|
59
|
+
'True',
|
|
60
|
+
'Velvet',
|
|
61
|
+
'Verdant',
|
|
62
|
+
'Violet',
|
|
63
|
+
'Warm',
|
|
64
|
+
'Wild',
|
|
65
|
+
'Winter',
|
|
66
|
+
'Wise',
|
|
67
|
+
'Witty',
|
|
68
|
+
'Yellow',
|
|
69
|
+
'Young',
|
|
70
|
+
'Zealous',
|
|
71
|
+
] as const
|
|
72
|
+
|
|
73
|
+
const NOUNS = [
|
|
74
|
+
'Alpaca',
|
|
75
|
+
'Antelope',
|
|
76
|
+
'Badger',
|
|
77
|
+
'Bear',
|
|
78
|
+
'Beaver',
|
|
79
|
+
'Bison',
|
|
80
|
+
'Bobcat',
|
|
81
|
+
'Caribou',
|
|
82
|
+
'Cougar',
|
|
83
|
+
'Coyote',
|
|
84
|
+
'Crane',
|
|
85
|
+
'Deer',
|
|
86
|
+
'Dolphin',
|
|
87
|
+
'Dragonfly',
|
|
88
|
+
'Eagle',
|
|
89
|
+
'Elk',
|
|
90
|
+
'Falcon',
|
|
91
|
+
'Finch',
|
|
92
|
+
'Firefly',
|
|
93
|
+
'Fox',
|
|
94
|
+
'Gazelle',
|
|
95
|
+
'Gecko',
|
|
96
|
+
'Hare',
|
|
97
|
+
'Heron',
|
|
98
|
+
'Ibex',
|
|
99
|
+
'Jay',
|
|
100
|
+
'Kestrel',
|
|
101
|
+
'Kingfisher',
|
|
102
|
+
'Koala',
|
|
103
|
+
'Lark',
|
|
104
|
+
'Leopard',
|
|
105
|
+
'Lynx',
|
|
106
|
+
'Magpie',
|
|
107
|
+
'Mantis',
|
|
108
|
+
'Marten',
|
|
109
|
+
'Moose',
|
|
110
|
+
'Osprey',
|
|
111
|
+
'Otter',
|
|
112
|
+
'Owl',
|
|
113
|
+
'Panda',
|
|
114
|
+
'Panther',
|
|
115
|
+
'Pelican',
|
|
116
|
+
'Penguin',
|
|
117
|
+
'Puma',
|
|
118
|
+
'Quail',
|
|
119
|
+
'Rabbit',
|
|
120
|
+
'Raven',
|
|
121
|
+
'Robin',
|
|
122
|
+
'Sable',
|
|
123
|
+
'Salmon',
|
|
124
|
+
'Seal',
|
|
125
|
+
'Shark',
|
|
126
|
+
'Sparrow',
|
|
127
|
+
'Stag',
|
|
128
|
+
'Swan',
|
|
129
|
+
'Tiger',
|
|
130
|
+
'Tortoise',
|
|
131
|
+
'Trout',
|
|
132
|
+
'Turtle',
|
|
133
|
+
'Viper',
|
|
134
|
+
'Whale',
|
|
135
|
+
'Wolf',
|
|
136
|
+
'Wren',
|
|
137
|
+
'Yak',
|
|
138
|
+
] as const
|
|
139
|
+
|
|
140
|
+
function fnv1a64(value: string): bigint {
|
|
141
|
+
let hash = FNV1A_64_OFFSET
|
|
142
|
+
for (const byte of new TextEncoder().encode(value)) {
|
|
143
|
+
hash ^= BigInt(byte)
|
|
144
|
+
hash = BigInt.asUintN(64, hash * FNV1A_64_PRIME)
|
|
145
|
+
}
|
|
146
|
+
return hash
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Deterministic fallback for a session whose work has no semantic title. */
|
|
150
|
+
export function generatedSessionLabel(sessionId: string): string {
|
|
151
|
+
const hash = fnv1a64(sessionId)
|
|
152
|
+
const adjective = ADJECTIVES[Number(hash % WORD_COUNT)]
|
|
153
|
+
const noun = NOUNS[Number((hash >> 6n) % WORD_COUNT)]
|
|
154
|
+
return `${adjective} ${noun}`
|
|
155
|
+
}
|
package/src/hooks.test.ts
CHANGED
|
@@ -15,7 +15,6 @@ import os from 'node:os'
|
|
|
15
15
|
import path from 'node:path'
|
|
16
16
|
import { PassThrough } from 'node:stream'
|
|
17
17
|
import {
|
|
18
|
-
sessionLabelFromId,
|
|
19
18
|
type ListRepliesResponse,
|
|
20
19
|
type ReplyView,
|
|
21
20
|
type SubmissionReceipt,
|
|
@@ -3139,7 +3138,7 @@ describe('telling concurrent agents apart', () => {
|
|
|
3139
3138
|
question: 'Ship it?',
|
|
3140
3139
|
source: {
|
|
3141
3140
|
session_id: 'sess-abc',
|
|
3142
|
-
session_label:
|
|
3141
|
+
session_label: 'Semantic session',
|
|
3143
3142
|
harness: 'claude-code',
|
|
3144
3143
|
},
|
|
3145
3144
|
},
|
|
@@ -3150,7 +3149,7 @@ describe('telling concurrent agents apart', () => {
|
|
|
3150
3149
|
|
|
3151
3150
|
expect(h.recorder.submitted[0]?.draft.source).toEqual({
|
|
3152
3151
|
session_id: 'sess-abc',
|
|
3153
|
-
session_label:
|
|
3152
|
+
session_label: 'Semantic session',
|
|
3154
3153
|
harness: 'claude-code',
|
|
3155
3154
|
})
|
|
3156
3155
|
})
|
|
@@ -3164,7 +3163,7 @@ describe('telling concurrent agents apart', () => {
|
|
|
3164
3163
|
question: 'Ship it?',
|
|
3165
3164
|
source: {
|
|
3166
3165
|
session_id: 'sess-abc',
|
|
3167
|
-
session_label:
|
|
3166
|
+
session_label: 'Semantic session',
|
|
3168
3167
|
harness: 'claude-code',
|
|
3169
3168
|
},
|
|
3170
3169
|
},
|
|
@@ -3183,7 +3182,7 @@ describe('telling concurrent agents apart', () => {
|
|
|
3183
3182
|
const retirement = h.recorder.submitted.find((s) => s.draft.event === 'question_retired')
|
|
3184
3183
|
expect(retirement?.draft.source).toEqual({
|
|
3185
3184
|
session_id: 'sess-abc',
|
|
3186
|
-
session_label:
|
|
3185
|
+
session_label: 'Semantic session',
|
|
3187
3186
|
harness: 'claude-code',
|
|
3188
3187
|
})
|
|
3189
3188
|
})
|
|
@@ -838,11 +838,94 @@ describe('the OpenCode adapter', () => {
|
|
|
838
838
|
expect(source).not.toContain('synthetic: true')
|
|
839
839
|
})
|
|
840
840
|
|
|
841
|
-
it('injects
|
|
841
|
+
it('injects exact identity and the first-party OpenCode title into agent shells', () => {
|
|
842
842
|
expect(source).toContain('"shell.env"')
|
|
843
843
|
expect(source).toContain('NOTIFAI_ACTIVE_HARNESS')
|
|
844
844
|
expect(source).toContain('NOTIFAI_ACTIVE_SESSION_ID')
|
|
845
|
+
expect(source).toContain('NOTIFAI_ACTIVE_SESSION_LABEL')
|
|
846
|
+
expect(source).toContain('NOTIFAI_ACTIVE_SESSION_LABEL_PENDING')
|
|
845
847
|
expect(source).toContain('input?.sessionID')
|
|
848
|
+
expect(source).toContain('client.session.get({ path: { id: sessionID } })')
|
|
849
|
+
expect(source).toContain('response?.data?.title')
|
|
850
|
+
})
|
|
851
|
+
|
|
852
|
+
it('executes the generated adapter and publishes the SDK session title', async () => {
|
|
853
|
+
let requested = ''
|
|
854
|
+
const generated = (await import(
|
|
855
|
+
`data:text/javascript;base64,${Buffer.from(source).toString('base64')}`
|
|
856
|
+
)) as {
|
|
857
|
+
NotifAIPlugin(input: {
|
|
858
|
+
directory: string
|
|
859
|
+
client: {
|
|
860
|
+
session: {
|
|
861
|
+
get(input: { path: { id: string } }): Promise<{ data: { title: string } }>
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}): Promise<{
|
|
865
|
+
'shell.env'?: (
|
|
866
|
+
input: { sessionID?: string },
|
|
867
|
+
output: { env: Record<string, string> },
|
|
868
|
+
) => Promise<void>
|
|
869
|
+
}>
|
|
870
|
+
}
|
|
871
|
+
const plugin = await generated.NotifAIPlugin({
|
|
872
|
+
directory: '/repo',
|
|
873
|
+
client: {
|
|
874
|
+
session: {
|
|
875
|
+
get: async ({ path: request }) => {
|
|
876
|
+
requested = request.id
|
|
877
|
+
return { data: { title: 'Semantic session names' } }
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
},
|
|
881
|
+
})
|
|
882
|
+
const output = { env: {} as Record<string, string> }
|
|
883
|
+
await plugin['shell.env']?.({ sessionID: 'opencode-session' }, output)
|
|
884
|
+
|
|
885
|
+
expect(requested).toBe('opencode-session')
|
|
886
|
+
expect(output.env).toMatchObject({
|
|
887
|
+
NOTIFAI_ACTIVE_HARNESS: 'opencode',
|
|
888
|
+
NOTIFAI_ACTIVE_SESSION_ID: 'opencode-session',
|
|
889
|
+
NOTIFAI_ACTIVE_SESSION_LABEL: 'Semantic session names',
|
|
890
|
+
})
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
it('marks OpenCode placeholder titles as pending instead of publishing them', async () => {
|
|
894
|
+
const generated = (await import(
|
|
895
|
+
`data:text/javascript;base64,${Buffer.from(source).toString('base64')}`
|
|
896
|
+
)) as {
|
|
897
|
+
NotifAIPlugin(input: {
|
|
898
|
+
directory: string
|
|
899
|
+
client: {
|
|
900
|
+
session: {
|
|
901
|
+
get(input: { path: { id: string } }): Promise<{ data: { title: string } }>
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}): Promise<{
|
|
905
|
+
'shell.env'?: (
|
|
906
|
+
input: { sessionID?: string },
|
|
907
|
+
output: { env: Record<string, string> },
|
|
908
|
+
) => Promise<void>
|
|
909
|
+
}>
|
|
910
|
+
}
|
|
911
|
+
const plugin = await generated.NotifAIPlugin({
|
|
912
|
+
directory: '/repo',
|
|
913
|
+
client: {
|
|
914
|
+
session: {
|
|
915
|
+
get: async () => ({
|
|
916
|
+
data: { title: 'New session - 2026-08-20T13:05:00.000Z' },
|
|
917
|
+
}),
|
|
918
|
+
},
|
|
919
|
+
},
|
|
920
|
+
})
|
|
921
|
+
const output = { env: {} as Record<string, string> }
|
|
922
|
+
await plugin['shell.env']?.({ sessionID: 'opencode-session' }, output)
|
|
923
|
+
|
|
924
|
+
expect(output.env).toEqual({
|
|
925
|
+
NOTIFAI_ACTIVE_HARNESS: 'opencode',
|
|
926
|
+
NOTIFAI_ACTIVE_SESSION_ID: 'opencode-session',
|
|
927
|
+
NOTIFAI_ACTIVE_SESSION_LABEL_PENDING: '1',
|
|
928
|
+
})
|
|
846
929
|
})
|
|
847
930
|
|
|
848
931
|
it('carries the ownership marker so a second checkout replaces it', () => {
|