@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/atomic-file.js +13 -0
  3. package/dist/atomic-file.js.map +1 -1
  4. package/dist/commands-core.d.ts +3 -0
  5. package/dist/commands-core.js +23 -4
  6. package/dist/commands-core.js.map +1 -1
  7. package/dist/commands-doctor.js +7 -5
  8. package/dist/commands-doctor.js.map +1 -1
  9. package/dist/commands-harness-context.d.ts +4 -0
  10. package/dist/commands-harness-context.js +4 -0
  11. package/dist/commands-harness-context.js.map +1 -1
  12. package/dist/commands-init.js +0 -1
  13. package/dist/commands-init.js.map +1 -1
  14. package/dist/commands-send-support.js +22 -0
  15. package/dist/commands-send-support.js.map +1 -1
  16. package/dist/config.d.ts +3 -3
  17. package/dist/config.js +3 -3
  18. package/dist/generated-session-label.d.ts +2 -0
  19. package/dist/generated-session-label.js +152 -0
  20. package/dist/generated-session-label.js.map +1 -0
  21. package/dist/invocation-context.d.ts +3 -0
  22. package/dist/invocation-context.js +23 -6
  23. package/dist/invocation-context.js.map +1 -1
  24. package/dist/main.js +2 -2
  25. package/dist/main.js.map +1 -1
  26. package/dist/opencode-plugin.js +18 -3
  27. package/dist/opencode-plugin.js.map +1 -1
  28. package/dist/orca-session-title.d.ts +10 -0
  29. package/dist/orca-session-title.js +99 -0
  30. package/dist/orca-session-title.js.map +1 -0
  31. package/dist/session-labels.d.ts +30 -0
  32. package/dist/session-labels.js +278 -0
  33. package/dist/session-labels.js.map +1 -0
  34. package/package.json +2 -2
  35. package/src/atomic-file.ts +12 -0
  36. package/src/commands-core.ts +24 -4
  37. package/src/commands-doctor.ts +7 -6
  38. package/src/commands-harness-context.ts +8 -0
  39. package/src/commands-init.ts +0 -1
  40. package/src/commands-send-support.ts +27 -0
  41. package/src/commands.test.ts +203 -6
  42. package/src/config.ts +3 -3
  43. package/src/generated-session-label.ts +155 -0
  44. package/src/hooks.test.ts +4 -5
  45. package/src/install-hooks.test.ts +84 -1
  46. package/src/invocation-context.test.ts +115 -16
  47. package/src/invocation-context.ts +32 -11
  48. package/src/main.ts +8 -2
  49. package/src/opencode-plugin.ts +18 -3
  50. package/src/orca-session-title.test.ts +95 -0
  51. package/src/orca-session-title.ts +105 -0
  52. package/src/session-labels.test.ts +324 -0
  53. package/src/session-labels.ts +360 -0
  54. package/src/skill.test.ts +15 -0
@@ -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).toMatch(/^[A-Z][a-z]+ [A-Z][a-z]+$/)
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(".option('--session-label")
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 PlainInteractiveIo()
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 CapturedIo()
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
- ).toMatch(/^[A-Z][a-z]+ [A-Z][a-z]+$/)
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 the named device.',
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, and
192
- * session config overrides. Separate from the config dir because none of it is
193
- * user-authored and none of it should ever be synced or committed.
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: sessionLabelFromId('sess-abc'),
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: sessionLabelFromId('sess-abc'),
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: sessionLabelFromId('sess-abc'),
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: sessionLabelFromId('sess-abc'),
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 an exact active-harness and session marker into OpenCode shells', () => {
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', () => {