@raidiant/notifai 10.1.0 → 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.
- package/CHANGELOG.md +37 -0
- package/README.md +3 -0
- package/dist/cli-bin.d.ts +21 -1
- package/dist/cli-bin.js +141 -10
- package/dist/cli-bin.js.map +1 -1
- package/dist/cli-update-contract.d.ts +7 -0
- package/dist/cli-update-contract.js +10 -0
- package/dist/cli-update-contract.js.map +1 -0
- package/dist/client.d.ts +6 -1
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -1
- package/dist/codex-wake.d.ts +2 -0
- package/dist/codex-wake.js +2 -1
- package/dist/codex-wake.js.map +1 -1
- package/dist/commands-auth.d.ts +15 -1
- package/dist/commands-auth.js +29 -11
- package/dist/commands-auth.js.map +1 -1
- package/dist/commands-core.d.ts +9 -2
- package/dist/commands-core.js +11 -4
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-devices.d.ts +10 -1
- package/dist/commands-devices.js +17 -13
- package/dist/commands-devices.js.map +1 -1
- package/dist/commands-doctor.js +93 -49
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-hooks.js +47 -10
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +16 -0
- package/dist/commands-init.js +254 -93
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.js +3 -0
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-send.d.ts +1 -1
- package/dist/commands-send.js +121 -35
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-skill.js +72 -54
- package/dist/commands-skill.js.map +1 -1
- package/dist/commands-update.d.ts +10 -0
- package/dist/commands-update.js +141 -0
- package/dist/commands-update.js.map +1 -0
- package/dist/commands.d.ts +2 -1
- package/dist/commands.js +2 -1
- package/dist/commands.js.map +1 -1
- package/dist/hook-adapter.d.ts +2 -0
- package/dist/hook-adapter.js +12 -1
- package/dist/hook-adapter.js.map +1 -1
- package/dist/hooks.d.ts +43 -2
- package/dist/hooks.js +163 -21
- package/dist/hooks.js.map +1 -1
- package/dist/invocation-context.d.ts +1 -0
- package/dist/invocation-context.js +9 -1
- package/dist/invocation-context.js.map +1 -1
- package/dist/main-run.d.ts +1 -0
- package/dist/main-run.js +42 -0
- package/dist/main-run.js.map +1 -0
- package/dist/main.js +10 -31
- package/dist/main.js.map +1 -1
- package/dist/native-skills.d.ts +13 -3
- package/dist/native-skills.js +51 -12
- package/dist/native-skills.js.map +1 -1
- package/dist/node-floor.d.ts +19 -0
- package/dist/node-floor.js +39 -0
- package/dist/node-floor.js.map +1 -0
- package/dist/orca-session-title.d.ts +5 -4
- package/dist/orca-session-title.js +32 -19
- package/dist/orca-session-title.js.map +1 -1
- package/dist/platform.d.ts +11 -17
- package/dist/platform.js +20 -23
- package/dist/platform.js.map +1 -1
- package/dist/program.d.ts +2 -1
- package/dist/program.js +22 -5
- package/dist/program.js.map +1 -1
- package/dist/question-settlement-process.d.ts +15 -0
- package/dist/question-settlement-process.js +35 -0
- package/dist/question-settlement-process.js.map +1 -0
- package/dist/readiness.d.ts +25 -1
- package/dist/readiness.js +32 -2
- package/dist/readiness.js.map +1 -1
- package/dist/release.d.ts +5 -5
- package/dist/release.js +5 -5
- package/dist/setup-destinations.d.ts +23 -0
- package/dist/setup-destinations.js +44 -0
- package/dist/setup-destinations.js.map +1 -0
- package/dist/skill-integrity.d.ts +49 -0
- package/dist/skill-integrity.js +154 -0
- package/dist/skill-integrity.js.map +1 -0
- package/dist/skill-source/manifest.json +16 -0
- package/dist/skill-source/notifai/SKILL.md +390 -0
- package/dist/skill-source/notifai/references/harness-setup.md +220 -0
- package/package.json +11 -14
- package/src/atomic-file.ts +0 -198
- package/src/claude-wake.test.ts +0 -321
- package/src/claude-wake.ts +0 -499
- package/src/cli-bin.test.ts +0 -38
- package/src/cli-bin.ts +0 -67
- package/src/cli-release.test.ts +0 -46
- package/src/cli-release.ts +0 -78
- package/src/client.test.ts +0 -390
- package/src/client.ts +0 -345
- package/src/codex-wake.test.ts +0 -254
- package/src/codex-wake.ts +0 -297
- package/src/command-session.ts +0 -38
- package/src/commands-agent-sessions.ts +0 -72
- package/src/commands-auth.ts +0 -205
- package/src/commands-config.ts +0 -336
- package/src/commands-core.ts +0 -298
- package/src/commands-devices.test.ts +0 -15
- package/src/commands-devices.ts +0 -173
- package/src/commands-doctor.ts +0 -1440
- package/src/commands-guidance.ts +0 -181
- package/src/commands-harness-context.ts +0 -207
- package/src/commands-hook-shape.ts +0 -69
- package/src/commands-hooks.ts +0 -2268
- package/src/commands-init.ts +0 -878
- package/src/commands-io.ts +0 -108
- package/src/commands-logs.ts +0 -243
- package/src/commands-project.ts +0 -47
- package/src/commands-send-support.test.ts +0 -156
- package/src/commands-send-support.ts +0 -311
- package/src/commands-send.ts +0 -769
- package/src/commands-setup-proof.test.ts +0 -72
- package/src/commands-setup-proof.ts +0 -124
- package/src/commands-skill.test.ts +0 -44
- package/src/commands-skill.ts +0 -204
- package/src/commands-sounds.test.ts +0 -106
- package/src/commands-sounds.ts +0 -49
- package/src/commands.test.ts +0 -7828
- package/src/commands.ts +0 -75
- package/src/config-schema.test.ts +0 -153
- package/src/config-schema.ts +0 -349
- package/src/config.test.ts +0 -581
- package/src/config.ts +0 -475
- package/src/credentials.test.ts +0 -403
- package/src/credentials.ts +0 -403
- package/src/file-lock-process-worker.test.ts +0 -64
- package/src/file-lock.test.ts +0 -320
- package/src/file-lock.ts +0 -393
- package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
- package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
- package/src/generated-session-label.ts +0 -155
- package/src/guidance-content.ts +0 -278
- package/src/guidance-render.ts +0 -56
- package/src/guidance.test.ts +0 -327
- package/src/guidance.ts +0 -233
- package/src/harness-integration.test.ts +0 -294
- package/src/harnesses.test.ts +0 -149
- package/src/harnesses.ts +0 -164
- package/src/hook-adapter.test.ts +0 -496
- package/src/hook-adapter.ts +0 -410
- package/src/hook-input.test.ts +0 -39
- package/src/hook-input.ts +0 -72
- package/src/hooks.test.ts +0 -5549
- package/src/hooks.ts +0 -3759
- package/src/install-hooks.test.ts +0 -1614
- package/src/install-hooks.ts +0 -1501
- package/src/interactive.test.ts +0 -387
- package/src/interactive.ts +0 -858
- package/src/invocation-context.test.ts +0 -352
- package/src/invocation-context.ts +0 -197
- package/src/logging-process-worker.test.ts +0 -45
- package/src/logging.test.ts +0 -544
- package/src/logging.ts +0 -698
- package/src/main.ts +0 -36
- package/src/native-skills.test.ts +0 -115
- package/src/native-skills.ts +0 -162
- package/src/onboarding-docs.test.ts +0 -25
- package/src/openclaw-plugin.ts +0 -409
- package/src/opencode-plugin.ts +0 -298
- package/src/orca-session-title.test.ts +0 -54
- package/src/orca-session-title.ts +0 -103
- package/src/pack-feedback-slice.test.ts +0 -212
- package/src/pack-feedback-slice.ts +0 -187
- package/src/platform.test.ts +0 -152
- package/src/platform.ts +0 -173
- package/src/program.test.ts +0 -282
- package/src/program.ts +0 -818
- package/src/project-enablement.test.ts +0 -55
- package/src/project-enablement.ts +0 -99
- package/src/question-caps.test.ts +0 -49
- package/src/question-timing.ts +0 -56
- package/src/readiness.test.ts +0 -25
- package/src/readiness.ts +0 -179
- package/src/release.ts +0 -51
- package/src/send-attempts.test.ts +0 -143
- package/src/send-attempts.ts +0 -188
- package/src/send.test.ts +0 -28
- package/src/send.ts +0 -436
- package/src/session-activation.ts +0 -61
- package/src/session-end-log-lock-worker.test.ts +0 -35
- package/src/session-labels.test.ts +0 -490
- package/src/session-labels.ts +0 -474
- package/src/skill.test.ts +0 -415
- package/src/sound-ref.test.ts +0 -31
- package/src/sound-ref.ts +0 -34
- package/src/trust-policy.test.ts +0 -701
- package/src/ui/banner.ts +0 -95
- package/src/ui/config-view.test.ts +0 -90
- package/src/ui/config-view.ts +0 -138
- package/src/ui/help.test.ts +0 -16
- package/src/ui/help.ts +0 -128
- package/src/ui/theme.test.ts +0 -92
- package/src/ui/theme.ts +0 -265
- package/src/url-policy.ts +0 -399
- package/tsconfig.json +0 -10
package/src/guidance.ts
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
closeSync,
|
|
3
|
-
constants,
|
|
4
|
-
existsSync,
|
|
5
|
-
fstatSync,
|
|
6
|
-
lstatSync,
|
|
7
|
-
openSync,
|
|
8
|
-
readSync,
|
|
9
|
-
readdirSync,
|
|
10
|
-
realpathSync,
|
|
11
|
-
type Dirent,
|
|
12
|
-
} from 'node:fs'
|
|
13
|
-
import path from 'node:path'
|
|
14
|
-
import { globalConfigDir, personalProjectIdentity } from './config.js'
|
|
15
|
-
import { SHIPPED_GUIDANCE } from './guidance-content.js'
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Layered notification-writing guidance with provenance. Most specific wins,
|
|
19
|
-
* per topic, exactly the discipline config keys follow:
|
|
20
|
-
*
|
|
21
|
-
* project-local > project > global > shipped default
|
|
22
|
-
*
|
|
23
|
-
* A topic is one Markdown file named `<topic>.md`. A user file replaces the
|
|
24
|
-
* shipped topic of the same name wholesale — their words outrank the default,
|
|
25
|
-
* not annotate it — and a file whose name matches no shipped topic is purely
|
|
26
|
-
* additive house rules. There is no session layer: an instruction given in
|
|
27
|
-
* conversation tunes the session by being followed, not persisted.
|
|
28
|
-
*
|
|
29
|
-
* Two of those layers are the User's own word and one is not. `global` and
|
|
30
|
-
* `project-local` live under the User's config home: only they can write
|
|
31
|
-
* there. `project` is `.notifai/guidance` inside the repository, so it
|
|
32
|
-
* arrives with a clone and is written by whoever wrote the repository —
|
|
33
|
-
* which may be nobody the User has ever met. It remains authoritative about
|
|
34
|
-
* *how this project's notifications should read*, because shared house rules
|
|
35
|
-
* are the feature; it is never the User speaking. `authority` carries that
|
|
36
|
-
* distinction to every reader so a cloned repository cannot borrow the
|
|
37
|
-
* User's voice.
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
export type GuidanceSource =
|
|
41
|
-
| `project-local:${string}`
|
|
42
|
-
| `project:${string}`
|
|
43
|
-
| `global:${string}`
|
|
44
|
-
| 'default'
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Who is speaking in a topic.
|
|
48
|
-
*
|
|
49
|
-
* - `user` — the User's own standing word, from a location only they can write.
|
|
50
|
-
* - `repository` — this checkout's shared house rules; committed content, and
|
|
51
|
-
* therefore untrusted input that speaks for the project, not the User.
|
|
52
|
-
* - `shipped` — the default that applies when nobody said otherwise.
|
|
53
|
-
*/
|
|
54
|
-
export type GuidanceAuthority = 'user' | 'repository' | 'shipped'
|
|
55
|
-
|
|
56
|
-
export interface ResolvedGuidanceTopic {
|
|
57
|
-
name: string
|
|
58
|
-
source: GuidanceSource
|
|
59
|
-
authority: GuidanceAuthority
|
|
60
|
-
/** One line for lists; user-authored topics describe themselves by content. */
|
|
61
|
-
summary: string
|
|
62
|
-
content: string
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function guidanceAuthority(source: GuidanceSource): GuidanceAuthority {
|
|
66
|
-
if (source === 'default') return 'shipped'
|
|
67
|
-
return source.startsWith('project:') ? 'repository' : 'user'
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Topic names double as filenames, and the project layer arrives from a
|
|
72
|
-
* repository — untrusted input. Anything that does not match is not a topic,
|
|
73
|
-
* the same way `coerce` drops an unrecognised config value.
|
|
74
|
-
*/
|
|
75
|
-
export const GUIDANCE_TOPIC_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* One topic is capped at the body limit. Guidance is read into agent context
|
|
79
|
-
* on every session, and a repository could otherwise feed it without bound.
|
|
80
|
-
*/
|
|
81
|
-
export const GUIDANCE_TOPIC_MAX_BYTES = 16_000
|
|
82
|
-
|
|
83
|
-
export function globalGuidanceDir(env: NodeJS.ProcessEnv = process.env): string {
|
|
84
|
-
return path.join(globalConfigDir(env), 'guidance')
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function isWithin(root: string, candidate: string): boolean {
|
|
88
|
-
const relative = path.relative(root, candidate)
|
|
89
|
-
return relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`))
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Walk up from cwd looking for `.notifai/guidance` (shared, committed). */
|
|
93
|
-
export function findProjectGuidanceDir(startDir: string): string | null {
|
|
94
|
-
let dir = path.resolve(startDir)
|
|
95
|
-
for (;;) {
|
|
96
|
-
const candidate = path.join(dir, '.notifai', 'guidance')
|
|
97
|
-
if (existsSync(candidate)) {
|
|
98
|
-
try {
|
|
99
|
-
// A clone may contain symlinks. Repository guidance may read only
|
|
100
|
-
// regular content rooted in that repository; it must never turn a
|
|
101
|
-
// local private file into model context by pointing outside the tree.
|
|
102
|
-
const candidateStat = lstatSync(candidate)
|
|
103
|
-
const repositoryRoot = realpathSync(dir)
|
|
104
|
-
const resolved = realpathSync(candidate)
|
|
105
|
-
if (candidateStat.isDirectory() && isWithin(repositoryRoot, resolved)) return candidate
|
|
106
|
-
} catch {
|
|
107
|
-
// An unreadable or unstable candidate is not guidance. Keep walking.
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const parent = path.dirname(dir)
|
|
111
|
-
if (parent === dir) return null
|
|
112
|
-
dir = parent
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Personal project guidance lives beside the personal project config file,
|
|
118
|
-
* under the user's config home rather than the repository:
|
|
119
|
-
* `$XDG_CONFIG_HOME/notifai/projects/<identity>/guidance`.
|
|
120
|
-
*/
|
|
121
|
-
export function personalProjectGuidanceDir(
|
|
122
|
-
cwd: string,
|
|
123
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
124
|
-
): string {
|
|
125
|
-
return path.join(globalConfigDir(env), 'projects', personalProjectIdentity(cwd), 'guidance')
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function topicsInDir(dir: string): Map<string, string> {
|
|
129
|
-
const topics = new Map<string, string>()
|
|
130
|
-
if (!existsSync(dir)) return topics
|
|
131
|
-
let entries: Dirent[]
|
|
132
|
-
try {
|
|
133
|
-
if (!lstatSync(dir).isDirectory()) return topics
|
|
134
|
-
entries = readdirSync(dir, { withFileTypes: true })
|
|
135
|
-
} catch {
|
|
136
|
-
return topics
|
|
137
|
-
}
|
|
138
|
-
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
139
|
-
// In particular, refuse symlinks. A committed link to a User-owned file
|
|
140
|
-
// must not make that file part of repository guidance output.
|
|
141
|
-
if (!entry.isFile() || !entry.name.endsWith('.md')) continue
|
|
142
|
-
const name = entry.name.slice(0, -'.md'.length)
|
|
143
|
-
if (!GUIDANCE_TOPIC_PATTERN.test(name)) continue
|
|
144
|
-
topics.set(name, path.join(dir, entry.name))
|
|
145
|
-
}
|
|
146
|
-
return topics
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function readTopicFile(filePath: string): string | null {
|
|
150
|
-
let descriptor: number | null = null
|
|
151
|
-
try {
|
|
152
|
-
descriptor = openSync(filePath, constants.O_RDONLY | constants.O_NOFOLLOW)
|
|
153
|
-
if (!fstatSync(descriptor).isFile()) return null
|
|
154
|
-
const bytes = Buffer.alloc(GUIDANCE_TOPIC_MAX_BYTES + 1)
|
|
155
|
-
let offset = 0
|
|
156
|
-
while (offset < bytes.length) {
|
|
157
|
-
const count = readSync(descriptor, bytes, offset, bytes.length - offset, offset)
|
|
158
|
-
if (count === 0) break
|
|
159
|
-
offset += count
|
|
160
|
-
}
|
|
161
|
-
const truncated = offset > GUIDANCE_TOPIC_MAX_BYTES
|
|
162
|
-
const content = bytes.subarray(0, Math.min(offset, GUIDANCE_TOPIC_MAX_BYTES)).toString('utf8')
|
|
163
|
-
if (content.trim() === '') return null
|
|
164
|
-
return truncated
|
|
165
|
-
? `${content}\n\n[Truncated: this guidance file exceeds ${GUIDANCE_TOPIC_MAX_BYTES} bytes.]\n`
|
|
166
|
-
: content
|
|
167
|
-
} catch {
|
|
168
|
-
return null
|
|
169
|
-
} finally {
|
|
170
|
-
if (descriptor !== null) closeSync(descriptor)
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Every effective topic, shipped reading order first, additive user topics
|
|
176
|
-
* after it in name order. Each carries the layer that supplied it.
|
|
177
|
-
*/
|
|
178
|
-
export function resolveGuidance(options: {
|
|
179
|
-
cwd?: string
|
|
180
|
-
env?: NodeJS.ProcessEnv
|
|
181
|
-
}): ResolvedGuidanceTopic[] {
|
|
182
|
-
const env = options.env ?? process.env
|
|
183
|
-
const cwd = options.cwd ?? process.cwd()
|
|
184
|
-
|
|
185
|
-
// Ascending precedence: a later layer overwrites an earlier one per topic.
|
|
186
|
-
const layers: { dir: string | null; label: 'global' | 'project' | 'project-local' }[] = [
|
|
187
|
-
{ dir: globalGuidanceDir(env), label: 'global' },
|
|
188
|
-
{ dir: findProjectGuidanceDir(cwd), label: 'project' },
|
|
189
|
-
{ dir: personalProjectGuidanceDir(cwd, env), label: 'project-local' },
|
|
190
|
-
]
|
|
191
|
-
|
|
192
|
-
const winners = new Map<string, { source: GuidanceSource; content: string }>()
|
|
193
|
-
for (const layer of layers) {
|
|
194
|
-
if (layer.dir === null) continue
|
|
195
|
-
for (const [name, filePath] of topicsInDir(layer.dir)) {
|
|
196
|
-
const content = readTopicFile(filePath)
|
|
197
|
-
if (content === null) continue
|
|
198
|
-
winners.set(name, { source: `${layer.label}:${filePath}`, content })
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const resolved: ResolvedGuidanceTopic[] = SHIPPED_GUIDANCE.map((topic) => {
|
|
203
|
-
const override = winners.get(topic.name)
|
|
204
|
-
winners.delete(topic.name)
|
|
205
|
-
return override === undefined
|
|
206
|
-
? {
|
|
207
|
-
name: topic.name,
|
|
208
|
-
source: 'default' as const,
|
|
209
|
-
authority: 'shipped' as const,
|
|
210
|
-
summary: topic.summary,
|
|
211
|
-
content: topic.content,
|
|
212
|
-
}
|
|
213
|
-
: {
|
|
214
|
-
name: topic.name,
|
|
215
|
-
source: override.source,
|
|
216
|
-
authority: guidanceAuthority(override.source),
|
|
217
|
-
summary: topic.summary,
|
|
218
|
-
content: override.content,
|
|
219
|
-
}
|
|
220
|
-
})
|
|
221
|
-
for (const name of [...winners.keys()].sort()) {
|
|
222
|
-
const extra = winners.get(name)!
|
|
223
|
-
const authority = guidanceAuthority(extra.source)
|
|
224
|
-
resolved.push({
|
|
225
|
-
name,
|
|
226
|
-
source: extra.source,
|
|
227
|
-
authority,
|
|
228
|
-
summary: authority === 'repository' ? 'Project house rules' : 'User-added guidance',
|
|
229
|
-
content: extra.content,
|
|
230
|
-
})
|
|
231
|
-
}
|
|
232
|
-
return resolved
|
|
233
|
-
}
|
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
2
|
-
import { mkdtempSync } from 'node:fs'
|
|
3
|
-
import os from 'node:os'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { describe, expect, it } from 'vitest'
|
|
6
|
-
import {
|
|
7
|
-
resolveActiveHarness,
|
|
8
|
-
sourceContextHarnessSession,
|
|
9
|
-
} from './commands-harness-context.js'
|
|
10
|
-
import {
|
|
11
|
-
HARNESS_CAPABILITIES,
|
|
12
|
-
HERMES_CLASSIC_CLI_LOCAL_CAPABILITY,
|
|
13
|
-
HERMES_QUESTION_ROUTING_UNAVAILABLE,
|
|
14
|
-
} from './harnesses.js'
|
|
15
|
-
import { buildSourceContext, inferInvocationContext, type GitCommand } from './invocation-context.js'
|
|
16
|
-
|
|
17
|
-
const PINNED = JSON.parse(
|
|
18
|
-
readFileSync(new URL('./fixtures/hermes-0.20.6-classic-cli-local.json', import.meta.url), 'utf8'),
|
|
19
|
-
) as {
|
|
20
|
-
durableSessionId: string
|
|
21
|
-
observedToolSubprocessEnv: { HERMES_SESSION_ID: string }
|
|
22
|
-
instance: { harness: 'hermes'; surface: 'classic-cli'; terminalBackend: 'local' }
|
|
23
|
-
cwd: { invocation: string }
|
|
24
|
-
evidence: {
|
|
25
|
-
nativeHostOs: 'macos'
|
|
26
|
-
portableFixtureFamilies: ['posix', 'windows']
|
|
27
|
-
nativeOsUnrun: ['linux', 'windows']
|
|
28
|
-
}
|
|
29
|
-
compression: {
|
|
30
|
-
defaultMode: 'in-place'
|
|
31
|
-
sessionIdBefore: string
|
|
32
|
-
sessionIdAfter: string
|
|
33
|
-
legacyRotatingSessionId: string
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const SYNTHETIC_GATEWAY_SESSION_KEY = 'synthetic-gateway-route-key-not-a-session-id'
|
|
38
|
-
|
|
39
|
-
function stateEnv(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
|
|
40
|
-
return {
|
|
41
|
-
XDG_STATE_HOME: mkdtempSync(path.join(os.tmpdir(), 'notifai-hermes-context-')),
|
|
42
|
-
...overrides,
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function fixtureGit(values: Record<string, string | null>): GitCommand {
|
|
47
|
-
return (_cwd, args) => values[args.join(' ')] ?? null
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
describe('Hermes classic CLI/local Source Context seam', () => {
|
|
51
|
-
it('attributes an uncontested HERMES_SESSION_ID as the durable Agent Session', () => {
|
|
52
|
-
const cwd = PINNED.cwd.invocation
|
|
53
|
-
const now = 1
|
|
54
|
-
const resolution = resolveActiveHarness(
|
|
55
|
-
{ HERMES_SESSION_ID: PINNED.observedToolSubprocessEnv.HERMES_SESSION_ID },
|
|
56
|
-
cwd,
|
|
57
|
-
now,
|
|
58
|
-
)
|
|
59
|
-
expect(resolution.contested).toEqual([])
|
|
60
|
-
expect(resolution.active).toMatchObject({
|
|
61
|
-
harness: 'hermes',
|
|
62
|
-
sessionId: PINNED.durableSessionId,
|
|
63
|
-
})
|
|
64
|
-
expect(sourceContextHarnessSession({ HERMES_SESSION_ID: PINNED.durableSessionId }, cwd, now)).toEqual(
|
|
65
|
-
resolution.active,
|
|
66
|
-
)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('never treats HERMES_SESSION_KEY as Agent Session identity', () => {
|
|
70
|
-
const cwd = '/workspace'
|
|
71
|
-
const now = 1
|
|
72
|
-
const keyOnly = resolveActiveHarness(
|
|
73
|
-
{ HERMES_SESSION_KEY: SYNTHETIC_GATEWAY_SESSION_KEY },
|
|
74
|
-
cwd,
|
|
75
|
-
now,
|
|
76
|
-
)
|
|
77
|
-
expect(keyOnly.active).toBeNull()
|
|
78
|
-
|
|
79
|
-
const gatewayEnv = {
|
|
80
|
-
HERMES_SESSION_ID: PINNED.observedToolSubprocessEnv.HERMES_SESSION_ID,
|
|
81
|
-
HERMES_SESSION_KEY: SYNTHETIC_GATEWAY_SESSION_KEY,
|
|
82
|
-
}
|
|
83
|
-
const both = resolveActiveHarness(gatewayEnv, cwd, now)
|
|
84
|
-
expect(both.active?.sessionId).toBe(PINNED.durableSessionId)
|
|
85
|
-
expect(both.active?.sessionId).not.toBe(SYNTHETIC_GATEWAY_SESSION_KEY)
|
|
86
|
-
expect(sourceContextHarnessSession(gatewayEnv, cwd, now)).toBeNull()
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('fails closed when Hermes markers nest with a hook-installable harness', () => {
|
|
90
|
-
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-hermes-nested-'))
|
|
91
|
-
const env = {
|
|
92
|
-
CLAUDECODE: '1',
|
|
93
|
-
CLAUDE_CODE_SESSION_ID: 'claude-orchestrator',
|
|
94
|
-
HERMES_SESSION_ID: PINNED.durableSessionId,
|
|
95
|
-
}
|
|
96
|
-
const resolution = resolveActiveHarness(env, cwd, 42)
|
|
97
|
-
expect(resolution.contested.map((candidate) => candidate.harness).sort()).toEqual([
|
|
98
|
-
'claude-code',
|
|
99
|
-
'hermes',
|
|
100
|
-
])
|
|
101
|
-
expect(sourceContextHarnessSession(env, cwd, 42)).toBeNull()
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('does not apply the send-only mix rule to a Hermes-free Claude/Codex nest', () => {
|
|
105
|
-
// Existing send attribution still uses declared order when nothing has
|
|
106
|
-
// fired. Live-evidence ownership for ask lives in commands.test.ts.
|
|
107
|
-
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-claude-codex-pair-'))
|
|
108
|
-
const env = {
|
|
109
|
-
CLAUDECODE: '1',
|
|
110
|
-
CLAUDE_CODE_SESSION_ID: 'claude-orchestrator',
|
|
111
|
-
CODEX_THREAD_ID: 'codex-current-thread',
|
|
112
|
-
}
|
|
113
|
-
const resolution = resolveActiveHarness(env, cwd, 42)
|
|
114
|
-
expect(resolution.contested.map((candidate) => candidate.harness).sort()).toEqual([
|
|
115
|
-
'claude-code',
|
|
116
|
-
'codex',
|
|
117
|
-
])
|
|
118
|
-
expect(sourceContextHarnessSession(env, cwd, 42)?.harness).toBe('claude-code')
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('builds Source Context from HERMES_SESSION_ID plus invocation cwd git/worktree', () => {
|
|
122
|
-
const env = stateEnv({
|
|
123
|
-
HERMES_SESSION_ID: PINNED.durableSessionId,
|
|
124
|
-
HERMES_SESSION_KEY: SYNTHETIC_GATEWAY_SESSION_KEY,
|
|
125
|
-
})
|
|
126
|
-
const invocation = inferInvocationContext(
|
|
127
|
-
'/worktrees/hermes-topic/apps/cli',
|
|
128
|
-
fixtureGit({
|
|
129
|
-
'rev-parse --git-common-dir': '/code/notifai/.git',
|
|
130
|
-
'rev-parse --git-dir': '/code/notifai/.git/worktrees/hermes-topic',
|
|
131
|
-
'rev-parse --show-toplevel': '/worktrees/hermes-topic/',
|
|
132
|
-
'rev-parse --abbrev-ref HEAD': 'feature/hermes-send',
|
|
133
|
-
}),
|
|
134
|
-
)
|
|
135
|
-
const active = resolveActiveHarness(env, '/worktrees/hermes-topic/apps/cli', Date.now()).active
|
|
136
|
-
const built = buildSourceContext({
|
|
137
|
-
env,
|
|
138
|
-
invocation,
|
|
139
|
-
...(active === null ? {} : { activeHarness: active }),
|
|
140
|
-
now: new Date(2026, 7, 28, 11, 13).getTime(),
|
|
141
|
-
})
|
|
142
|
-
expect(built).toMatchObject({
|
|
143
|
-
ok: true,
|
|
144
|
-
source: {
|
|
145
|
-
session_id: PINNED.durableSessionId,
|
|
146
|
-
harness: 'hermes',
|
|
147
|
-
branch: 'feature/hermes-send',
|
|
148
|
-
worktree: 'hermes-topic',
|
|
149
|
-
},
|
|
150
|
-
})
|
|
151
|
-
if (built.ok) {
|
|
152
|
-
expect(JSON.stringify(built.source)).not.toContain('gateway-route-key')
|
|
153
|
-
expect(JSON.stringify(built.source)).not.toContain('/worktrees/')
|
|
154
|
-
expect(JSON.stringify(built.source)).not.toContain('/code/')
|
|
155
|
-
}
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it('omits harness attribution when nested markers make ownership ambiguous', () => {
|
|
159
|
-
const env = stateEnv({
|
|
160
|
-
CLAUDECODE: '1',
|
|
161
|
-
CLAUDE_CODE_SESSION_ID: 'claude-orchestrator',
|
|
162
|
-
HERMES_SESSION_ID: PINNED.durableSessionId,
|
|
163
|
-
})
|
|
164
|
-
const invocation = inferInvocationContext('/workspace', fixtureGit({}))
|
|
165
|
-
const active = sourceContextHarnessSession(env, '/workspace', 1)
|
|
166
|
-
expect(active).toBeNull()
|
|
167
|
-
expect(
|
|
168
|
-
buildSourceContext({
|
|
169
|
-
env,
|
|
170
|
-
invocation,
|
|
171
|
-
now: 1,
|
|
172
|
-
}),
|
|
173
|
-
).toEqual({ ok: true })
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
it('does not invent a Question Routing route for the pinned Hermes instance', () => {
|
|
177
|
-
expect(HERMES_CLASSIC_CLI_LOCAL_CAPABILITY.instance).toEqual(PINNED.instance)
|
|
178
|
-
expect(HERMES_QUESTION_ROUTING_UNAVAILABLE.stopContinuation).toBe('unsupported')
|
|
179
|
-
expect(Object.keys(HARNESS_CAPABILITIES)).not.toContain('hermes')
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('keeps unproven Hermes surfaces out of Source Context', () => {
|
|
183
|
-
for (const extra of [
|
|
184
|
-
{ HERMES_SESSION_KEY: SYNTHETIC_GATEWAY_SESSION_KEY },
|
|
185
|
-
{ HERMES_SESSION_PLATFORM: 'api_server' },
|
|
186
|
-
{ HERMES_SESSION_SOURCE: 'tui' },
|
|
187
|
-
{ TERMINAL_ENV: 'docker' },
|
|
188
|
-
{ TERMINAL_ENV: 'ssh' },
|
|
189
|
-
]) {
|
|
190
|
-
expect(
|
|
191
|
-
sourceContextHarnessSession(
|
|
192
|
-
{ HERMES_SESSION_ID: PINNED.durableSessionId, ...extra },
|
|
193
|
-
'/workspace',
|
|
194
|
-
1,
|
|
195
|
-
),
|
|
196
|
-
).toBeNull()
|
|
197
|
-
}
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
it('records the precise platform-fixture and compression evidence boundary', () => {
|
|
201
|
-
expect(PINNED.evidence).toEqual({
|
|
202
|
-
nativeHostOs: 'macos',
|
|
203
|
-
portableFixtureFamilies: ['posix', 'windows'],
|
|
204
|
-
nativeOsUnrun: ['linux', 'windows'],
|
|
205
|
-
})
|
|
206
|
-
expect(PINNED.compression.defaultMode).toBe('in-place')
|
|
207
|
-
expect(PINNED.compression.sessionIdAfter).toBe(PINNED.compression.sessionIdBefore)
|
|
208
|
-
expect(PINNED.compression.legacyRotatingSessionId).not.toBe(
|
|
209
|
-
PINNED.compression.sessionIdBefore,
|
|
210
|
-
)
|
|
211
|
-
})
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
describe('OpenClaw agent-local Source Context seam', () => {
|
|
215
|
-
const PINNED_OPENCLAW = JSON.parse(
|
|
216
|
-
readFileSync(new URL('./fixtures/openclaw-2026.7.1-2-agent-local.json', import.meta.url), 'utf8'),
|
|
217
|
-
) as {
|
|
218
|
-
durableSessionKey: string
|
|
219
|
-
rotatingSessionId: string
|
|
220
|
-
observedPluginEnv: { NOTIFAI_ACTIVE_HARNESS: 'openclaw'; NOTIFAI_ACTIVE_SESSION_ID: string }
|
|
221
|
-
instance: { harness: 'openclaw'; surface: 'agent-local'; execHost: 'gateway' }
|
|
222
|
-
hooks: {
|
|
223
|
-
proven: { activation: string; stop: string; execMarkers: string }
|
|
224
|
-
unprovenOnThisSurface: { presence: string; sessionEnd: string }
|
|
225
|
-
}
|
|
226
|
-
deferred: string[]
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
it('pins only the probed agent-local surface, not Gateway-embedded', () => {
|
|
230
|
-
expect(PINNED_OPENCLAW.instance).toEqual({
|
|
231
|
-
harness: 'openclaw',
|
|
232
|
-
surface: 'agent-local',
|
|
233
|
-
execHost: 'gateway',
|
|
234
|
-
})
|
|
235
|
-
expect(PINNED_OPENCLAW.durableSessionKey).toBe('agent:main:probe-owner')
|
|
236
|
-
expect(PINNED_OPENCLAW.observedPluginEnv.NOTIFAI_ACTIVE_SESSION_ID).toBe(
|
|
237
|
-
PINNED_OPENCLAW.durableSessionKey,
|
|
238
|
-
)
|
|
239
|
-
expect(PINNED_OPENCLAW.hooks.proven).toEqual({
|
|
240
|
-
activation: 'before_prompt_build',
|
|
241
|
-
stop: 'agent_end',
|
|
242
|
-
execMarkers: 'resolve_exec_env',
|
|
243
|
-
})
|
|
244
|
-
expect(PINNED_OPENCLAW.hooks.unprovenOnThisSurface).toEqual({
|
|
245
|
-
presence: 'message_received',
|
|
246
|
-
sessionEnd: 'session_end',
|
|
247
|
-
})
|
|
248
|
-
expect(PINNED_OPENCLAW.deferred).toContain('gateway-embedded')
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
it('attributes Source Context to the plugin-injected sessionKey', () => {
|
|
252
|
-
const env = stateEnv(PINNED_OPENCLAW.observedPluginEnv)
|
|
253
|
-
const cwd = '/workspace'
|
|
254
|
-
const active = resolveActiveHarness(env, cwd, Date.now()).active
|
|
255
|
-
expect(active).toMatchObject({
|
|
256
|
-
harness: 'openclaw',
|
|
257
|
-
sessionId: PINNED_OPENCLAW.durableSessionKey,
|
|
258
|
-
})
|
|
259
|
-
const built = buildSourceContext({
|
|
260
|
-
env,
|
|
261
|
-
invocation: inferInvocationContext(cwd, fixtureGit({})),
|
|
262
|
-
...(active === null ? {} : { activeHarness: active }),
|
|
263
|
-
now: Date.now(),
|
|
264
|
-
})
|
|
265
|
-
expect(built).toMatchObject({
|
|
266
|
-
ok: true,
|
|
267
|
-
source: {
|
|
268
|
-
harness: 'openclaw',
|
|
269
|
-
session_id: PINNED_OPENCLAW.durableSessionKey,
|
|
270
|
-
},
|
|
271
|
-
})
|
|
272
|
-
if (built.ok) {
|
|
273
|
-
expect(JSON.stringify(built.source)).not.toContain(PINNED_OPENCLAW.rotatingSessionId)
|
|
274
|
-
}
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
it('treats nested OpenClaw and Claude markers as contested until live evidence exists', () => {
|
|
278
|
-
const env = stateEnv({
|
|
279
|
-
...PINNED_OPENCLAW.observedPluginEnv,
|
|
280
|
-
CLAUDECODE: '1',
|
|
281
|
-
CLAUDE_CODE_SESSION_ID: 'claude-orchestrator',
|
|
282
|
-
})
|
|
283
|
-
const resolution = resolveActiveHarness(env, '/workspace', 1)
|
|
284
|
-
expect(resolution.contested.map((candidate) => candidate.harness).sort()).toEqual([
|
|
285
|
-
'claude-code',
|
|
286
|
-
'openclaw',
|
|
287
|
-
])
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
it('keeps OpenClaw Question Routing unsupported', () => {
|
|
291
|
-
expect(HARNESS_CAPABILITIES.openclaw.stopContinuation).toBe('unsupported')
|
|
292
|
-
expect(HARNESS_CAPABILITIES.openclaw.deliveryRoutes).toEqual(['unsupported'])
|
|
293
|
-
})
|
|
294
|
-
})
|
package/src/harnesses.test.ts
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import {
|
|
4
|
-
HARNESS_CAPABILITIES,
|
|
5
|
-
HARNESS_LABELS,
|
|
6
|
-
HERMES_CLASSIC_CLI_LOCAL_CAPABILITY,
|
|
7
|
-
HERMES_QUESTION_ROUTING_UNAVAILABLE,
|
|
8
|
-
HOOK_INSTALLABLE_HARNESSES,
|
|
9
|
-
SOURCE_CONTEXT_HARNESSES,
|
|
10
|
-
hermesClassicCliLocalInstance,
|
|
11
|
-
isHookInstallableHarness,
|
|
12
|
-
questionRoutingCapability,
|
|
13
|
-
} from './harnesses.js'
|
|
14
|
-
|
|
15
|
-
const PINNED_HERMES_TRACE = JSON.parse(
|
|
16
|
-
readFileSync(new URL('./fixtures/hermes-0.20.6-classic-cli-local.json', import.meta.url), 'utf8'),
|
|
17
|
-
) as {
|
|
18
|
-
instance: { harness: 'hermes'; surface: 'classic-cli'; terminalBackend: 'local' }
|
|
19
|
-
observedToolSubprocessEnv: { HERMES_SESSION_ID: string }
|
|
20
|
-
supported: string[]
|
|
21
|
-
unsupported: string[]
|
|
22
|
-
deferred: string[]
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* This file used to hold a verbatim copy of the table below it, which meant
|
|
27
|
-
* changing a harness contract required changing the same words twice and the
|
|
28
|
-
* test could never disagree with the code — only with whoever forgot the second
|
|
29
|
-
* edit. What is worth asserting is the shape the rest of the CLI assumes.
|
|
30
|
-
*/
|
|
31
|
-
describe('harness contract', () => {
|
|
32
|
-
it('describes every shipped Source Context harness, and only those', () => {
|
|
33
|
-
expect(Object.keys(HARNESS_LABELS).sort()).toEqual([...SOURCE_CONTEXT_HARNESSES].sort())
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('keeps managed hook installation a strict subset of Source Context', () => {
|
|
37
|
-
expect(HOOK_INSTALLABLE_HARNESSES).toEqual([
|
|
38
|
-
'claude-code',
|
|
39
|
-
'codex',
|
|
40
|
-
'cursor',
|
|
41
|
-
'opencode',
|
|
42
|
-
'openclaw',
|
|
43
|
-
])
|
|
44
|
-
expect(SOURCE_CONTEXT_HARNESSES).toContain('hermes')
|
|
45
|
-
expect(HOOK_INSTALLABLE_HARNESSES).not.toContain('hermes')
|
|
46
|
-
expect(Object.keys(HARNESS_CAPABILITIES).sort()).toEqual([...HOOK_INSTALLABLE_HARNESSES].sort())
|
|
47
|
-
for (const harness of HOOK_INSTALLABLE_HARNESSES) {
|
|
48
|
-
expect(isHookInstallableHarness(harness)).toBe(true)
|
|
49
|
-
}
|
|
50
|
-
expect(isHookInstallableHarness('hermes')).toBe(false)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('gives a harness that cannot continue a turn no route to pretend with', () => {
|
|
54
|
-
// `ask` fails closed on these, and the reason it can is that the two facts
|
|
55
|
-
// agree. A harness claiming a delivery route while declaring it cannot
|
|
56
|
-
// continue would accept an answer into a void.
|
|
57
|
-
for (const harness of HOOK_INSTALLABLE_HARNESSES) {
|
|
58
|
-
const capability = questionRoutingCapability(harness)
|
|
59
|
-
if (capability.stopContinuation !== 'unsupported') continue
|
|
60
|
-
expect(capability.deliveryRoutes, harness).toEqual(['unsupported'])
|
|
61
|
-
}
|
|
62
|
-
expect(HERMES_QUESTION_ROUTING_UNAVAILABLE.deliveryRoutes).toEqual(['unsupported'])
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('gives a harness that can continue a turn at least one real route', () => {
|
|
66
|
-
for (const harness of HOOK_INSTALLABLE_HARNESSES) {
|
|
67
|
-
const capability = HARNESS_CAPABILITIES[harness]
|
|
68
|
-
if (capability.stopContinuation === 'unsupported') continue
|
|
69
|
-
expect(capability.deliveryRoutes.length, harness).toBeGreaterThan(0)
|
|
70
|
-
expect(capability.deliveryRoutes, harness).not.toContain('unsupported')
|
|
71
|
-
// The journal is the floor under every supported route: without it an
|
|
72
|
-
// answer that misses its turn has nowhere to wait.
|
|
73
|
-
expect(capability.deliveryRoutes, harness).toContain('hold-for-next-turn')
|
|
74
|
-
}
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('explains each harness in prose an operator can act on', () => {
|
|
78
|
-
for (const harness of HOOK_INSTALLABLE_HARNESSES) {
|
|
79
|
-
expect(questionRoutingCapability(harness).deliveryContract.length, harness).toBeGreaterThan(40)
|
|
80
|
-
}
|
|
81
|
-
expect(HERMES_QUESTION_ROUTING_UNAVAILABLE.deliveryContract.length).toBeGreaterThan(40)
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('keeps Claude Code, Codex, Cursor, and OpenCode continuation contracts unchanged', () => {
|
|
85
|
-
expect(HARNESS_CAPABILITIES['claude-code'].stopContinuation).toBe('decision-block')
|
|
86
|
-
expect(HARNESS_CAPABILITIES['claude-code'].deliveryRoutes).toEqual([
|
|
87
|
-
'hook-continuation',
|
|
88
|
-
'inbox-socket',
|
|
89
|
-
'cold-resume',
|
|
90
|
-
'hold-for-next-turn',
|
|
91
|
-
])
|
|
92
|
-
expect(HARNESS_CAPABILITIES.codex.stopContinuation).toBe('decision-block')
|
|
93
|
-
expect(HARNESS_CAPABILITIES.codex.deliveryRoutes).toEqual([
|
|
94
|
-
'hook-continuation',
|
|
95
|
-
'cold-resume',
|
|
96
|
-
'hold-for-next-turn',
|
|
97
|
-
])
|
|
98
|
-
expect(HARNESS_CAPABILITIES.cursor).toEqual({
|
|
99
|
-
stopContinuation: 'unsupported',
|
|
100
|
-
deliveryRoutes: ['unsupported'],
|
|
101
|
-
deliveryContract: HARNESS_CAPABILITIES.cursor.deliveryContract,
|
|
102
|
-
})
|
|
103
|
-
expect(HARNESS_CAPABILITIES.opencode.stopContinuation).toBe('unsupported')
|
|
104
|
-
expect(HARNESS_CAPABILITIES.opencode.deliveryRoutes).toEqual(['unsupported'])
|
|
105
|
-
expect(HARNESS_CAPABILITIES.openclaw.stopContinuation).toBe('unsupported')
|
|
106
|
-
expect(HARNESS_CAPABILITIES.openclaw.deliveryRoutes).toEqual(['unsupported'])
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('treats the pinned Hermes classic CLI/local trace as send-only', () => {
|
|
110
|
-
expect(HERMES_CLASSIC_CLI_LOCAL_CAPABILITY.instance).toEqual(PINNED_HERMES_TRACE.instance)
|
|
111
|
-
expect(PINNED_HERMES_TRACE.supported).toContain('deliberate-send')
|
|
112
|
-
expect(PINNED_HERMES_TRACE.unsupported).toContain('question-routing')
|
|
113
|
-
expect(PINNED_HERMES_TRACE.deferred).toContain('gateway')
|
|
114
|
-
expect(HERMES_CLASSIC_CLI_LOCAL_CAPABILITY.sourceContext).toBe(
|
|
115
|
-
'hermes-session-id-and-invocation-cwd',
|
|
116
|
-
)
|
|
117
|
-
expect(HERMES_QUESTION_ROUTING_UNAVAILABLE.stopContinuation).toBe('unsupported')
|
|
118
|
-
expect(HERMES_QUESTION_ROUTING_UNAVAILABLE.deliveryRoutes).toEqual(['unsupported'])
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('resolves only the pinned classic CLI/local marker envelope', () => {
|
|
122
|
-
expect(
|
|
123
|
-
hermesClassicCliLocalInstance({
|
|
124
|
-
HERMES_SESSION_ID: PINNED_HERMES_TRACE.observedToolSubprocessEnv.HERMES_SESSION_ID,
|
|
125
|
-
}),
|
|
126
|
-
).toEqual(PINNED_HERMES_TRACE.instance)
|
|
127
|
-
expect(
|
|
128
|
-
hermesClassicCliLocalInstance({
|
|
129
|
-
HERMES_SESSION_ID: PINNED_HERMES_TRACE.observedToolSubprocessEnv.HERMES_SESSION_ID,
|
|
130
|
-
HERMES_SESSION_SOURCE: 'cli',
|
|
131
|
-
TERMINAL_ENV: 'local',
|
|
132
|
-
}),
|
|
133
|
-
).toEqual(PINNED_HERMES_TRACE.instance)
|
|
134
|
-
for (const env of [
|
|
135
|
-
{ HERMES_SESSION_KEY: 'gateway-key' },
|
|
136
|
-
{ HERMES_SESSION_PLATFORM: 'api_server' },
|
|
137
|
-
{ HERMES_SESSION_SOURCE: 'tui' },
|
|
138
|
-
{ TERMINAL_ENV: 'docker' },
|
|
139
|
-
{ TERMINAL_ENV: 'ssh' },
|
|
140
|
-
]) {
|
|
141
|
-
expect(
|
|
142
|
-
hermesClassicCliLocalInstance({
|
|
143
|
-
HERMES_SESSION_ID: PINNED_HERMES_TRACE.observedToolSubprocessEnv.HERMES_SESSION_ID,
|
|
144
|
-
...env,
|
|
145
|
-
}),
|
|
146
|
-
).toBeNull()
|
|
147
|
-
}
|
|
148
|
-
})
|
|
149
|
-
})
|