@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,181 +0,0 @@
1
- import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
2
- import path from 'node:path'
3
- import { EXIT, type CommandDeps } from './commands-core.js'
4
- import { GUIDANCE_TRUST_PREAMBLE, SHIPPED_GUIDANCE, shippedGuidanceTopic } from './guidance-content.js'
5
- import {
6
- GUIDANCE_TOPIC_MAX_BYTES,
7
- GUIDANCE_TOPIC_PATTERN,
8
- findProjectGuidanceDir,
9
- globalGuidanceDir,
10
- personalProjectGuidanceDir,
11
- resolveGuidance,
12
- } from './guidance.js'
13
- import { renderGuidance } from './guidance-render.js'
14
-
15
- // ---------------------------------------------------------------------------
16
- // guidance show / set / unset
17
- // ---------------------------------------------------------------------------
18
-
19
- /**
20
- * The whole effective guidance, ready to be followed as read.
21
- *
22
- * The trust preamble comes first and belongs to the CLI: it says who supplied
23
- * each topic and what no topic may ask for. Each topic then follows its own
24
- * marker, so a reader can tell the User's standing word from a repository's
25
- * house rules from the shipped default without the output stopping being
26
- * Markdown.
27
- */
28
- export function guidanceShowCommand(deps: CommandDeps, flags: { json?: boolean }): number {
29
- const topics = resolveGuidance({ cwd: deps.cwd, env: deps.env })
30
- if (flags.json) {
31
- deps.io.out(
32
- JSON.stringify(
33
- {
34
- trust: GUIDANCE_TRUST_PREAMBLE,
35
- topics: topics.map((topic) => ({
36
- name: topic.name,
37
- source: topic.source,
38
- authority: topic.authority,
39
- summary: topic.summary,
40
- content: topic.content,
41
- })),
42
- },
43
- null,
44
- 2,
45
- ),
46
- )
47
- return EXIT.ok
48
- }
49
- deps.io.out(renderGuidance(topics))
50
- return EXIT.ok
51
- }
52
-
53
- type GuidanceMutationFlags = {
54
- project?: boolean | undefined
55
- local?: boolean | undefined
56
- yes?: boolean | undefined
57
- }
58
- type GuidanceLayer = 'global' | 'project' | 'local'
59
-
60
- /**
61
- * Where a topic file for one layer lives. The project layer prefers an
62
- * existing `.notifai/guidance` up the tree and otherwise starts one at cwd,
63
- * the same anchoring `config set --project` uses for its file.
64
- */
65
- function guidanceLayerDir(deps: CommandDeps, layer: GuidanceLayer): string {
66
- if (layer === 'local') return personalProjectGuidanceDir(deps.cwd, deps.env)
67
- if (layer === 'project') {
68
- return findProjectGuidanceDir(deps.cwd) ?? path.join(deps.cwd, '.notifai', 'guidance')
69
- }
70
- return globalGuidanceDir(deps.env)
71
- }
72
-
73
- async function guidanceMutationTarget(
74
- deps: CommandDeps,
75
- topic: string,
76
- flags: GuidanceMutationFlags,
77
- ): Promise<{ path: string; layer: GuidanceLayer } | null> {
78
- if (!GUIDANCE_TOPIC_PATTERN.test(topic)) {
79
- deps.io.err(
80
- `"${topic}" is not a topic name. Use lowercase letters, digits, "-" and "_", e.g. ${SHIPPED_GUIDANCE.map((t) => t.name).join(', ')}.`,
81
- )
82
- return null
83
- }
84
- let layer: GuidanceLayer = flags.local ? 'local' : flags.project ? 'project' : 'global'
85
- if (
86
- flags.local !== true &&
87
- flags.project !== true &&
88
- flags.yes !== true &&
89
- deps.io.interactive === true &&
90
- deps.io.select
91
- ) {
92
- const selected = await deps.io.select('Where should this guidance live?', [
93
- { value: 'global', label: 'This machine', hint: 'your standing word, applies across projects' },
94
- { value: 'project', label: 'This project (shared)', hint: '.notifai/guidance/ — committed, read as project policy' },
95
- { value: 'local', label: 'This project (personal)', hint: 'your standing word here, stored outside the repo' },
96
- ])
97
- if (selected === null) {
98
- deps.io.err('No guidance layer selected.')
99
- return null
100
- }
101
- layer = selected as GuidanceLayer
102
- }
103
- return { path: path.join(guidanceLayerDir(deps, layer), `${topic}.md`), layer }
104
- }
105
-
106
- /**
107
- * Writes one topic at one layer, replacing the shipped topic of that name for
108
- * this scope. The confirmation gate is the same one `config set` holds: the
109
- * user owns their standing word, so an agent writes it only with their words
110
- * and their say-so.
111
- */
112
- export async function guidanceSetCommand(
113
- deps: CommandDeps,
114
- topic: string,
115
- text: string,
116
- flags: GuidanceMutationFlags,
117
- ): Promise<number> {
118
- if (text.trim() === '') {
119
- deps.io.err('Guidance text is empty. Pass the text, or --file <path|-> to read it.')
120
- return EXIT.usage
121
- }
122
- if (text.length > GUIDANCE_TOPIC_MAX_BYTES) {
123
- deps.io.err(`Guidance for one topic is capped at ${GUIDANCE_TOPIC_MAX_BYTES} bytes.`)
124
- return EXIT.usage
125
- }
126
- const target = await guidanceMutationTarget(deps, topic, flags)
127
- if (target === null) return EXIT.usage
128
-
129
- const shipped = shippedGuidanceTopic(topic)
130
- if (!flags.yes) {
131
- const replaces =
132
- shipped === undefined
133
- ? `Add guidance topic "${topic}"`
134
- : `Replace the shipped "${topic}" guidance for this scope`
135
- const confirmed = await deps.io.confirm(`${replaces} at ${target.path}?`)
136
- if (!confirmed) {
137
- deps.io.err('Not confirmed. Pass --yes to skip the confirmation gate.')
138
- return EXIT.usage
139
- }
140
- }
141
-
142
- mkdirSync(path.dirname(target.path), { recursive: true })
143
- writeFileSync(target.path, text.endsWith('\n') ? text : `${text}\n`)
144
- deps.io.out(`Wrote ${topic} to ${target.path}`)
145
- if (shipped !== undefined) {
146
- deps.io.out(`This file now replaces the shipped "${topic}" guidance wherever it applies.`)
147
- }
148
- // A shared file is committed and travels to everyone who clones this
149
- // repository, where it is read as the project's policy rather than as
150
- // anyone's personal standing word. Worth saying at the moment of writing:
151
- // the same sentence means something different in the two locations.
152
- if (target.layer === 'project') {
153
- deps.io.out(
154
- 'This is shared project policy: it is committed, everyone working here reads it, and it is never read as the User\'s own standing word. Use --local for a personal preference.',
155
- )
156
- }
157
- return EXIT.ok
158
- }
159
-
160
- export async function guidanceUnsetCommand(
161
- deps: CommandDeps,
162
- topic: string,
163
- flags: GuidanceMutationFlags,
164
- ): Promise<number> {
165
- const target = await guidanceMutationTarget(deps, topic, flags)
166
- if (target === null) return EXIT.usage
167
- if (!existsSync(target.path)) {
168
- deps.io.out(`${topic} is not set at ${target.path}`)
169
- return EXIT.ok
170
- }
171
- if (!flags.yes) {
172
- const confirmed = await deps.io.confirm(`Remove ${target.path}?`)
173
- if (!confirmed) {
174
- deps.io.err('Not confirmed. Pass --yes to skip the confirmation gate.')
175
- return EXIT.usage
176
- }
177
- }
178
- rmSync(target.path, { force: true })
179
- deps.io.out(`Removed ${target.path}; the inherited guidance now applies.`)
180
- return EXIT.ok
181
- }
@@ -1,207 +0,0 @@
1
- import {
2
- hermesClassicCliLocalInstance,
3
- isHookInstallableHarness,
4
- type HermesClassicCliLocalInstance,
5
- type SourceContextHarness,
6
- } from './harnesses.js'
7
- import { readLiveProjectSessionPointers, readSessionState } from './hooks.js'
8
-
9
- /**
10
- * Exact evidence that this shell command is running inside one supported
11
- * harness. Configuration-directory variables are deliberately absent: they
12
- * describe where a tool stores files, not which tool owns the current shell.
13
- * OpenCode's generated plugin supplies the Notifai-owned marker because its
14
- * plugin API exposes Agent Session identity but the ordinary environment does not.
15
- *
16
- * Hermes contributes `HERMES_SESSION_ID` only — the durable Agent Session id
17
- * bridged into tool subprocesses. `HERMES_SESSION_KEY` is a gateway routing
18
- * key and must never be treated as Agent Session identity.
19
- */
20
- export interface ActiveHarnessSession {
21
- harness: SourceContextHarness
22
- label: string
23
- sessionId?: string
24
- /** Exact surface/backend cell proven for this marker envelope. */
25
- integrationInstance?: HermesClassicCliLocalInstance
26
- /** Trusted human title published by this harness's managed adapter. */
27
- sessionLabel?: string
28
- /** The harness has only a temporary placeholder title so far. */
29
- sessionLabelPending?: boolean
30
- }
31
-
32
- /**
33
- * Every harness marker present in this environment, in declared order.
34
- *
35
- * Order here is a last resort, not an answer. A harness exports its markers
36
- * into every process it starts, so a nested harness inherits its parent's
37
- * markers alongside its own and the environment alone cannot say which of them
38
- * owns this shell; `resolveActiveHarness` settles that with live evidence.
39
- */
40
- function harnessEnvCandidates(env: NodeJS.ProcessEnv): ActiveHarnessSession[] {
41
- const candidates: ActiveHarnessSession[] = []
42
- if (env['NOTIFAI_ACTIVE_HARNESS'] === 'opencode') {
43
- const sessionId = env['NOTIFAI_ACTIVE_SESSION_ID']
44
- const sessionLabel = env['NOTIFAI_ACTIVE_SESSION_LABEL']
45
- const sessionLabelPending = env['NOTIFAI_ACTIVE_SESSION_LABEL_PENDING'] === '1'
46
- candidates.push({
47
- harness: 'opencode',
48
- label: 'OpenCode',
49
- ...(sessionId === undefined || sessionId === '' ? {} : { sessionId }),
50
- ...(sessionLabel === undefined || sessionLabel === '' ? {} : { sessionLabel }),
51
- ...(sessionLabelPending ? { sessionLabelPending: true } : {}),
52
- })
53
- }
54
- if (env['NOTIFAI_ACTIVE_HARNESS'] === 'openclaw') {
55
- const sessionId = env['NOTIFAI_ACTIVE_SESSION_ID']
56
- candidates.push({
57
- harness: 'openclaw',
58
- label: 'OpenClaw',
59
- ...(sessionId === undefined || sessionId === '' ? {} : { sessionId }),
60
- })
61
- }
62
- if (env['CLAUDECODE'] === '1') {
63
- const sessionId = env['CLAUDE_CODE_SESSION_ID']
64
- candidates.push({
65
- harness: 'claude-code',
66
- label: 'Claude Code',
67
- ...(sessionId === undefined || sessionId === '' ? {} : { sessionId }),
68
- })
69
- }
70
- const codexSession = env['CODEX_THREAD_ID']
71
- if (codexSession !== undefined && codexSession !== '') {
72
- candidates.push({ harness: 'codex', label: 'Codex', sessionId: codexSession })
73
- }
74
- if ((env['CURSOR_AGENT'] ?? '') !== '') candidates.push({ harness: 'cursor', label: 'Cursor' })
75
- const hermesSession = env['HERMES_SESSION_ID']
76
- if (hermesSession !== undefined && hermesSession !== '') {
77
- const integrationInstance = hermesClassicCliLocalInstance(env)
78
- candidates.push({
79
- harness: 'hermes',
80
- label: 'Hermes',
81
- sessionId: hermesSession,
82
- ...(integrationInstance === null ? {} : { integrationInstance }),
83
- })
84
- }
85
- return candidates
86
- }
87
-
88
- interface ActiveHarnessResolution {
89
- active: ActiveHarnessSession | null
90
- /**
91
- * Markers of harnesses that could equally own this shell, present only when
92
- * nothing here has fired yet and declared order had to pick. Whatever is
93
- * reported then has to hold for every one of them.
94
- */
95
- contested: ActiveHarnessSession[]
96
- }
97
-
98
- /**
99
- * Which Agent Session owns this shell, when several claim to.
100
- *
101
- * Nesting is ordinary: an orchestrator running inside Claude Code starts a
102
- * Codex session, and that Codex process inherits `CLAUDECODE` and
103
- * `CLAUDE_CODE_SESSION_ID` on top of its own `CODEX_THREAD_ID`. The mirror is
104
- * just as ordinary, so no fixed precedence between two markers can be right —
105
- * whichever one it favours is wrong in the opposite nesting, and the cost is
106
- * silent: `ask` looks up a session that is not this one, and every remedy the
107
- * agent is told to try addresses a harness that is not running here.
108
- *
109
- * The general rule is to prefer the most specific *live* signal over inherited
110
- * environment. An inherited marker is a claim about some ancestor process; a
111
- * session id that names an entry in this directory's live pointer index is
112
- * evidence that that exact session fired a hook here and its state still
113
- * exists. Live evidence therefore wins over declared order, and the most
114
- * recently active pointer wins between two live candidates: the harness whose
115
- * turn is running is the one that fired last, while its parent sits blocked on
116
- * the child it started. Declared order decides only when nothing here has
117
- * fired yet, and it says so — every route fails closed there anyway.
118
- */
119
- export function resolveActiveHarness(
120
- env: NodeJS.ProcessEnv,
121
- cwd: string,
122
- now: number,
123
- ): ActiveHarnessResolution {
124
- const candidates = harnessEnvCandidates(env)
125
- const first = candidates[0]
126
- if (first === undefined) return { active: null, contested: [] }
127
- if (candidates.length === 1) return { active: first, contested: [] }
128
-
129
- // A send-only harness has no hook evidence, so live pointer/state of a
130
- // hook-installable parent cannot prove which nested process owns this
131
- // shell. Mixing Hermes with another marker therefore stays contested.
132
- const mixedWithSendOnly = candidates.some((candidate) => !isHookInstallableHarness(candidate.harness))
133
- if (mixedWithSendOnly) return { active: first, contested: candidates }
134
-
135
- // Exact lifecycle state is machine-global and survives a command moving to
136
- // another linked checkout. Prefer it before the checkout-local
137
- // pointer. A unique newest hook event proves which nested harness owns this
138
- // shell; ties and missing evidence remain contested and fail closed.
139
- const evidenced = candidates.flatMap((candidate) => {
140
- if (candidate.sessionId === undefined) return []
141
- const state = readSessionState(candidate.sessionId, env)
142
- if (state.harness !== candidate.harness) return []
143
- const activity = Math.max(state.last_prompt_at ?? 0, state.last_stop_at ?? 0)
144
- return activity > 0 ? [{ candidate, activity }] : []
145
- })
146
- if (evidenced.length > 0) {
147
- evidenced.sort((left, right) => right.activity - left.activity)
148
- if (evidenced.length === 1 || evidenced[0]!.activity > evidenced[1]!.activity) {
149
- return { active: evidenced[0]!.candidate, contested: [] }
150
- }
151
- return { active: first, contested: candidates }
152
- }
153
- for (const pointer of readLiveProjectSessionPointers(cwd, env, now)) {
154
- const owner = candidates.find(
155
- (candidate) =>
156
- candidate.harness === pointer.harness && candidate.sessionId === pointer.sessionId,
157
- )
158
- if (owner !== undefined) return { active: owner, contested: [] }
159
- }
160
- return { active: first, contested: candidates }
161
- }
162
-
163
- export function activeHarnessSession(
164
- env: NodeJS.ProcessEnv,
165
- cwd: string,
166
- now: number,
167
- ): ActiveHarnessSession | null {
168
- return resolveActiveHarness(env, cwd, now).active
169
- }
170
-
171
- /**
172
- * Source Context attribution. Hook-installable nesting still uses declared
173
- * order when nothing has fired — that existing send behavior is preserved.
174
- * A contested mix that includes a send-only harness (Hermes) fails closed:
175
- * the wrong Agent Session would otherwise be named as the owner.
176
- */
177
- export function sourceContextHarnessSession(
178
- env: NodeJS.ProcessEnv,
179
- cwd: string,
180
- now: number,
181
- ): ActiveHarnessSession | null {
182
- const { active, contested } = resolveActiveHarness(env, cwd, now)
183
- if (contested.some((candidate) => !isHookInstallableHarness(candidate.harness))) {
184
- return null
185
- }
186
- if (active?.harness === 'hermes' && active.integrationInstance === undefined) return null
187
- return active
188
- }
189
-
190
- /**
191
- * The pid of the Claude Code session this hook belongs to.
192
- *
193
- * Claude exports `CLAUDE_PID` to its hooks, and that is the authoritative
194
- * answer. `process.ppid` agrees with it today — the hook command runs through
195
- * a shell, but the shell `exec`s, so this process's parent *is* the session
196
- * (probed against a live 2.1.228 session) — and it remains the fallback for a
197
- * harness build that stops exporting the variable.
198
- *
199
- * Preferring the explicit value costs a line and removes a silent failure: if
200
- * this ever resolved to a shell instead, no session descriptor would match, the
201
- * route could not prove own-child ownership, and every answer would degrade to
202
- * hold-for-next-turn with nothing reported as wrong.
203
- */
204
- export function claudeSessionPid(env: NodeJS.ProcessEnv): number {
205
- const declared = Number(env['CLAUDE_PID'])
206
- return Number.isInteger(declared) && declared > 0 ? declared : process.ppid
207
- }
@@ -1,69 +0,0 @@
1
- import {
2
- NON_ROUTING_BLOCKING_STOP_TIMEOUT_SECONDS,
3
- handlerEvent,
4
- QUESTION_STOP_TIMEOUT_SECONDS,
5
- stopHandlerIsDetached,
6
- type Installation,
7
- } from './install-hooks.js'
8
-
9
- /**
10
- * Whether an installed Stop handler declares the shape its harness needs.
11
- *
12
- * The three answers differ, and getting one wrong fails silently in a
13
- * different way each time:
14
- *
15
- * - Claude Code's handler must be `async: true`, or the waiter holds the
16
- * user's turn for its whole wait instead of returning at once. It must
17
- * also declare a `timeout`, because the harness default is 600 s and the
18
- * kill is silent — the backgrounded waiter vanishes and the answer the
19
- * user already gave is never delivered.
20
- * - Codex must declare the same full-window timeout. That changes its trusted
21
- * definition and deliberately requires the User to approve it once.
22
- * - Everything else cannot own asynchronous Question Routing; it receives
23
- * only the shorter bounded cleanup/refusal timeout.
24
- */
25
- export function stopShapeProblems(
26
- installation: Installation,
27
- platform: NodeJS.Platform = process.platform,
28
- ): string[] {
29
- const problems: string[] = []
30
- for (const handler of installation.handlers.filter(
31
- (entry) => handlerEvent(entry.command) === 'stop',
32
- )) {
33
- if (stopHandlerIsDetached(installation.harness, platform)) {
34
- if (handler.async !== true) {
35
- problems.push(
36
- `${installation.file} declares a blocking Stop handler; the Claude Code wake route needs \`async: true\` so the turn ends while the waiter runs`,
37
- )
38
- }
39
- if (handler.timeout === undefined || handler.timeout < QUESTION_STOP_TIMEOUT_SECONDS) {
40
- problems.push(
41
- `${installation.file} gives Stop ${handler.timeout ?? 'no'} declared seconds; Claude Code then kills the backgrounded waiter silently before the complete answer window, so it needs an explicit ${QUESTION_STOP_TIMEOUT_SECONDS}s`,
42
- )
43
- }
44
- continue
45
- }
46
- if (installation.harness === 'codex' || installation.harness === 'claude-code') {
47
- if (handler.async === true) {
48
- problems.push(
49
- `${installation.file} declares an asynchronous ${installation.harness} Stop handler, but this platform needs the blocking continuation so the harness consumes the answer output`,
50
- )
51
- }
52
- if (handler.timeout === undefined || handler.timeout < QUESTION_STOP_TIMEOUT_SECONDS) {
53
- problems.push(
54
- `${installation.file} gives Stop ${handler.timeout ?? 'no'}s, but ${installation.harness} Question Routing requires ${QUESTION_STOP_TIMEOUT_SECONDS}s to own the complete answer window`,
55
- )
56
- }
57
- continue
58
- }
59
- if (
60
- handler.timeout === undefined ||
61
- handler.timeout < NON_ROUTING_BLOCKING_STOP_TIMEOUT_SECONDS
62
- ) {
63
- problems.push(
64
- `${installation.file} gives Stop ${handler.timeout ?? 'no'}s, but the non-routing blocking handler requires ${NON_ROUTING_BLOCKING_STOP_TIMEOUT_SECONDS}s`,
65
- )
66
- }
67
- }
68
- return problems
69
- }