@raidiant/notifai 7.0.0 → 7.0.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.
- package/CHANGELOG.md +18 -0
- 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-init.js +0 -1
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.js +20 -1
- package/dist/commands-send-support.js.map +1 -1
- package/dist/config-schema.js +1 -1
- package/dist/config-schema.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/package.json +1 -1
- package/src/commands-core.ts +24 -4
- package/src/commands-doctor.ts +7 -6
- package/src/commands-init.ts +0 -1
- package/src/commands-send-support.ts +25 -1
- package/src/commands.test.ts +129 -3
- package/src/config-schema.ts +1 -1
- package/src/orca-session-title.test.ts +95 -0
- package/src/orca-session-title.ts +105 -0
- package/src/session-labels.test.ts +21 -0
- package/src/skill.test.ts +16 -2
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const runOrcaCommand = (executable, args) => {
|
|
4
|
+
try {
|
|
5
|
+
return execFileSync(executable, args, {
|
|
6
|
+
encoding: 'utf8',
|
|
7
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
8
|
+
timeout: 2_000,
|
|
9
|
+
maxBuffer: 512 * 1024,
|
|
10
|
+
windowsHide: true,
|
|
11
|
+
}).trim();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
function containsControlCharacter(value) {
|
|
18
|
+
return Array.from(value).some((character) => {
|
|
19
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
20
|
+
return codePoint < 32 || codePoint === 127;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function worktreePathFromId(value) {
|
|
24
|
+
if (value.length === 0 || value.length > 4_096 || containsControlCharacter(value)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const separator = value.indexOf('::');
|
|
28
|
+
if (separator <= 0)
|
|
29
|
+
return null;
|
|
30
|
+
const repoId = value.slice(0, separator);
|
|
31
|
+
const worktreePath = value.slice(separator + 2);
|
|
32
|
+
if (!/^[A-Za-z0-9._-]+$/u.test(repoId) || worktreePath.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
return path.posix.isAbsolute(worktreePath) || path.win32.isAbsolute(worktreePath)
|
|
35
|
+
? worktreePath
|
|
36
|
+
: null;
|
|
37
|
+
}
|
|
38
|
+
function orcaExecutable(env) {
|
|
39
|
+
const configured = env['ORCA_CLI_COMMAND']?.trim();
|
|
40
|
+
if (configured !== undefined && configured.length > 0) {
|
|
41
|
+
return containsControlCharacter(configured) ? null : configured;
|
|
42
|
+
}
|
|
43
|
+
return env['ORCA_DEV_REPO_ROOT'] === undefined ? 'orca' : 'orca-dev';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read the semantic display name assigned to this exact Orca worktree.
|
|
47
|
+
*
|
|
48
|
+
* The environment contributes only an opaque selector. Orca must return that
|
|
49
|
+
* exact id and its matching path before its display name becomes trusted; no
|
|
50
|
+
* path component or identifier is ever promoted into the User-visible label.
|
|
51
|
+
*/
|
|
52
|
+
export function readOrcaSessionTitle(env, command = runOrcaCommand) {
|
|
53
|
+
if (env['TERM_PROGRAM'] !== 'Orca')
|
|
54
|
+
return undefined;
|
|
55
|
+
const worktreeId = env['ORCA_WORKTREE_ID'];
|
|
56
|
+
if (worktreeId === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
const expectedPath = worktreePathFromId(worktreeId);
|
|
59
|
+
if (expectedPath === null)
|
|
60
|
+
return undefined;
|
|
61
|
+
const executable = orcaExecutable(env);
|
|
62
|
+
if (executable === null)
|
|
63
|
+
return undefined;
|
|
64
|
+
let output;
|
|
65
|
+
try {
|
|
66
|
+
output = command(executable, [
|
|
67
|
+
'worktree',
|
|
68
|
+
'show',
|
|
69
|
+
'--worktree',
|
|
70
|
+
`id:${worktreeId}`,
|
|
71
|
+
'--json',
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
if (output === null || output === '')
|
|
78
|
+
return undefined;
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = JSON.parse(output);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
87
|
+
return undefined;
|
|
88
|
+
const response = parsed;
|
|
89
|
+
const worktree = response.result?.worktree;
|
|
90
|
+
if (response.ok !== true ||
|
|
91
|
+
worktree?.id !== worktreeId ||
|
|
92
|
+
worktree.path !== expectedPath ||
|
|
93
|
+
typeof worktree.displayName !== 'string' ||
|
|
94
|
+
worktree.displayName.trim().length === 0) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return worktree.displayName;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=orca-session-title.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orca-session-title.js","sourceRoot":"","sources":["../src/orca-session-title.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/commands-core.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { CredentialStore, MachineCredential } from './credentials.js'
|
|
|
13
13
|
import { type HookAdapterTarget } from './hook-adapter.js'
|
|
14
14
|
import { logConfigResolved, logSettingsFrom, nullLogger, type Logger } from './logging.js'
|
|
15
15
|
import type { NativeSkills } from './native-skills.js'
|
|
16
|
+
import type { OrcaSessionTitleLookup } from './orca-session-title.js'
|
|
16
17
|
import { packageVersion } from './release.js'
|
|
17
18
|
import type { Tone } from './ui/theme.js'
|
|
18
19
|
|
|
@@ -81,6 +82,8 @@ export interface CommandDeps {
|
|
|
81
82
|
codexSourcePid?: number
|
|
82
83
|
/** Test seam and production adapter for the external native skills installer. */
|
|
83
84
|
nativeSkills?: NativeSkills
|
|
85
|
+
/** Test seam for Orca's exact-worktree semantic title lookup. */
|
|
86
|
+
orcaSessionTitle?: OrcaSessionTitleLookup
|
|
84
87
|
/**
|
|
85
88
|
* The local record of what this invocation did. Optional so a test fake need
|
|
86
89
|
* not carry one; `log(deps)` supplies a logger that records nothing.
|
|
@@ -151,12 +154,29 @@ export function authedClient(deps: CommandDeps, config: CliConfig): { client: Ap
|
|
|
151
154
|
|
|
152
155
|
export const UPDATE_CLI_COMMAND = 'npm install -g @raidiant/notifai'
|
|
153
156
|
|
|
154
|
-
function
|
|
157
|
+
function affectedDeviceNames(details: unknown): string[] {
|
|
158
|
+
if (typeof details !== 'object' || details === null || !('device_names' in details)) return []
|
|
159
|
+
const names = (details as { device_names?: unknown }).device_names
|
|
160
|
+
if (!Array.isArray(names)) return []
|
|
161
|
+
return [...new Set(names.filter((name): name is string => typeof name === 'string' && name.trim() !== ''))]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function naturalList(values: readonly string[]): string {
|
|
165
|
+
if (values.length <= 1) return values[0] ?? ''
|
|
166
|
+
if (values.length === 2) return `${values[0]} and ${values[1]}`
|
|
167
|
+
return `${values.slice(0, -1).join(', ')}, and ${values.at(-1)}`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function localRecovery(action: RecoveryAction | null, details: unknown): string | null {
|
|
155
171
|
switch (action) {
|
|
156
172
|
case 'update_cli':
|
|
157
173
|
return `next: ${UPDATE_CLI_COMMAND}`
|
|
158
|
-
case 'update_companion':
|
|
159
|
-
|
|
174
|
+
case 'update_companion': {
|
|
175
|
+
const names = affectedDeviceNames(details)
|
|
176
|
+
return names.length > 0
|
|
177
|
+
? `next: Update Notifai on ${naturalList(names)}.`
|
|
178
|
+
: 'next: Update Notifai on the affected devices.'
|
|
179
|
+
}
|
|
160
180
|
case 'wait_for_service':
|
|
161
181
|
return 'next: The service is being updated; try again later.'
|
|
162
182
|
default:
|
|
@@ -213,7 +233,7 @@ export function reportError(
|
|
|
213
233
|
'disagree about the contract; check with `notifai doctor`',
|
|
214
234
|
)
|
|
215
235
|
}
|
|
216
|
-
const recovery = localRecovery(err.recoveryAction)
|
|
236
|
+
const recovery = localRecovery(err.recoveryAction, err.details)
|
|
217
237
|
if (recovery !== null) deps.io.err(recovery)
|
|
218
238
|
else if (err.recoveryAction === null && err.nextAction) deps.io.err(`next: ${err.nextAction}`)
|
|
219
239
|
if (err.code === 'auth_required' || err.code === 'machine_revoked') return EXIT.auth
|
package/src/commands-doctor.ts
CHANGED
|
@@ -589,13 +589,14 @@ export async function doctorCommand(
|
|
|
589
589
|
deps.io.out(`${mark} ${line(s)}`)
|
|
590
590
|
}
|
|
591
591
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
? 'Everything looks good'
|
|
595
|
-
: blocker?.id === 'contract'
|
|
596
|
-
? 'Update Notifai, then run doctor again'
|
|
597
|
-
: `Start with: ${remedyLine(blocker)}`,
|
|
592
|
+
const surfacedUpdate = readiness.states.some(
|
|
593
|
+
(state) => state.id === 'contract' && (state.status === 'gap' || state.status === 'optional-gap'),
|
|
598
594
|
)
|
|
595
|
+
if (blocker !== null && blocker.id !== 'contract') {
|
|
596
|
+
await deps.io.outro?.(`Start with: ${remedyLine(blocker)}`)
|
|
597
|
+
} else if (!surfacedUpdate) {
|
|
598
|
+
await deps.io.outro?.('Everything looks good')
|
|
599
|
+
}
|
|
599
600
|
return ok ? EXIT.ok : EXIT.failed
|
|
600
601
|
}
|
|
601
602
|
|
package/src/commands-init.ts
CHANGED
|
@@ -4,16 +4,40 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { isRetryableReplyPollError, type ApiClient } from './client.js'
|
|
6
6
|
import { buildSourceContext, inferInvocationContext } from './invocation-context.js'
|
|
7
|
+
import { readOrcaSessionTitle } from './orca-session-title.js'
|
|
7
8
|
import { type DraftInvocation, type SendFlags } from './send.js'
|
|
8
9
|
import { EXIT, reportError, type CommandDeps } from './commands-core.js'
|
|
9
10
|
import type { ActiveHarnessSession } from './commands-harness-context.js'
|
|
10
11
|
|
|
12
|
+
function managedSessionTitle(
|
|
13
|
+
deps: CommandDeps,
|
|
14
|
+
flags: Pick<SendFlags, 'sessionLabel'>,
|
|
15
|
+
active: ActiveHarnessSession | null,
|
|
16
|
+
): string | undefined {
|
|
17
|
+
if (active?.sessionLabel !== undefined) return active.sessionLabel
|
|
18
|
+
if (
|
|
19
|
+
active?.harness !== 'claude-code' ||
|
|
20
|
+
active.sessionId === undefined ||
|
|
21
|
+
active.sessionId !== deps.env['CLAUDE_CODE_SESSION_ID'] ||
|
|
22
|
+
flags.sessionLabel !== undefined ||
|
|
23
|
+
deps.env['NOTIFAI_SESSION_LABEL'] !== undefined
|
|
24
|
+
) {
|
|
25
|
+
return undefined
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return (deps.orcaSessionTitle ?? readOrcaSessionTitle)(deps.env)
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
11
34
|
export function resolveDraftInvocation(
|
|
12
35
|
deps: CommandDeps,
|
|
13
36
|
flags: Pick<SendFlags, 'sessionId' | 'sessionLabel'>,
|
|
14
37
|
active: ActiveHarnessSession | null,
|
|
15
38
|
): { ok: true; invocation: DraftInvocation } | { ok: false; error: string } {
|
|
16
39
|
const inferred = inferInvocationContext(deps.cwd)
|
|
40
|
+
const sessionTitle = managedSessionTitle(deps, flags, active)
|
|
17
41
|
const source = buildSourceContext({
|
|
18
42
|
env: deps.env,
|
|
19
43
|
invocation: inferred,
|
|
@@ -25,7 +49,7 @@ export function resolveDraftInvocation(
|
|
|
25
49
|
activeHarness: {
|
|
26
50
|
harness: active.harness,
|
|
27
51
|
...(active.sessionId === undefined ? {} : { sessionId: active.sessionId }),
|
|
28
|
-
...(
|
|
52
|
+
...(sessionTitle === undefined ? {} : { sessionLabel: sessionTitle }),
|
|
29
53
|
...(active.sessionLabelPending === true ? { sessionLabelPending: true } : {}),
|
|
30
54
|
},
|
|
31
55
|
}),
|
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'
|
|
@@ -505,6 +513,86 @@ describe('command contracts', () => {
|
|
|
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
|
+
|
|
508
596
|
it('uses the trusted OpenCode session title published by its managed adapter', async () => {
|
|
509
597
|
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-opencode-title-'))
|
|
510
598
|
const io = new CapturedIo()
|
|
@@ -2938,7 +3026,7 @@ describe('compatibility-first update guidance', () => {
|
|
|
2938
3026
|
listDevices: async () => ({ devices: [] }),
|
|
2939
3027
|
accessStatus: async () => ({ email: 'user@example.test' }),
|
|
2940
3028
|
} as unknown as ApiClient
|
|
2941
|
-
const humanIo = new
|
|
3029
|
+
const humanIo = new InteractiveIo()
|
|
2942
3030
|
const deps = {
|
|
2943
3031
|
...makeDeps(humanIo, client),
|
|
2944
3032
|
cwd,
|
|
@@ -2953,6 +3041,9 @@ describe('compatibility-first update guidance', () => {
|
|
|
2953
3041
|
).toBe(exit)
|
|
2954
3042
|
expect(humanIo.outLines).toEqual(humanLines)
|
|
2955
3043
|
expect(humanIo.errLines).toEqual([])
|
|
3044
|
+
expect(humanIo.outros).toEqual(
|
|
3045
|
+
status === 'ready' ? ['Everything looks good'] : [],
|
|
3046
|
+
)
|
|
2956
3047
|
|
|
2957
3048
|
const jsonIo = new CapturedIo()
|
|
2958
3049
|
expect(
|
|
@@ -3211,7 +3302,7 @@ describe('init', () => {
|
|
|
3211
3302
|
|
|
3212
3303
|
it('resumes the same init after a required CLI update becomes current', async () => {
|
|
3213
3304
|
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-init-update-resume-'))
|
|
3214
|
-
const io = new
|
|
3305
|
+
const io = new OutroCapturedIo()
|
|
3215
3306
|
let cliSupport = compatibilityWithCli({
|
|
3216
3307
|
state: 'must_update',
|
|
3217
3308
|
reason: 'minimum_not_met',
|
|
@@ -3246,11 +3337,13 @@ describe('init', () => {
|
|
|
3246
3337
|
expect(await initCommand(deps, { hooks: false, skills: false })).toBe(EXIT.failed)
|
|
3247
3338
|
expect(io.outLines).toContain("Notifai can't send notifications until you update.")
|
|
3248
3339
|
expect(io.outLines).toContain('npm install -g @raidiant/notifai')
|
|
3340
|
+
expect(io.outros).toEqual([])
|
|
3249
3341
|
expect(submissions).toBe(0)
|
|
3250
3342
|
|
|
3251
3343
|
cliSupport = currentCompatibility
|
|
3252
3344
|
io.outLines = []
|
|
3253
3345
|
io.errLines = []
|
|
3346
|
+
io.outros = []
|
|
3254
3347
|
|
|
3255
3348
|
expect(await initCommand(deps, { hooks: false, skills: false })).toBe(EXIT.ok)
|
|
3256
3349
|
expect(submissions).toBe(1)
|
|
@@ -4555,6 +4648,39 @@ describe('asking before the hooks have ever run', () => {
|
|
|
4555
4648
|
expect(readSessionState('codex-orchestrator', env).pending).toBeUndefined()
|
|
4556
4649
|
})
|
|
4557
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
|
+
|
|
4558
4684
|
it('judges the fired check against the active harness, not a global installation of another', async () => {
|
|
4559
4685
|
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-fired-active-harness-'))
|
|
4560
4686
|
const io = new CapturedIo()
|
|
@@ -5457,7 +5583,7 @@ describe('command failures carrying server details', () => {
|
|
|
5457
5583
|
|
|
5458
5584
|
expect(io.errLines).toEqual([
|
|
5459
5585
|
"feature_unavailable: The selected device can't answer questions.",
|
|
5460
|
-
'next: Update Notifai on
|
|
5586
|
+
'next: Update Notifai on Old phone.',
|
|
5461
5587
|
])
|
|
5462
5588
|
expect(io.errLines.join(' ')).not.toContain('untrusted')
|
|
5463
5589
|
expect(io.errLines.join(' ')).not.toContain('disagree about the contract')
|
package/src/config-schema.ts
CHANGED
|
@@ -199,7 +199,7 @@ const INFO: Record<ConfigKey, Omit<ConfigKeyInfo, 'key'>> = {
|
|
|
199
199
|
kind: 'string',
|
|
200
200
|
summary: 'Free text an agent reads before deciding whether to notify you',
|
|
201
201
|
detail:
|
|
202
|
-
'Your own guidance, in your own words, about what is worth a notification in this project — read by agents before they send. Something like "only tell me about failures and finished deploys; never per-file progress".\n\nIt is advice to the agent, not a filter the service enforces.',
|
|
202
|
+
'Your own guidance, in your own words, about what is worth a notification in this project — read by agents before they send. Something like "only tell me about failures and finished deploys; never per-file progress".\n\nIt is advice to the agent, not a filter the service enforces. Agents follow it as your literal standing word, so an agent may write it only when you ask for a preference to be remembered — and only in your words, never its own summary.',
|
|
203
203
|
unsetMeans: 'agents fall back to their default judgement',
|
|
204
204
|
example: 'Only failures and completed deploys',
|
|
205
205
|
},
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { readOrcaSessionTitle, type OrcaCommand } from './orca-session-title.js'
|
|
3
|
+
|
|
4
|
+
const worktreeId = 'repo-123::/worktrees/semantic-session-implementation'
|
|
5
|
+
|
|
6
|
+
function response(overrides: Record<string, unknown> = {}): string {
|
|
7
|
+
return JSON.stringify({
|
|
8
|
+
ok: true,
|
|
9
|
+
result: {
|
|
10
|
+
worktree: {
|
|
11
|
+
id: worktreeId,
|
|
12
|
+
path: '/worktrees/semantic-session-implementation',
|
|
13
|
+
displayName: 'Worker - semantic session implementation',
|
|
14
|
+
...overrides,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('Orca semantic session title', () => {
|
|
21
|
+
it('accepts only the display name returned for the exact managed worktree', () => {
|
|
22
|
+
const calls: { executable: string; args: readonly string[] }[] = []
|
|
23
|
+
const command: OrcaCommand = (executable, args) => {
|
|
24
|
+
calls.push({ executable, args })
|
|
25
|
+
return response()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
expect(
|
|
29
|
+
readOrcaSessionTitle(
|
|
30
|
+
{ TERM_PROGRAM: 'Orca', ORCA_WORKTREE_ID: worktreeId },
|
|
31
|
+
command,
|
|
32
|
+
),
|
|
33
|
+
).toBe('Worker - semantic session implementation')
|
|
34
|
+
expect(calls).toEqual([
|
|
35
|
+
{
|
|
36
|
+
executable: 'orca',
|
|
37
|
+
args: ['worktree', 'show', '--worktree', `id:${worktreeId}`, '--json'],
|
|
38
|
+
},
|
|
39
|
+
])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('uses Orca-provided executable selection without invoking a shell', () => {
|
|
43
|
+
const calls: string[] = []
|
|
44
|
+
expect(
|
|
45
|
+
readOrcaSessionTitle(
|
|
46
|
+
{
|
|
47
|
+
TERM_PROGRAM: 'Orca',
|
|
48
|
+
ORCA_WORKTREE_ID: worktreeId,
|
|
49
|
+
ORCA_CLI_COMMAND: '/mnt/c/Program Files/Orca/orca.exe',
|
|
50
|
+
},
|
|
51
|
+
(executable) => {
|
|
52
|
+
calls.push(executable)
|
|
53
|
+
return response()
|
|
54
|
+
},
|
|
55
|
+
),
|
|
56
|
+
).toBe('Worker - semantic session implementation')
|
|
57
|
+
expect(calls).toEqual(['/mnt/c/Program Files/Orca/orca.exe'])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('ignores non-Orca and malformed selectors without running a command', () => {
|
|
61
|
+
const command: OrcaCommand = () => {
|
|
62
|
+
throw new Error('must not run')
|
|
63
|
+
}
|
|
64
|
+
expect(readOrcaSessionTitle({ ORCA_WORKTREE_ID: worktreeId }, command)).toBeUndefined()
|
|
65
|
+
expect(
|
|
66
|
+
readOrcaSessionTitle(
|
|
67
|
+
{ TERM_PROGRAM: 'Orca', ORCA_WORKTREE_ID: '/private/untrusted' },
|
|
68
|
+
command,
|
|
69
|
+
),
|
|
70
|
+
).toBeUndefined()
|
|
71
|
+
expect(
|
|
72
|
+
readOrcaSessionTitle(
|
|
73
|
+
{ TERM_PROGRAM: 'Orca', ORCA_WORKTREE_ID: 'repo-123::relative/path' },
|
|
74
|
+
command,
|
|
75
|
+
),
|
|
76
|
+
).toBeUndefined()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it.each([
|
|
80
|
+
['command failure', () => null],
|
|
81
|
+
['thrown command failure', () => { throw new Error('offline') }],
|
|
82
|
+
['malformed JSON', () => '{not json}'],
|
|
83
|
+
['failed response', () => JSON.stringify({ ok: false })],
|
|
84
|
+
['different worktree id', () => response({ id: 'repo-123::/worktrees/other' })],
|
|
85
|
+
['different worktree path', () => response({ path: '/private/untrusted' })],
|
|
86
|
+
['blank display name', () => response({ displayName: ' ' })],
|
|
87
|
+
])('fails closed for %s', (_name, command) => {
|
|
88
|
+
expect(
|
|
89
|
+
readOrcaSessionTitle(
|
|
90
|
+
{ TERM_PROGRAM: 'Orca', ORCA_WORKTREE_ID: worktreeId },
|
|
91
|
+
command,
|
|
92
|
+
),
|
|
93
|
+
).toBeUndefined()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export type OrcaSessionTitleLookup = (env: NodeJS.ProcessEnv) => string | undefined
|
|
5
|
+
export type OrcaCommand = (executable: string, args: readonly string[]) => string | null
|
|
6
|
+
|
|
7
|
+
const runOrcaCommand: OrcaCommand = (executable, args) => {
|
|
8
|
+
try {
|
|
9
|
+
return execFileSync(executable, args, {
|
|
10
|
+
encoding: 'utf8',
|
|
11
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
12
|
+
timeout: 2_000,
|
|
13
|
+
maxBuffer: 512 * 1024,
|
|
14
|
+
windowsHide: true,
|
|
15
|
+
}).trim()
|
|
16
|
+
} catch {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function containsControlCharacter(value: string): boolean {
|
|
22
|
+
return Array.from(value).some((character) => {
|
|
23
|
+
const codePoint = character.codePointAt(0) ?? 0
|
|
24
|
+
return codePoint < 32 || codePoint === 127
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function worktreePathFromId(value: string): string | null {
|
|
29
|
+
if (value.length === 0 || value.length > 4_096 || containsControlCharacter(value)) {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
const separator = value.indexOf('::')
|
|
33
|
+
if (separator <= 0) return null
|
|
34
|
+
const repoId = value.slice(0, separator)
|
|
35
|
+
const worktreePath = value.slice(separator + 2)
|
|
36
|
+
if (!/^[A-Za-z0-9._-]+$/u.test(repoId) || worktreePath.length === 0) return null
|
|
37
|
+
return path.posix.isAbsolute(worktreePath) || path.win32.isAbsolute(worktreePath)
|
|
38
|
+
? worktreePath
|
|
39
|
+
: null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function orcaExecutable(env: NodeJS.ProcessEnv): string | null {
|
|
43
|
+
const configured = env['ORCA_CLI_COMMAND']?.trim()
|
|
44
|
+
if (configured !== undefined && configured.length > 0) {
|
|
45
|
+
return containsControlCharacter(configured) ? null : configured
|
|
46
|
+
}
|
|
47
|
+
return env['ORCA_DEV_REPO_ROOT'] === undefined ? 'orca' : 'orca-dev'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read the semantic display name assigned to this exact Orca worktree.
|
|
52
|
+
*
|
|
53
|
+
* The environment contributes only an opaque selector. Orca must return that
|
|
54
|
+
* exact id and its matching path before its display name becomes trusted; no
|
|
55
|
+
* path component or identifier is ever promoted into the User-visible label.
|
|
56
|
+
*/
|
|
57
|
+
export function readOrcaSessionTitle(
|
|
58
|
+
env: NodeJS.ProcessEnv,
|
|
59
|
+
command: OrcaCommand = runOrcaCommand,
|
|
60
|
+
): string | undefined {
|
|
61
|
+
if (env['TERM_PROGRAM'] !== 'Orca') return undefined
|
|
62
|
+
const worktreeId = env['ORCA_WORKTREE_ID']
|
|
63
|
+
if (worktreeId === undefined) return undefined
|
|
64
|
+
const expectedPath = worktreePathFromId(worktreeId)
|
|
65
|
+
if (expectedPath === null) return undefined
|
|
66
|
+
const executable = orcaExecutable(env)
|
|
67
|
+
if (executable === null) return undefined
|
|
68
|
+
|
|
69
|
+
let output: string | null
|
|
70
|
+
try {
|
|
71
|
+
output = command(executable, [
|
|
72
|
+
'worktree',
|
|
73
|
+
'show',
|
|
74
|
+
'--worktree',
|
|
75
|
+
`id:${worktreeId}`,
|
|
76
|
+
'--json',
|
|
77
|
+
])
|
|
78
|
+
} catch {
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
if (output === null || output === '') return undefined
|
|
82
|
+
|
|
83
|
+
let parsed: unknown
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(output)
|
|
86
|
+
} catch {
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
if (typeof parsed !== 'object' || parsed === null) return undefined
|
|
90
|
+
const response = parsed as {
|
|
91
|
+
ok?: unknown
|
|
92
|
+
result?: { worktree?: { id?: unknown; path?: unknown; displayName?: unknown } }
|
|
93
|
+
}
|
|
94
|
+
const worktree = response.result?.worktree
|
|
95
|
+
if (
|
|
96
|
+
response.ok !== true ||
|
|
97
|
+
worktree?.id !== worktreeId ||
|
|
98
|
+
worktree.path !== expectedPath ||
|
|
99
|
+
typeof worktree.displayName !== 'string' ||
|
|
100
|
+
worktree.displayName.trim().length === 0
|
|
101
|
+
) {
|
|
102
|
+
return undefined
|
|
103
|
+
}
|
|
104
|
+
return worktree.displayName
|
|
105
|
+
}
|