@raidiant/notifai 10.1.1 → 10.1.3

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 (168) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/commands-core.d.ts +4 -1
  3. package/dist/commands-core.js.map +1 -1
  4. package/dist/commands-doctor.js +2 -7
  5. package/dist/commands-doctor.js.map +1 -1
  6. package/dist/commands-hooks.d.ts +12 -0
  7. package/dist/commands-hooks.js +15 -1
  8. package/dist/commands-hooks.js.map +1 -1
  9. package/dist/commands-init.js +9 -6
  10. package/dist/commands-init.js.map +1 -1
  11. package/dist/commands-send-support.js +10 -11
  12. package/dist/commands-send-support.js.map +1 -1
  13. package/dist/commands-skill.js +72 -54
  14. package/dist/commands-skill.js.map +1 -1
  15. package/dist/harness-session-title.d.ts +22 -0
  16. package/dist/harness-session-title.js +74 -0
  17. package/dist/harness-session-title.js.map +1 -0
  18. package/dist/invocation-context.d.ts +1 -0
  19. package/dist/invocation-context.js +15 -1
  20. package/dist/invocation-context.js.map +1 -1
  21. package/dist/native-skills.d.ts +13 -3
  22. package/dist/native-skills.js +51 -12
  23. package/dist/native-skills.js.map +1 -1
  24. package/dist/node-floor.d.ts +2 -1
  25. package/dist/node-floor.js +9 -5
  26. package/dist/node-floor.js.map +1 -1
  27. package/dist/orca-session-title.d.ts +5 -4
  28. package/dist/orca-session-title.js +32 -19
  29. package/dist/orca-session-title.js.map +1 -1
  30. package/dist/platform.d.ts +11 -17
  31. package/dist/platform.js +20 -23
  32. package/dist/platform.js.map +1 -1
  33. package/dist/release.d.ts +5 -5
  34. package/dist/release.js +5 -5
  35. package/dist/session-labels.d.ts +2 -0
  36. package/dist/session-labels.js +20 -9
  37. package/dist/session-labels.js.map +1 -1
  38. package/dist/setup-destinations.js +1 -1
  39. package/dist/setup-destinations.js.map +1 -1
  40. package/dist/skill-integrity.d.ts +49 -0
  41. package/dist/skill-integrity.js +154 -0
  42. package/dist/skill-integrity.js.map +1 -0
  43. package/dist/skill-source/manifest.json +16 -0
  44. package/dist/skill-source/notifai/SKILL.md +390 -0
  45. package/dist/skill-source/notifai/references/harness-setup.md +220 -0
  46. package/package.json +11 -14
  47. package/src/atomic-file.ts +0 -198
  48. package/src/claude-wake.test.ts +0 -321
  49. package/src/claude-wake.ts +0 -499
  50. package/src/cli-bin.test.ts +0 -176
  51. package/src/cli-bin.ts +0 -233
  52. package/src/cli-release.test.ts +0 -46
  53. package/src/cli-release.ts +0 -78
  54. package/src/cli-update-contract.ts +0 -9
  55. package/src/cli-update.test.ts +0 -125
  56. package/src/client.test.ts +0 -390
  57. package/src/client.ts +0 -352
  58. package/src/codex-wake.test.ts +0 -254
  59. package/src/codex-wake.ts +0 -301
  60. package/src/command-session.ts +0 -38
  61. package/src/commands-agent-sessions.ts +0 -72
  62. package/src/commands-auth.ts +0 -238
  63. package/src/commands-config.ts +0 -336
  64. package/src/commands-core.ts +0 -312
  65. package/src/commands-devices.ts +0 -180
  66. package/src/commands-doctor.ts +0 -1501
  67. package/src/commands-guidance.ts +0 -181
  68. package/src/commands-harness-context.ts +0 -207
  69. package/src/commands-hook-shape.ts +0 -69
  70. package/src/commands-hooks.ts +0 -2318
  71. package/src/commands-init.ts +0 -1055
  72. package/src/commands-io.ts +0 -108
  73. package/src/commands-logs.ts +0 -243
  74. package/src/commands-project.ts +0 -47
  75. package/src/commands-send-support.test.ts +0 -156
  76. package/src/commands-send-support.ts +0 -311
  77. package/src/commands-send.ts +0 -875
  78. package/src/commands-setup-proof.test.ts +0 -72
  79. package/src/commands-setup-proof.ts +0 -124
  80. package/src/commands-skill.test.ts +0 -44
  81. package/src/commands-skill.ts +0 -204
  82. package/src/commands-sounds.test.ts +0 -106
  83. package/src/commands-sounds.ts +0 -49
  84. package/src/commands-update.ts +0 -180
  85. package/src/commands.test.ts +0 -8297
  86. package/src/commands.ts +0 -77
  87. package/src/config-schema.test.ts +0 -153
  88. package/src/config-schema.ts +0 -349
  89. package/src/config.test.ts +0 -581
  90. package/src/config.ts +0 -475
  91. package/src/credentials.test.ts +0 -403
  92. package/src/credentials.ts +0 -403
  93. package/src/file-lock-process-worker.test.ts +0 -64
  94. package/src/file-lock.test.ts +0 -320
  95. package/src/file-lock.ts +0 -393
  96. package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
  97. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
  98. package/src/generated-session-label.ts +0 -155
  99. package/src/guidance-content.ts +0 -278
  100. package/src/guidance-render.ts +0 -56
  101. package/src/guidance.test.ts +0 -327
  102. package/src/guidance.ts +0 -233
  103. package/src/harness-integration.test.ts +0 -294
  104. package/src/harnesses.test.ts +0 -149
  105. package/src/harnesses.ts +0 -164
  106. package/src/hook-adapter.test.ts +0 -496
  107. package/src/hook-adapter.ts +0 -424
  108. package/src/hook-input.test.ts +0 -39
  109. package/src/hook-input.ts +0 -72
  110. package/src/hooks.test.ts +0 -5824
  111. package/src/hooks.ts +0 -4011
  112. package/src/install-hooks.test.ts +0 -1614
  113. package/src/install-hooks.ts +0 -1501
  114. package/src/interactive.test.ts +0 -387
  115. package/src/interactive.ts +0 -858
  116. package/src/invocation-context.test.ts +0 -352
  117. package/src/invocation-context.ts +0 -197
  118. package/src/logging-process-worker.test.ts +0 -45
  119. package/src/logging.test.ts +0 -544
  120. package/src/logging.ts +0 -698
  121. package/src/main-run.ts +0 -45
  122. package/src/main.ts +0 -12
  123. package/src/native-skills.test.ts +0 -115
  124. package/src/native-skills.ts +0 -162
  125. package/src/node-floor.test.ts +0 -27
  126. package/src/node-floor.ts +0 -37
  127. package/src/onboarding-docs.test.ts +0 -25
  128. package/src/openclaw-plugin.ts +0 -409
  129. package/src/opencode-plugin.ts +0 -298
  130. package/src/orca-session-title.test.ts +0 -54
  131. package/src/orca-session-title.ts +0 -103
  132. package/src/pack-feedback-slice.test.ts +0 -212
  133. package/src/pack-feedback-slice.ts +0 -187
  134. package/src/platform.test.ts +0 -152
  135. package/src/platform.ts +0 -173
  136. package/src/program.test.ts +0 -317
  137. package/src/program.ts +0 -842
  138. package/src/project-enablement.test.ts +0 -55
  139. package/src/project-enablement.ts +0 -99
  140. package/src/question-caps.test.ts +0 -49
  141. package/src/question-settlement-process.ts +0 -47
  142. package/src/question-timing.ts +0 -56
  143. package/src/readiness.test.ts +0 -25
  144. package/src/readiness.ts +0 -212
  145. package/src/release.ts +0 -51
  146. package/src/send-attempts.test.ts +0 -143
  147. package/src/send-attempts.ts +0 -188
  148. package/src/send.test.ts +0 -28
  149. package/src/send.ts +0 -436
  150. package/src/session-activation.ts +0 -61
  151. package/src/session-end-log-lock-worker.test.ts +0 -35
  152. package/src/session-labels.test.ts +0 -490
  153. package/src/session-labels.ts +0 -474
  154. package/src/setup-destinations.test.ts +0 -41
  155. package/src/setup-destinations.ts +0 -50
  156. package/src/skill.test.ts +0 -426
  157. package/src/sound-ref.test.ts +0 -31
  158. package/src/sound-ref.ts +0 -34
  159. package/src/trust-policy.test.ts +0 -701
  160. package/src/ui/banner.ts +0 -95
  161. package/src/ui/config-view.test.ts +0 -90
  162. package/src/ui/config-view.ts +0 -138
  163. package/src/ui/help.test.ts +0 -16
  164. package/src/ui/help.ts +0 -128
  165. package/src/ui/theme.test.ts +0 -92
  166. package/src/ui/theme.ts +0 -265
  167. package/src/url-policy.ts +0 -399
  168. package/tsconfig.json +0 -10
