thinkpool-pair 0.7.253 → 0.7.255
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/bridge.mjs +5 -5
- package/cross-terminal.mjs +20 -0
- package/hermes-acp-bootstrap.py +3 -3
- package/hermes-policy.mjs +9 -4
- package/hermes-session.mjs +99 -12
- package/package.json +1 -1
- package/runtime-registry.mjs +1 -1
package/bridge.mjs
CHANGED
|
@@ -2060,9 +2060,6 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2060
2060
|
if (runtime === 'hermes' && !modelCatalogValues(catalog).has(args.model)) {
|
|
2061
2061
|
return { error: `Could not open a Hermes worker on ${JSON.stringify(args.model)} — that exact model is not in this parent session's ACP catalog.` }
|
|
2062
2062
|
}
|
|
2063
|
-
if (runtime === 'hermes' && args?.mode && !['default', 'acceptEdits'].includes(args.mode)) {
|
|
2064
|
-
return { error: `Hermes ACP only exposes default/acceptEdits as user modes; Flow roles use a bridge-owned process-local tool policy.` }
|
|
2065
|
-
}
|
|
2066
2063
|
if (runtime === 'claude' && !args?.provider && args?.model && /^gpt-/i.test(args.model)) {
|
|
2067
2064
|
return { error: `Could not open a Claude terminal on Codex model ${JSON.stringify(args.model)}. Choose runtime="codex" or a Claude model.` }
|
|
2068
2065
|
}
|
|
@@ -2144,7 +2141,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2144
2141
|
// (reset per human turn alongside the in-room budgets) + TP_CROSSROOM_OFF kill-switch.
|
|
2145
2142
|
tool(
|
|
2146
2143
|
'list_sessions',
|
|
2147
|
-
'List
|
|
2144
|
+
'List OTHER ThinkPool Code sessions (rooms) reachable from this room: your own rooms on this machine plus your partner\'s shared rooms over the pair bus. Returns each room\'s code, name, and partner host when remote so you can read into one with read_session. This is Thinkpool Ensemble across SESSIONS, not just terminals. Read-only.',
|
|
2148
2145
|
{},
|
|
2149
2146
|
async () => {
|
|
2150
2147
|
const okText = (t) => ({ content: [{ type: 'text', text: t }] })
|
|
@@ -2156,7 +2153,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2156
2153
|
),
|
|
2157
2154
|
tool(
|
|
2158
2155
|
'read_session',
|
|
2159
|
-
'Read-only view INTO another
|
|
2156
|
+
'Read-only view INTO another ThinkPool Code session returned by list_sessions—either your own room on this machine or your partner\'s shared room over the pair bus. Pass `session` (the room code) and optionally `terminal` (a ref/name in that room) to read its recent activity; omit `terminal` to list that room\'s terminals. It never changes the other session.',
|
|
2160
2157
|
{
|
|
2161
2158
|
session: z.string().describe('the room code of the other session (from list_sessions)'),
|
|
2162
2159
|
terminal: z.string().optional().describe('ref, id, or name of a terminal in that room; omit to list that room\'s terminals'),
|
|
@@ -3831,6 +3828,9 @@ channel
|
|
|
3831
3828
|
return
|
|
3832
3829
|
}
|
|
3833
3830
|
s.mode = payload.mode
|
|
3831
|
+
if (payload.mode === 'bypassPermissions') allowPending(s)
|
|
3832
|
+
else if (payload.mode === 'acceptEdits') acceptEditsPending(s)
|
|
3833
|
+
else if (payload.mode === 'plan') drainPending(s)
|
|
3834
3834
|
s.flush?.()
|
|
3835
3835
|
announce()
|
|
3836
3836
|
return
|
package/cross-terminal.mjs
CHANGED
|
@@ -606,6 +606,26 @@ export const CROSSROOM = {
|
|
|
606
606
|
peekPerTurnCap: 5, // list_sessions + read_session calls allowed per turn (vs PEEK.perTurnCap=10 in-room)
|
|
607
607
|
}
|
|
608
608
|
|
|
609
|
+
// Canonical ThinkPool peer capability names. Claude/Codex receive the SDK MCP
|
|
610
|
+
// server directly; Hermes reconstructs a process-local schema and therefore
|
|
611
|
+
// needs the same names explicitly. Keep the peer surface here beside its gates
|
|
612
|
+
// so a runtime cannot silently lose cross-room reach when the server grows.
|
|
613
|
+
export const PEER_READ_MCP_TOOLS = Object.freeze([
|
|
614
|
+
'read_terminal',
|
|
615
|
+
'list_sessions',
|
|
616
|
+
'read_session',
|
|
617
|
+
])
|
|
618
|
+
|
|
619
|
+
export const PEER_POST_MCP_TOOLS = Object.freeze([
|
|
620
|
+
'post_to_terminal',
|
|
621
|
+
'post_to_session',
|
|
622
|
+
])
|
|
623
|
+
|
|
624
|
+
export const PEER_MCP_TOOLS = Object.freeze([
|
|
625
|
+
...PEER_READ_MCP_TOOLS,
|
|
626
|
+
...PEER_POST_MCP_TOOLS,
|
|
627
|
+
])
|
|
628
|
+
|
|
609
629
|
// Render the list_sessions roster from the supervisor's room list. `rooms` is
|
|
610
630
|
// [{ code, name }]; `thisRoom` is excluded defensively (the supervisor already
|
|
611
631
|
// drops it). Mirrors formatPeek's no-arg roster shape so the agent reads one voice.
|
package/hermes-acp-bootstrap.py
CHANGED
|
@@ -39,7 +39,7 @@ def policy():
|
|
|
39
39
|
builtin = value.get("builtinTools")
|
|
40
40
|
required_builtin = value.get("requiredBuiltinTools")
|
|
41
41
|
tools = value.get("mcpTools")
|
|
42
|
-
if role not in {"ordinary", "builder", "conductor", "reviewer", "manual-review"}:
|
|
42
|
+
if role not in {"ordinary", "plan", "builder", "conductor", "reviewer", "manual-review"}:
|
|
43
43
|
die("unknown role")
|
|
44
44
|
if value.get("mcpServer") != "thinkpool" or not isinstance(builtin, list) or not isinstance(required_builtin, list) or not isinstance(tools, list):
|
|
45
45
|
die("invalid tool policy")
|
|
@@ -49,14 +49,14 @@ def policy():
|
|
|
49
49
|
if forbidden.intersection(builtin) or forbidden.intersection(required_builtin) or forbidden.intersection(tools):
|
|
50
50
|
die("delegation and session search are forbidden")
|
|
51
51
|
required_mcp = {
|
|
52
|
-
"ordinary": {"read_terminal"}, "builder": {"mark_flow_done"},
|
|
52
|
+
"ordinary": {"read_terminal"}, "plan": set(), "builder": {"mark_flow_done"},
|
|
53
53
|
"conductor": {"submit_flow_plan"},
|
|
54
54
|
"reviewer": {"submit_flow_review", "run_review_check", "read_review_file"},
|
|
55
55
|
"manual-review": {"run_review_check", "read_review_file"},
|
|
56
56
|
}
|
|
57
57
|
if not required_mcp[role].issubset(tools):
|
|
58
58
|
die("incomplete role MCP policy")
|
|
59
|
-
restricted = role in {"conductor", "reviewer", "manual-review"}
|
|
59
|
+
restricted = role in {"plan", "conductor", "reviewer", "manual-review"}
|
|
60
60
|
allowed_builtin = READ_ONLY_TOOLS if restricted else CODING_TOOLS
|
|
61
61
|
required = READ_ONLY_TOOLS if restricted else ESSENTIAL_CODING_TOOLS
|
|
62
62
|
if set(builtin) != allowed_builtin:
|
package/hermes-policy.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Bridge-owned Hermes ACP schema policy. This is intentionally a small, pure
|
|
2
2
|
// contract: Python receives only this JSON and fails closed for anything else.
|
|
3
|
+
import { PEER_MCP_TOOLS, PEER_READ_MCP_TOOLS } from './cross-terminal.mjs'
|
|
4
|
+
|
|
3
5
|
export const HERMES_POLICY_VERSION = 1
|
|
4
6
|
|
|
5
7
|
export const CODING_TOOLS = Object.freeze([
|
|
@@ -20,7 +22,8 @@ export const ESSENTIAL_CODING_TOOLS = Object.freeze([
|
|
|
20
22
|
'terminal', 'process', 'read_file', 'write_file', 'patch', 'search_files',
|
|
21
23
|
])
|
|
22
24
|
const ROLE_REQUIRED = Object.freeze({
|
|
23
|
-
ordinary: [
|
|
25
|
+
ordinary: [...PEER_MCP_TOOLS, 'spawn_terminal', 'close_terminal'],
|
|
26
|
+
plan: [],
|
|
24
27
|
conductor: ['submit_flow_plan'],
|
|
25
28
|
builder: ['mark_flow_done'],
|
|
26
29
|
reviewer: ['submit_flow_review', 'run_review_check', 'read_review_file'],
|
|
@@ -42,9 +45,9 @@ export function hermesPolicyForRole(role, { mcpTools } = {}) {
|
|
|
42
45
|
// Ordinary worker leaves deliberately lack spawn/close. Main-lane proof is
|
|
43
46
|
// enforced by requiredMcpTools at dispatch; keep this schema usable for a
|
|
44
47
|
// non-delegating ordinary child without widening it.
|
|
45
|
-
const minimum = role === 'ordinary' ?
|
|
48
|
+
const minimum = role === 'ordinary' ? PEER_MCP_TOOLS : required
|
|
46
49
|
for (const tool of minimum) if (!supplied.includes(tool)) throw new Error(`Hermes ${role} policy is missing required ThinkPool tool ${tool}`)
|
|
47
|
-
const restricted = role === 'conductor' || role === 'reviewer' || role === 'manual-review'
|
|
50
|
+
const restricted = role === 'plan' || role === 'conductor' || role === 'reviewer' || role === 'manual-review'
|
|
48
51
|
const builtinTools = restricted ? READ_ONLY_TOOLS : CODING_TOOLS
|
|
49
52
|
const requiredBuiltinTools = restricted ? READ_ONLY_TOOLS : ESSENTIAL_CODING_TOOLS
|
|
50
53
|
return Object.freeze({
|
|
@@ -60,10 +63,12 @@ export function hermesPolicyForRole(role, { mcpTools } = {}) {
|
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
export function hermesRequiredMcpTools(role, { canSpawnWorkers = false } = {}) {
|
|
63
|
-
if (role === 'ordinary') return [
|
|
66
|
+
if (role === 'ordinary') return [...PEER_MCP_TOOLS, ...(canSpawnWorkers ? ['spawn_terminal', 'close_terminal'] : [])]
|
|
64
67
|
return [...ROLE_REQUIRED[role]]
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
export const HERMES_PLAN_SAFE_MCP_TOOLS = PEER_READ_MCP_TOOLS
|
|
71
|
+
|
|
67
72
|
// Keep the bridge's local /tools proof on the same canonical inventory as the
|
|
68
73
|
// process-local Python bootstrap. `allBuiltinTools` lets that proof reject a
|
|
69
74
|
// known built-in which is not part of this role, rather than merely looking
|
package/hermes-session.mjs
CHANGED
|
@@ -6,14 +6,16 @@ import { randomUUID } from 'node:crypto'
|
|
|
6
6
|
import { AcpClient } from './acp-client.mjs'
|
|
7
7
|
import { HermesEventMapper, hermesToolFor } from './hermes-event-mapper.mjs'
|
|
8
8
|
import { probeHermesRuntime } from './hermes-probe.mjs'
|
|
9
|
-
import { hermesExactInventory, hermesPolicyEnv, hermesRoleFor } from './hermes-policy.mjs'
|
|
9
|
+
import { HERMES_PLAN_SAFE_MCP_TOOLS, hermesExactInventory, hermesPolicyEnv, hermesRoleFor } from './hermes-policy.mjs'
|
|
10
10
|
import { startCodexMcpHttp } from './codex-mcp-http.mjs'
|
|
11
|
-
import { classifyRisk } from './claude-session.mjs'
|
|
11
|
+
import { autoAllow, classifyRisk } from './claude-session.mjs'
|
|
12
|
+
import { crossPostNeedsCard } from './cross-terminal.mjs'
|
|
12
13
|
|
|
13
14
|
export const HERMES_COMMAND = 'thinkpool'
|
|
14
15
|
export const HERMES_ACP_PROTOCOL_VERSION = 1
|
|
15
|
-
export const HERMES_SUPPORTED_MODES = new Set(['default', 'acceptEdits'])
|
|
16
|
+
export const HERMES_SUPPORTED_MODES = new Set(['default', 'acceptEdits', 'plan', 'bypassPermissions'])
|
|
16
17
|
const HERMES_INITIALIZE_TIMEOUT_MS = 15_000
|
|
18
|
+
const PLAN_SAFE_MCP_TOOLS = new Set([...HERMES_PLAN_SAFE_MCP_TOOLS, 'read_review_file'])
|
|
17
19
|
|
|
18
20
|
const HERMES_SECRET_ENV_KEY = /(?:TOKEN|SECRET|PASSWORD|PRIVATE_KEY|API_KEY|ACCESS_KEY|CREDENTIAL)/i
|
|
19
21
|
const HERMES_REPLAY_UPDATES = new Set(['agent_message_chunk', 'agent_thought_chunk', 'tool_call', 'tool_call_update', 'plan'])
|
|
@@ -47,6 +49,7 @@ export function startHermesSession({
|
|
|
47
49
|
roomContext, terminalRolePrompt, rolePrompt, mcpServers, requiredMcpTools = [], prepareCwd = null,
|
|
48
50
|
command = HERMES_COMMAND, args = ['acp'], clientFactory = createAcpClient,
|
|
49
51
|
mcpHttpFactory = startCodexMcpHttp, lazy = false, hermesRole = null,
|
|
52
|
+
crossPostGate = null, didSpawnTarget = null, crossRoomPostGate = null,
|
|
50
53
|
} = {}) {
|
|
51
54
|
let activeCwd = cwd
|
|
52
55
|
const requestedModel = model || null
|
|
@@ -71,18 +74,67 @@ export function startHermesSession({
|
|
|
71
74
|
let inventoryProbe = null
|
|
72
75
|
let modelSwitchPending = false
|
|
73
76
|
let bootCancelled = false
|
|
77
|
+
let suppressNextSessionPublish = false
|
|
74
78
|
const policyRole = hermesRole || hermesRoleFor({})
|
|
75
79
|
|
|
80
|
+
const effectivePolicyRole = () => activeMode === 'plan' ? 'plan' : policyRole
|
|
81
|
+
const effectiveMcpTools = () => activeMode === 'plan'
|
|
82
|
+
? requiredMcpTools.filter((name) => PLAN_SAFE_MCP_TOOLS.has(name))
|
|
83
|
+
: requiredMcpTools
|
|
84
|
+
const acpModeFor = (value) => value === 'acceptEdits'
|
|
85
|
+
? 'accept_edits'
|
|
86
|
+
: value === 'bypassPermissions'
|
|
87
|
+
? 'dont_ask'
|
|
88
|
+
: 'default'
|
|
89
|
+
|
|
76
90
|
const emit = (event) => { try { onEvent?.(event) } catch { /* consumer isolation */ } }
|
|
77
91
|
|
|
92
|
+
const thinkpoolPeerTool = (name) => {
|
|
93
|
+
const lower = String(name || '').toLowerCase()
|
|
94
|
+
return [...HERMES_PLAN_SAFE_MCP_TOOLS, 'post_to_terminal', 'post_to_session']
|
|
95
|
+
.find((candidate) => lower === candidate || lower === `mcp__thinkpool__${candidate}` || lower === `mcp_thinkpool_${candidate}`) || null
|
|
96
|
+
}
|
|
97
|
+
|
|
78
98
|
async function onRequest(method, params, requestId) {
|
|
79
99
|
if (method !== 'session/request_permission') throw Object.assign(new Error(`Unsupported ACP client method: ${method}`), { code: -32601 })
|
|
80
100
|
const tool = hermesToolFor(params.toolCall || mapper?.tools.get(params.toolCallId) || {})
|
|
101
|
+
const peerTool = thinkpoolPeerTool(tool.name)
|
|
81
102
|
const card = {
|
|
82
103
|
id: `hermes-perm:${requestId}`,
|
|
83
|
-
toolName: tool.name,
|
|
104
|
+
toolName: peerTool ? `mcp__thinkpool__${peerTool}` : tool.name,
|
|
84
105
|
input: tool.input,
|
|
85
|
-
risk: classifyRisk(tool.name, tool.input),
|
|
106
|
+
risk: peerTool?.startsWith('post_to_') ? 'high' : classifyRisk(tool.name, tool.input),
|
|
107
|
+
}
|
|
108
|
+
// ACP's native modes currently govern edit proposals only. ThinkPool's
|
|
109
|
+
// permission chip is the cross-runtime authority, so apply the same pure
|
|
110
|
+
// mode policy Claude uses before raising a durable room card. Structural
|
|
111
|
+
// role schemas still win: bypass can auto-allow a request, but it cannot
|
|
112
|
+
// restore a tool that the process-local Hermes policy never exposed.
|
|
113
|
+
// Match Claude/Codex peer semantics before the generic permission matrix.
|
|
114
|
+
// Paired-room reads are always read-only, including in Plan. Same-room posts
|
|
115
|
+
// honor bypass/ownership after their bridge gate. Cross-room posts always
|
|
116
|
+
// retain sender consent—even in bypass—because the target has a second card.
|
|
117
|
+
if (HERMES_PLAN_SAFE_MCP_TOOLS.includes(peerTool)) {
|
|
118
|
+
return { outcome: permissionOutcome('allow', params.options) }
|
|
119
|
+
}
|
|
120
|
+
if (peerTool === 'post_to_terminal') {
|
|
121
|
+
const gate = crossPostGate ? crossPostGate() : { ok: true }
|
|
122
|
+
if (!gate.ok) return { outcome: { outcome: 'cancelled' } }
|
|
123
|
+
const spawnedByMe = didSpawnTarget ? !!didSpawnTarget(tool.input?.terminal) : false
|
|
124
|
+
if (!crossPostNeedsCard({ mode: activeMode, spawnedByMe })) {
|
|
125
|
+
return { outcome: permissionOutcome('allow', params.options) }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (peerTool === 'post_to_session') {
|
|
129
|
+
const gate = crossRoomPostGate ? crossRoomPostGate() : { ok: true }
|
|
130
|
+
if (!gate.ok) return { outcome: { outcome: 'cancelled' } }
|
|
131
|
+
let decision = 'deny'
|
|
132
|
+
try { decision = await requestPermission?.(card) } catch { /* fail closed */ }
|
|
133
|
+
return { outcome: permissionOutcome(decision, params.options) }
|
|
134
|
+
}
|
|
135
|
+
if (activeMode === 'plan') return { outcome: { outcome: 'cancelled' } }
|
|
136
|
+
if (autoAllow({ toolName: card.toolName, input: card.input, mode: activeMode })) {
|
|
137
|
+
return { outcome: permissionOutcome('allow', params.options) }
|
|
86
138
|
}
|
|
87
139
|
let decision = 'deny'
|
|
88
140
|
try { decision = await requestPermission?.(card) } catch { /* fail closed */ }
|
|
@@ -103,13 +155,13 @@ export function startHermesSession({
|
|
|
103
155
|
}
|
|
104
156
|
|
|
105
157
|
async function probeMcpTools() {
|
|
106
|
-
const required = [...new Set((Array.isArray(
|
|
158
|
+
const required = [...new Set((Array.isArray(effectiveMcpTools()) ? effectiveMcpTools() : [])
|
|
107
159
|
.map((name) => String(name || '').trim()).filter(Boolean))]
|
|
108
160
|
// Direct runtime tests and unscoped upstream callers have no bridge role
|
|
109
161
|
// contract to prove. Every bridge-created Hermes lane supplies its required
|
|
110
162
|
// MCP list; only those lanes enter the exact-inventory transaction.
|
|
111
|
-
if (!required.length) return { inventory: '', missing: [], forbidden: [] }
|
|
112
|
-
const exact = hermesExactInventory(
|
|
163
|
+
if (!required.length && effectivePolicyRole() !== 'plan') return { inventory: '', missing: [], forbidden: [] }
|
|
164
|
+
const exact = hermesExactInventory(effectivePolicyRole(), { mcpTools: required })
|
|
113
165
|
const allowedBuiltins = exact.builtinTools
|
|
114
166
|
const requiredBuiltins = exact.requiredBuiltinTools
|
|
115
167
|
const expectedMcp = exact.mcpTools
|
|
@@ -175,7 +227,7 @@ export function startHermesSession({
|
|
|
175
227
|
// profile wrapper. HERMES_HOME is the probe-verified isolated profile.
|
|
176
228
|
launchCommand = profile.python
|
|
177
229
|
launchArgs = [profile.bootstrap]
|
|
178
|
-
childEnv = { ...childEnv, HERMES_HOME: profile.profile, THINKPOOL_HERMES_ACP_POLICY: hermesPolicyEnv(
|
|
230
|
+
childEnv = { ...childEnv, HERMES_HOME: profile.profile, THINKPOOL_HERMES_ACP_POLICY: hermesPolicyEnv(effectivePolicyRole(), { mcpTools: effectiveMcpTools() }) }
|
|
179
231
|
}
|
|
180
232
|
let retired = false
|
|
181
233
|
retireClient = () => { retired = true }
|
|
@@ -271,9 +323,18 @@ export function startHermesSession({
|
|
|
271
323
|
const publishedModels = state?.models
|
|
272
324
|
? { ...state.models, currentModelId: activeModel }
|
|
273
325
|
: activeModel ? { currentModelId: activeModel, availableModels: [] } : state?.models
|
|
274
|
-
|
|
326
|
+
if (!suppressNextSessionPublish) {
|
|
327
|
+
mapper.startSession({ sessionId, models: publishedModels, modes: state?.modes, commands: [] })
|
|
328
|
+
} else {
|
|
329
|
+
// A Plan transition recreates only the ACP process so its tool schema
|
|
330
|
+
// can become structurally read-only. Keep the room transcript/catalog
|
|
331
|
+
// stable while the fresh mapper resumes the same native session.
|
|
332
|
+
mapper.sessionId = sessionId
|
|
333
|
+
mapper.model = activeModel
|
|
334
|
+
suppressNextSessionPublish = false
|
|
335
|
+
}
|
|
275
336
|
started = true
|
|
276
|
-
const acpMode = activeMode
|
|
337
|
+
const acpMode = acpModeFor(activeMode)
|
|
277
338
|
if (state?.modes?.availableModes?.some((item) => item.id === acpMode) && state.modes.currentModeId !== acpMode) {
|
|
278
339
|
await client.request('session/set_mode', { sessionId, modeId: acpMode })
|
|
279
340
|
}
|
|
@@ -452,7 +513,33 @@ export function startHermesSession({
|
|
|
452
513
|
},
|
|
453
514
|
setMode(nextMode) {
|
|
454
515
|
if (turnActive || !HERMES_SUPPORTED_MODES.has(nextMode)) return false
|
|
455
|
-
|
|
516
|
+
const priorMode = activeMode
|
|
517
|
+
const changesPolicySchema = (priorMode === 'plan') !== (nextMode === 'plan')
|
|
518
|
+
if (changesPolicySchema) {
|
|
519
|
+
// Plan is stronger than an approval preference: it removes terminal,
|
|
520
|
+
// process, write, patch, browser, and execution schemas. ACP modes alone
|
|
521
|
+
// cannot do that, so resume the exact native session in a fresh process
|
|
522
|
+
// under the bridge-owned read-only policy.
|
|
523
|
+
activeMode = nextMode
|
|
524
|
+
suppressNextSessionPublish = started
|
|
525
|
+
if (started) {
|
|
526
|
+
retireClient()
|
|
527
|
+
const oldClient = client
|
|
528
|
+
client = null
|
|
529
|
+
mapper = null
|
|
530
|
+
started = false
|
|
531
|
+
crashed = false
|
|
532
|
+
stderrTail = ''
|
|
533
|
+
oldClient?.end()
|
|
534
|
+
}
|
|
535
|
+
void boot().then(() => emit({ kind: 'mode', mode: activeMode })).catch((error) => {
|
|
536
|
+
activeMode = priorMode
|
|
537
|
+
suppressNextSessionPublish = false
|
|
538
|
+
emit({ kind: 'error', message: `Hermes mode switch failed: ${error?.message || error}`, recoverable: true })
|
|
539
|
+
})
|
|
540
|
+
return true
|
|
541
|
+
}
|
|
542
|
+
void boot().then(() => client.request('session/set_mode', { sessionId, modeId: acpModeFor(nextMode) })).then(() => {
|
|
456
543
|
activeMode = nextMode
|
|
457
544
|
emit({ kind: 'mode', mode: activeMode })
|
|
458
545
|
}).catch((error) => emit({ kind: 'error', message: `Hermes mode switch failed: ${error?.message || error}`, recoverable: true }))
|
package/package.json
CHANGED
package/runtime-registry.mjs
CHANGED
|
@@ -14,7 +14,7 @@ const RUNTIMES = Object.freeze({
|
|
|
14
14
|
hermes: Object.freeze({
|
|
15
15
|
id: 'hermes', command: 'thinkpool', label: 'Hermes Agent', protocol: 'acp',
|
|
16
16
|
structured: true, flow: true, canSteer: true, images: true, nativeModelCatalog: true, catalogRequiresSession: true, effortControl: false, defaultMode: 'default',
|
|
17
|
-
modes: Object.freeze(['default', 'acceptEdits']),
|
|
17
|
+
modes: Object.freeze(['default', 'acceptEdits', 'plan', 'bypassPermissions']),
|
|
18
18
|
beta: true,
|
|
19
19
|
}),
|
|
20
20
|
})
|