@raidiant/notifai 10.1.1 → 10.1.2

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 (156) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/commands-core.d.ts +1 -1
  3. package/dist/commands-init.js +9 -6
  4. package/dist/commands-init.js.map +1 -1
  5. package/dist/commands-send-support.js +3 -0
  6. package/dist/commands-send-support.js.map +1 -1
  7. package/dist/commands-skill.js +72 -54
  8. package/dist/commands-skill.js.map +1 -1
  9. package/dist/invocation-context.d.ts +1 -0
  10. package/dist/invocation-context.js +9 -1
  11. package/dist/invocation-context.js.map +1 -1
  12. package/dist/native-skills.d.ts +13 -3
  13. package/dist/native-skills.js +51 -12
  14. package/dist/native-skills.js.map +1 -1
  15. package/dist/node-floor.d.ts +2 -1
  16. package/dist/node-floor.js +9 -5
  17. package/dist/node-floor.js.map +1 -1
  18. package/dist/orca-session-title.d.ts +5 -4
  19. package/dist/orca-session-title.js +32 -19
  20. package/dist/orca-session-title.js.map +1 -1
  21. package/dist/platform.d.ts +11 -17
  22. package/dist/platform.js +20 -23
  23. package/dist/platform.js.map +1 -1
  24. package/dist/release.d.ts +5 -5
  25. package/dist/release.js +5 -5
  26. package/dist/setup-destinations.js +1 -1
  27. package/dist/setup-destinations.js.map +1 -1
  28. package/dist/skill-integrity.d.ts +49 -0
  29. package/dist/skill-integrity.js +154 -0
  30. package/dist/skill-integrity.js.map +1 -0
  31. package/dist/skill-source/manifest.json +16 -0
  32. package/dist/skill-source/notifai/SKILL.md +390 -0
  33. package/dist/skill-source/notifai/references/harness-setup.md +220 -0
  34. package/package.json +11 -14
  35. package/src/atomic-file.ts +0 -198
  36. package/src/claude-wake.test.ts +0 -321
  37. package/src/claude-wake.ts +0 -499
  38. package/src/cli-bin.test.ts +0 -176
  39. package/src/cli-bin.ts +0 -233
  40. package/src/cli-release.test.ts +0 -46
  41. package/src/cli-release.ts +0 -78
  42. package/src/cli-update-contract.ts +0 -9
  43. package/src/cli-update.test.ts +0 -125
  44. package/src/client.test.ts +0 -390
  45. package/src/client.ts +0 -352
  46. package/src/codex-wake.test.ts +0 -254
  47. package/src/codex-wake.ts +0 -301
  48. package/src/command-session.ts +0 -38
  49. package/src/commands-agent-sessions.ts +0 -72
  50. package/src/commands-auth.ts +0 -238
  51. package/src/commands-config.ts +0 -336
  52. package/src/commands-core.ts +0 -312
  53. package/src/commands-devices.ts +0 -180
  54. package/src/commands-doctor.ts +0 -1501
  55. package/src/commands-guidance.ts +0 -181
  56. package/src/commands-harness-context.ts +0 -207
  57. package/src/commands-hook-shape.ts +0 -69
  58. package/src/commands-hooks.ts +0 -2318
  59. package/src/commands-init.ts +0 -1055
  60. package/src/commands-io.ts +0 -108
  61. package/src/commands-logs.ts +0 -243
  62. package/src/commands-project.ts +0 -47
  63. package/src/commands-send-support.test.ts +0 -156
  64. package/src/commands-send-support.ts +0 -311
  65. package/src/commands-send.ts +0 -875
  66. package/src/commands-setup-proof.test.ts +0 -72
  67. package/src/commands-setup-proof.ts +0 -124
  68. package/src/commands-skill.test.ts +0 -44
  69. package/src/commands-skill.ts +0 -204
  70. package/src/commands-sounds.test.ts +0 -106
  71. package/src/commands-sounds.ts +0 -49
  72. package/src/commands-update.ts +0 -180
  73. package/src/commands.test.ts +0 -8297
  74. package/src/commands.ts +0 -77
  75. package/src/config-schema.test.ts +0 -153
  76. package/src/config-schema.ts +0 -349
  77. package/src/config.test.ts +0 -581
  78. package/src/config.ts +0 -475
  79. package/src/credentials.test.ts +0 -403
  80. package/src/credentials.ts +0 -403
  81. package/src/file-lock-process-worker.test.ts +0 -64
  82. package/src/file-lock.test.ts +0 -320
  83. package/src/file-lock.ts +0 -393
  84. package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
  85. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
  86. package/src/generated-session-label.ts +0 -155
  87. package/src/guidance-content.ts +0 -278
  88. package/src/guidance-render.ts +0 -56
  89. package/src/guidance.test.ts +0 -327
  90. package/src/guidance.ts +0 -233
  91. package/src/harness-integration.test.ts +0 -294
  92. package/src/harnesses.test.ts +0 -149
  93. package/src/harnesses.ts +0 -164
  94. package/src/hook-adapter.test.ts +0 -496
  95. package/src/hook-adapter.ts +0 -424
  96. package/src/hook-input.test.ts +0 -39
  97. package/src/hook-input.ts +0 -72
  98. package/src/hooks.test.ts +0 -5824
  99. package/src/hooks.ts +0 -4011
  100. package/src/install-hooks.test.ts +0 -1614
  101. package/src/install-hooks.ts +0 -1501
  102. package/src/interactive.test.ts +0 -387
  103. package/src/interactive.ts +0 -858
  104. package/src/invocation-context.test.ts +0 -352
  105. package/src/invocation-context.ts +0 -197
  106. package/src/logging-process-worker.test.ts +0 -45
  107. package/src/logging.test.ts +0 -544
  108. package/src/logging.ts +0 -698
  109. package/src/main-run.ts +0 -45
  110. package/src/main.ts +0 -12
  111. package/src/native-skills.test.ts +0 -115
  112. package/src/native-skills.ts +0 -162
  113. package/src/node-floor.test.ts +0 -27
  114. package/src/node-floor.ts +0 -37
  115. package/src/onboarding-docs.test.ts +0 -25
  116. package/src/openclaw-plugin.ts +0 -409
  117. package/src/opencode-plugin.ts +0 -298
  118. package/src/orca-session-title.test.ts +0 -54
  119. package/src/orca-session-title.ts +0 -103
  120. package/src/pack-feedback-slice.test.ts +0 -212
  121. package/src/pack-feedback-slice.ts +0 -187
  122. package/src/platform.test.ts +0 -152
  123. package/src/platform.ts +0 -173
  124. package/src/program.test.ts +0 -317
  125. package/src/program.ts +0 -842
  126. package/src/project-enablement.test.ts +0 -55
  127. package/src/project-enablement.ts +0 -99
  128. package/src/question-caps.test.ts +0 -49
  129. package/src/question-settlement-process.ts +0 -47
  130. package/src/question-timing.ts +0 -56
  131. package/src/readiness.test.ts +0 -25
  132. package/src/readiness.ts +0 -212
  133. package/src/release.ts +0 -51
  134. package/src/send-attempts.test.ts +0 -143
  135. package/src/send-attempts.ts +0 -188
  136. package/src/send.test.ts +0 -28
  137. package/src/send.ts +0 -436
  138. package/src/session-activation.ts +0 -61
  139. package/src/session-end-log-lock-worker.test.ts +0 -35
  140. package/src/session-labels.test.ts +0 -490
  141. package/src/session-labels.ts +0 -474
  142. package/src/setup-destinations.test.ts +0 -41
  143. package/src/setup-destinations.ts +0 -50
  144. package/src/skill.test.ts +0 -426
  145. package/src/sound-ref.test.ts +0 -31
  146. package/src/sound-ref.ts +0 -34
  147. package/src/trust-policy.test.ts +0 -701
  148. package/src/ui/banner.ts +0 -95
  149. package/src/ui/config-view.test.ts +0 -90
  150. package/src/ui/config-view.ts +0 -138
  151. package/src/ui/help.test.ts +0 -16
  152. package/src/ui/help.ts +0 -128
  153. package/src/ui/theme.test.ts +0 -92
  154. package/src/ui/theme.ts +0 -265
  155. package/src/url-policy.ts +0 -399
  156. 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
- }