@@ -1,352 +0,0 @@
1
- import { mkdtempSync } from 'node:fs'
2
- import os from 'node:os'
3
- import path from 'node:path'
4
- import { describe, expect, it } from 'vitest'
5
- import {
6
- buildSourceContext,
7
- inferInvocationContext,
8
- portableBasename,
9
- projectSlugFrom,
10
- truncateContext,
11
- type GitCommand,
12
- } from './invocation-context.js'
13
-
14
- function fixtureGit(values: Record<string, string | null>): GitCommand {
15
- return (_cwd, args) => values[args.join(' ')] ?? null
16
- }
17
-
18
- function stateEnv(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
19
- return {
20
- XDG_STATE_HOME: mkdtempSync(path.join(os.tmpdir(), 'notifai-source-context-')),
21
- ...overrides,
22
- }
23
- }
24
-
25
- describe('projectSlugFrom', () => {
26
- it.each([
27
- ['My Project', 'my-project'],
28
- ['.Hidden Repo', 'hidden-repo'],
29
- ['API_v2.0', 'api_v2.0'],
30
- ['---', null],
31
- ['项目', null],
32
- ])('turns %s into %s', (name, expected) => {
33
- expect(projectSlugFrom(name)).toBe(expected)
34
- })
35
-
36
- it('keeps the Project identifier inside its 64-character bound', () => {
37
- expect(projectSlugFrom(`repo-${'x'.repeat(100)}`)).toHaveLength(64)
38
- })
39
- })
40
-
41
- describe('portableBasename', () => {
42
- it.each([
43
- ['/Users/person/code/notifai/', 'notifai'],
44
- ['C:\\Users\\Person\\code\\notifai\\', 'notifai'],
45
- ['\\\\server\\share\\worktrees\\release\\', 'release'],
46
- ])('extracts the basename from %s', (value, expected) => {
47
- expect(portableBasename(value)).toBe(expected)
48
- })
49
- })
50
-
51
- describe('inferInvocationContext', () => {
52
- it('uses the common-directory parent for a repository from a nested cwd', () => {
53
- const context = inferInvocationContext(
54
- '/code/Signal Garden/packages/api',
55
- fixtureGit({
56
- 'rev-parse --git-common-dir': '../../.git',
57
- 'rev-parse --git-dir': '../../.git',
58
- 'rev-parse --show-toplevel': '/code/Signal Garden',
59
- 'rev-parse --abbrev-ref HEAD': 'feature/alerts',
60
- }),
61
- )
62
- expect(context).toEqual({ project: 'signal-garden', branch: 'feature/alerts' })
63
- })
64
-
65
- it('keeps one Project across linked worktrees and exposes only the worktree basename', () => {
66
- const context = inferInvocationContext(
67
- '/Users/name/worktrees/notifai-release/apps/cli',
68
- fixtureGit({
69
- 'rev-parse --git-common-dir': '/Users/name/code/notifai/.git',
70
- 'rev-parse --git-dir': '/Users/name/code/notifai/.git/worktrees/notifai-release',
71
- 'rev-parse --show-toplevel': '/Users/name/worktrees/notifai-release/',
72
- 'rev-parse --abbrev-ref HEAD': 'release/content',
73
- }),
74
- )
75
- expect(context).toEqual({
76
- project: 'notifai',
77
- branch: 'release/content',
78
- worktree: 'notifai-release',
79
- })
80
- expect(JSON.stringify(context)).not.toContain('/Users/name')
81
- })
82
-
83
- it('handles drive-letter paths and compares them case-insensitively', () => {
84
- const context = inferInvocationContext(
85
- 'D:\\Worktrees\\Alerts\\src',
86
- fixtureGit({
87
- 'rev-parse --git-common-dir': 'C:\\Code\\Notifai\\.git\\',
88
- 'rev-parse --git-dir': 'c:\\code\\notifai\\.git\\worktrees\\Alerts',
89
- 'rev-parse --show-toplevel': 'D:\\Worktrees\\Alerts\\',
90
- 'rev-parse --abbrev-ref HEAD': 'feature/windows',
91
- }),
92
- )
93
- expect(context).toEqual({
94
- project: 'notifai',
95
- branch: 'feature/windows',
96
- worktree: 'Alerts',
97
- })
98
- expect(JSON.stringify(context)).not.toContain('D:\\Worktrees')
99
- expect(JSON.stringify(context)).not.toContain('C:\\Code')
100
- })
101
-
102
- it('handles a UNC common directory and trailing separators', () => {
103
- const context = inferInvocationContext(
104
- '\\\\server\\share\\trees\\topic\\src',
105
- fixtureGit({
106
- 'rev-parse --git-common-dir': '\\\\server\\share\\Notifai\\.git\\',
107
- 'rev-parse --git-dir': '\\\\server\\share\\Notifai\\.git\\worktrees\\topic\\',
108
- 'rev-parse --show-toplevel': '\\\\server\\share\\trees\\topic\\',
109
- 'rev-parse --abbrev-ref HEAD': 'topic',
110
- }),
111
- )
112
- expect(context).toEqual({ project: 'notifai', branch: 'topic', worktree: 'topic' })
113
- })
114
-
115
- it('falls back to the cwd basename outside Git', () => {
116
- expect(inferInvocationContext('/tmp/My Scratch/', fixtureGit({}))).toEqual({
117
- project: 'my-scratch',
118
- })
119
- })
120
-
121
- it('omits an unusable Project instead of inventing one', () => {
122
- expect(inferInvocationContext('/tmp/项目', fixtureGit({}))).toEqual({ project: null })
123
- })
124
-
125
- it('omits branch and worktree on detached HEAD in an ordinary checkout', () => {
126
- expect(
127
- inferInvocationContext(
128
- '/code/repo',
129
- fixtureGit({
130
- 'rev-parse --git-common-dir': '.git',
131
- 'rev-parse --git-dir': '.git',
132
- 'rev-parse --show-toplevel': '/code/repo',
133
- 'rev-parse --abbrev-ref HEAD': 'HEAD',
134
- }),
135
- ),
136
- ).toEqual({ project: 'repo' })
137
- })
138
-
139
- it('bounds branch and worktree display strings without leaking their parents', () => {
140
- const longBranch = `feature/${'界'.repeat(140)}`
141
- const longWorktree = `tree-${'x'.repeat(80)}`
142
- const context = inferInvocationContext(
143
- '/private/work/tree/src',
144
- fixtureGit({
145
- 'rev-parse --git-common-dir': '/private/main/repo/.git',
146
- 'rev-parse --git-dir': '/private/main/repo/.git/worktrees/tree',
147
- 'rev-parse --show-toplevel': `/private/work/${longWorktree}`,
148
- 'rev-parse --abbrev-ref HEAD': longBranch,
149
- }),
150
- )
151
- expect(Array.from(context.branch ?? '')).toHaveLength(128)
152
- expect(Array.from(context.worktree ?? '')).toHaveLength(64)
153
- expect(context.branch).toMatch(/…$/)
154
- expect(context.worktree).toMatch(/…$/)
155
- expect(JSON.stringify(context)).not.toContain('/private/')
156
- })
157
- })
158
-
159
- describe('truncateContext', () => {
160
- it('counts Unicode characters rather than UTF-16 code units', () => {
161
- expect(truncateContext('A😀BC', 3)).toBe('A😀…')
162
- })
163
- })
164
-
165
- describe('buildSourceContext', () => {
166
- const invocation = { project: 'repo', branch: 'main', worktree: 'topic' }
167
- const now = new Date(2026, 7, 20, 14, 5).getTime()
168
-
169
- it('applies flag, environment, then inferred precedence per session field', () => {
170
- const built = buildSourceContext({
171
- env: stateEnv({
172
- NOTIFAI_SESSION_ID: 'env-id',
173
- NOTIFAI_SESSION_LABEL: 'Environment Label',
174
- }),
175
- invocation,
176
- sessionId: 'flag-id',
177
- sessionLabel: 'Flag Label',
178
- activeHarness: { harness: 'claude-code', sessionId: 'flag-id' },
179
- now,
180
- })
181
- expect(built).toEqual({
182
- ok: true,
183
- source: {
184
- session_id: 'flag-id',
185
- session_label: 'Flag Label',
186
- harness: 'claude-code',
187
- branch: 'main',
188
- worktree: 'topic',
189
- },
190
- })
191
- })
192
-
193
- it('uses environment values ahead of an inferred exact session', () => {
194
- expect(
195
- buildSourceContext({
196
- env: stateEnv({
197
- NOTIFAI_SESSION_ID: 'env-id',
198
- NOTIFAI_SESSION_LABEL: 'Environment Label',
199
- }),
200
- invocation: { project: 'repo' },
201
- activeHarness: { harness: 'codex', sessionId: 'env-id' },
202
- now,
203
- }),
204
- ).toEqual({
205
- ok: true,
206
- source: {
207
- session_id: 'env-id',
208
- session_label: 'Environment Label',
209
- harness: 'codex',
210
- },
211
- })
212
- })
213
-
214
- it('uses and freezes a trusted harness-native title', () => {
215
- const env = stateEnv()
216
- const sessionId = 'opencode-thread'
217
- expect(
218
- buildSourceContext({
219
- env,
220
- invocation: { project: 'repo', branch: 'main' },
221
- activeHarness: {
222
- harness: 'opencode',
223
- sessionId,
224
- sessionLabel: 'Semantic session names',
225
- },
226
- now,
227
- }),
228
- ).toEqual({
229
- ok: true,
230
- source: {
231
- session_id: sessionId,
232
- session_label: 'Semantic session names',
233
- harness: 'opencode',
234
- branch: 'main',
235
- },
236
- })
237
- expect(
238
- buildSourceContext({
239
- env,
240
- invocation: { project: 'repo' },
241
- activeHarness: { harness: 'opencode', sessionId, sessionLabel: 'Changed title' },
242
- now: now + 1_000,
243
- }),
244
- ).toEqual({
245
- ok: true,
246
- source: {
247
- session_id: sessionId,
248
- session_label: 'Semantic session names',
249
- harness: 'opencode',
250
- },
251
- })
252
- })
253
-
254
- it('does not freeze an OpenCode placeholder while its semantic title is pending', () => {
255
- expect(
256
- buildSourceContext({
257
- env: stateEnv(),
258
- invocation: { project: 'repo' },
259
- activeHarness: {
260
- harness: 'opencode',
261
- sessionId: 'pending-session',
262
- sessionLabelPending: true,
263
- },
264
- now,
265
- }),
266
- ).toEqual({
267
- ok: false,
268
- error:
269
- "OpenCode is still generating this session's title; retry shortly or pass --session-label.",
270
- })
271
- })
272
-
273
- it('uses a stable generated fallback when no semantic name exists', () => {
274
- const env = stateEnv()
275
- const sessionId = 'opaque-thread-1234567890'
276
- const expected = {
277
- ok: true,
278
- source: {
279
- session_id: sessionId,
280
- session_label: 'Ivory Koala',
281
- harness: 'opencode' as const,
282
- branch: 'main',
283
- },
284
- }
285
- expect(
286
- buildSourceContext({
287
- env,
288
- invocation: { project: 'repo', branch: 'main' },
289
- activeHarness: { harness: 'opencode', sessionId },
290
- now,
291
- }),
292
- ).toEqual(expected)
293
- expect(expected.source.session_label).not.toContain('1234567890')
294
- })
295
-
296
- it('does not borrow harness identity or title for an explicit different session id', () => {
297
- expect(
298
- buildSourceContext({
299
- env: stateEnv(),
300
- invocation: { project: 'repo' },
301
- sessionId: 'other-session',
302
- activeHarness: {
303
- harness: 'claude-code',
304
- sessionId: 'current-session',
305
- sessionLabel: 'Current work',
306
- },
307
- now,
308
- }),
309
- ).toEqual({
310
- ok: true,
311
- source: {
312
- session_id: 'other-session',
313
- session_label: 'Golden Lynx',
314
- },
315
- })
316
- })
317
-
318
- it('does not emit a harness or label when no exact session id is available', () => {
319
- expect(
320
- buildSourceContext({
321
- env: stateEnv(),
322
- invocation: { project: 'repo', branch: 'main' },
323
- activeHarness: { harness: 'cursor' },
324
- now,
325
- }),
326
- ).toEqual({ ok: true, source: { branch: 'main' } })
327
- })
328
-
329
- it('ignores the deleted NOTIFAI_SESSION variable', () => {
330
- expect(
331
- buildSourceContext({
332
- env: stateEnv({ NOTIFAI_SESSION: 'legacy-id' }),
333
- invocation: { project: 'repo' },
334
- now,
335
- }),
336
- ).toEqual({ ok: true })
337
- })
338
-
339
- it('rejects a display label without an exact session id', () => {
340
- expect(
341
- buildSourceContext({
342
- env: stateEnv(),
343
- invocation: { project: 'repo' },
344
- sessionLabel: 'Unbound Label',
345
- now,
346
- }),
347
- ).toEqual({
348
- ok: false,
349
- error: '--session-label (or NOTIFAI_SESSION_LABEL) needs an exact Agent Session id.',
350
- })
351
- })
352
- })
@@ -1,197 +0,0 @@
1
- import { execFileSync } from 'node:child_process'
2
- import path from 'node:path'
3
- import { type SourceContextT } from '@raidiant/notifai-protocol'
4
- import type { SourceContextHarness } from './harnesses.js'
5
- import { resolveSessionLabel } from './session-labels.js'
6
-
7
- export type GitCommand = (cwd: string, args: readonly string[]) => string | null
8
-
9
- const runGit: GitCommand = (cwd, args) => {
10
- try {
11
- return execFileSync('git', ['-C', cwd, ...args], {
12
- encoding: 'utf8',
13
- stdio: ['ignore', 'pipe', 'ignore'],
14
- timeout: 2_000,
15
- windowsHide: true,
16
- }).trim()
17
- } catch {
18
- return null
19
- }
20
- }
21
-
22
- /** The useful, privacy-safe context inferred from one invocation directory. */
23
- export interface InvocationContext {
24
- project: string | null
25
- branch?: string
26
- worktree?: string
27
- }
28
-
29
- /** Contract-valid Project slug, or null when the name has no safe characters. */
30
- export function projectSlugFrom(name: string): string | null {
31
- const slug = name
32
- .toLowerCase()
33
- .replaceAll(/[^a-z0-9._-]+/g, '-')
34
- .replaceAll(/^[^a-z0-9]+|[^a-z0-9._-]+$/g, '')
35
- .slice(0, 64)
36
- return slug.length > 0 && /^[a-z0-9]/.test(slug) ? slug : null
37
- }
38
-
39
- function usesWindowsPaths(...values: string[]): boolean {
40
- return values.some((value) => /^[A-Za-z]:[\\/]/.test(value) || /^\\\\/.test(value))
41
- }
42
-
43
- function pathApi(...values: string[]): typeof path.posix | typeof path.win32 {
44
- return usesWindowsPaths(...values) ? path.win32 : path.posix
45
- }
46
-
47
- /** Basename that can be fixture-tested for POSIX, drive, and UNC paths on any OS. */
48
- export function portableBasename(value: string): string {
49
- const api = pathApi(value)
50
- return api.basename(api.normalize(value))
51
- }
52
-
53
- function resolveGitPath(cwd: string, value: string): string {
54
- const api = pathApi(cwd, value)
55
- return api.isAbsolute(value) ? api.normalize(value) : api.resolve(cwd, value)
56
- }
57
-
58
- function samePath(left: string, right: string): boolean {
59
- const api = pathApi(left, right)
60
- const normalizedLeft = api.normalize(left)
61
- const normalizedRight = api.normalize(right)
62
- return api === path.win32
63
- ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
64
- : normalizedLeft === normalizedRight
65
- }
66
-
67
- /** Truncate a display string by Unicode characters and keep the ellipsis inside the bound. */
68
- export function truncateContext(value: string, maxLength: number): string {
69
- const characters = Array.from(value)
70
- if (characters.length <= maxLength) return value
71
- return `${characters.slice(0, Math.max(0, maxLength - 1)).join('')}…`
72
- }
73
-
74
- /**
75
- * Infer stable Project identity plus branch/worktree display context.
76
- *
77
- * Linked worktrees share the parent of Git's common directory, while a non-Git
78
- * directory falls back to its own basename. Only basenames and branch names are
79
- * returned; no absolute path can leave this function.
80
- */
81
- export function inferInvocationContext(
82
- cwd: string,
83
- git: GitCommand = runGit,
84
- ): InvocationContext {
85
- const commonRaw = git(cwd, ['rev-parse', '--git-common-dir'])
86
- if (commonRaw === null || commonRaw === '') {
87
- return { project: projectSlugFrom(portableBasename(cwd)) }
88
- }
89
-
90
- const commonDir = resolveGitPath(cwd, commonRaw)
91
- const api = pathApi(cwd, commonDir)
92
- const gitDirRaw = git(cwd, ['rev-parse', '--git-dir'])
93
- const topLevelRaw = git(cwd, ['rev-parse', '--show-toplevel'])
94
- const linked =
95
- gitDirRaw !== null &&
96
- gitDirRaw !== '' &&
97
- !samePath(resolveGitPath(cwd, gitDirRaw), commonDir)
98
-
99
- // In an ordinary checkout the worktree root is the useful Project root. A
100
- // linked worktree needs its shared repository identity instead: standard Git
101
- // stores that in <main>/.git, while submodules and --separate-git-dir use a
102
- // named common directory whose basename is the only stable, path-free label.
103
- // Treating every common directory as `.git` would infer `modules` or `public`
104
- // for submodules — an implementation detail, not the repository's identity.
105
- const commonName = api.basename(commonDir)
106
- const projectName = linked
107
- ? commonName.toLowerCase() === '.git'
108
- ? api.basename(api.dirname(commonDir))
109
- : commonName
110
- : topLevelRaw !== null && topLevelRaw !== ''
111
- ? portableBasename(topLevelRaw)
112
- : commonName.toLowerCase() === '.git'
113
- ? api.basename(api.dirname(commonDir))
114
- : commonName
115
- const project = projectSlugFrom(projectName)
116
-
117
- const branchRaw = git(cwd, ['rev-parse', '--abbrev-ref', 'HEAD'])
118
- const branch =
119
- branchRaw === null || branchRaw === '' || branchRaw === 'HEAD'
120
- ? undefined
121
- : truncateContext(branchRaw, 128)
122
-
123
- const worktree =
124
- linked && topLevelRaw !== null && topLevelRaw !== ''
125
- ? truncateContext(portableBasename(topLevelRaw), 64)
126
- : undefined
127
-
128
- return {
129
- project,
130
- ...(branch !== undefined ? { branch } : {}),
131
- ...(worktree !== undefined && worktree !== '' ? { worktree } : {}),
132
- }
133
- }
134
-
135
- export interface SourceContextInput {
136
- env: NodeJS.ProcessEnv
137
- invocation: InvocationContext
138
- sessionId?: string
139
- sessionLabel?: string
140
- activeHarness?: {
141
- harness: SourceContextHarness
142
- sessionId?: string
143
- sessionLabel?: string
144
- sessionLabelPending?: boolean
145
- }
146
- now?: number
147
- }
148
-
149
- export type SourceContextBuild =
150
- | { ok: true; source?: SourceContextT }
151
- | { ok: false; error: string }
152
-
153
- /** Resolve per-field Source Context precedence without fabricating Agent Session identity. */
154
- export function buildSourceContext(input: SourceContextInput): SourceContextBuild {
155
- const sessionId =
156
- input.sessionId ?? input.env['NOTIFAI_SESSION_ID'] ?? input.activeHarness?.sessionId
157
- const explicitLabel = input.sessionLabel ?? input.env['NOTIFAI_SESSION_LABEL']
158
- if (explicitLabel !== undefined && (sessionId === undefined || sessionId === '')) {
159
- return {
160
- ok: false,
161
- error: '--session-label (or NOTIFAI_SESSION_LABEL) needs an exact Agent Session id.',
162
- }
163
- }
164
- if (sessionId === '') {
165
- return { ok: false, error: '--session-id must not be empty.' }
166
- }
167
-
168
- const activeOwnsSession =
169
- sessionId !== undefined && input.activeHarness?.sessionId === sessionId
170
- const harness = activeOwnsSession ? input.activeHarness?.harness : undefined
171
- const label =
172
- sessionId === undefined
173
- ? undefined
174
- : resolveSessionLabel({
175
- env: input.env,
176
- sessionId,
177
- ...(harness === undefined ? {} : { harness }),
178
- ...(explicitLabel === undefined ? {} : { explicitLabel }),
179
- ...(activeOwnsSession && input.activeHarness?.sessionLabel !== undefined
180
- ? { harnessLabel: input.activeHarness.sessionLabel }
181
- : {}),
182
- ...(activeOwnsSession && input.activeHarness?.sessionLabelPending === true
183
- ? { harnessLabelPending: true }
184
- : {}),
185
- ...(input.now === undefined ? {} : { now: input.now }),
186
- })
187
- if (label !== undefined && !label.ok) return label
188
-
189
- const source: SourceContextT = {
190
- ...(sessionId !== undefined ? { session_id: sessionId } : {}),
191
- ...(label !== undefined ? { session_label: label.label } : {}),
192
- ...(harness !== undefined ? { harness } : {}),
193
- ...(input.invocation.branch !== undefined ? { branch: input.invocation.branch } : {}),
194
- ...(input.invocation.worktree !== undefined ? { worktree: input.invocation.worktree } : {}),
195
- }
196
- return Object.keys(source).length === 0 ? { ok: true } : { ok: true, source }
197
- }
@@ -1,45 +0,0 @@
1
- import { existsSync, writeFileSync } from 'node:fs'
2
- import { describe, expect, it } from 'vitest'
3
- import { createLogger } from './logging.js'
4
-
5
- const worker = process.env['NOTIFAI_ROTATION_WORKER']
6
-
7
- if (worker === undefined) {
8
- describe('rotation process worker', () => {
9
- it('stays dormant outside the concurrency regression', () => {
10
- expect(worker).toBeUndefined()
11
- })
12
- })
13
- } else {
14
- describe('rotation process worker', () => {
15
- it('writes one share of an overlapping log stream', async () => {
16
- const readyPath = process.env['NOTIFAI_ROTATION_READY']!
17
- const startPath = process.env['NOTIFAI_ROTATION_START']!
18
- const records = Number(process.env['NOTIFAI_ROTATION_RECORDS'])
19
- const maxBytes = Number(process.env['NOTIFAI_ROTATION_MAX_BYTES'])
20
- const maxFiles = Number(process.env['NOTIFAI_ROTATION_MAX_FILES'])
21
- writeFileSync(readyPath, 'ready')
22
- // The parent's absolute deadline, not a countdown of this worker's own:
23
- // the barrier fails for everyone at one instant, so a worker that started
24
- // early cannot walk out on a parent still waiting for its last sibling.
25
- const startDeadline = Number(process.env['NOTIFAI_ROTATION_DEADLINE'])
26
- while (!existsSync(startPath)) {
27
- if (Date.now() >= startDeadline) throw new Error('rotation parent never released the barrier')
28
- await new Promise((resolve) => setTimeout(resolve, 2))
29
- }
30
-
31
- const logger = createLogger({
32
- runId: `r_worker_${worker}`,
33
- settings: { maxBytes, maxFiles },
34
- })
35
- for (let sequence = 0; sequence < records; sequence += 1) {
36
- logger.info('cli.end', { worker, sequence, pad: 'p'.repeat(140) })
37
- await new Promise((resolve) => setTimeout(resolve, 1))
38
- }
39
- // The sink switches itself off for the rest of a process the first time a
40
- // write throws, so this is the whole point of the worker: every record it
41
- // was given reached the shared file through the cross-process lock.
42
- expect(logger.enabled, 'the shared-log sink switched itself off mid-run').toBe(true)
43
- })
44
- })
45
- }