diff --git a/scripts/dev-api.js b/scripts/dev-api.js
index cb66ca5..f72d21b 100644
--- a/scripts/dev-api.js
+++ b/scripts/dev-api.js
@@ -5203,6 +5203,7 @@ export function buildHermesSessionRuntimeConfigValues(config = {}) {
atHour: parseHermesInteger(sessionReset.at_hour, 'at_hour', 4, 0, 23, false),
groupSessionsPerUser: readHermesBool(root.group_sessions_per_user, true),
threadSessionsPerUser: readHermesBool(root.thread_sessions_per_user, false),
+ worktreeEnabled: readHermesBool(root.worktree, false),
}
}
@@ -5224,6 +5225,7 @@ export function mergeHermesSessionRuntimeConfig(config = {}, form = {}) {
next.session_reset = sessionReset
next.group_sessions_per_user = formHermesBool(form, 'groupSessionsPerUser', currentValues.groupSessionsPerUser)
next.thread_sessions_per_user = formHermesBool(form, 'threadSessionsPerUser', currentValues.threadSessionsPerUser)
+ next.worktree = formHermesBool(form, 'worktreeEnabled', currentValues.worktreeEnabled)
return next
}
diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs
index 775db4d..1a23147 100644
--- a/src-tauri/src/commands/hermes.rs
+++ b/src-tauri/src/commands/hermes.rs
@@ -7908,6 +7908,9 @@ fn build_hermes_session_runtime_config_values(config: &serde_yaml::Value) -> Val
let thread_sessions_per_user = root
.and_then(|map| yaml_bool_field(map, "thread_sessions_per_user"))
.unwrap_or(false);
+ let worktree_enabled = root
+ .and_then(|map| yaml_bool_field(map, "worktree"))
+ .unwrap_or(false);
serde_json::json!({
"sessionResetMode": mode,
@@ -7915,6 +7918,7 @@ fn build_hermes_session_runtime_config_values(config: &serde_yaml::Value) -> Val
"atHour": at_hour,
"groupSessionsPerUser": group_sessions_per_user,
"threadSessionsPerUser": thread_sessions_per_user,
+ "worktreeEnabled": worktree_enabled,
})
}
@@ -7960,6 +7964,8 @@ fn merge_hermes_session_runtime_config(
.unwrap_or_else(|| current["groupSessionsPerUser"].as_bool().unwrap_or(true));
let thread_sessions_per_user = form_bool(form, "threadSessionsPerUser")
.unwrap_or_else(|| current["threadSessionsPerUser"].as_bool().unwrap_or(false));
+ let worktree_enabled = form_bool(form, "worktreeEnabled")
+ .unwrap_or_else(|| current["worktreeEnabled"].as_bool().unwrap_or(false));
let root = ensure_yaml_object(config)?;
let session_reset = yaml_child_object(root, "session_reset")?;
@@ -7980,6 +7986,10 @@ fn merge_hermes_session_runtime_config(
yaml_key("thread_sessions_per_user"),
serde_yaml::Value::Bool(thread_sessions_per_user),
);
+ root.insert(
+ yaml_key("worktree"),
+ serde_yaml::Value::Bool(worktree_enabled),
+ );
Ok(())
}
@@ -14379,6 +14389,31 @@ mod hermes_session_runtime_config_tests {
assert_eq!(values["atHour"], 4);
assert_eq!(values["groupSessionsPerUser"], true);
assert_eq!(values["threadSessionsPerUser"], false);
+ assert_eq!(values["worktreeEnabled"], false);
+ }
+
+ #[test]
+ fn session_runtime_values_read_worktree_flag() {
+ let config: serde_yaml::Value = serde_yaml::from_str(
+ r#"
+session_reset:
+ mode: daily
+ idle_minutes: 720
+ at_hour: 3
+group_sessions_per_user: false
+thread_sessions_per_user: true
+worktree: true
+"#,
+ )
+ .unwrap();
+ let values = build_hermes_session_runtime_config_values(&config);
+
+ assert_eq!(values["sessionResetMode"], "daily");
+ assert_eq!(values["idleMinutes"], 720);
+ assert_eq!(values["atHour"], 3);
+ assert_eq!(values["groupSessionsPerUser"], false);
+ assert_eq!(values["threadSessionsPerUser"], true);
+ assert_eq!(values["worktreeEnabled"], true);
}
#[test]
@@ -14406,6 +14441,7 @@ streaming:
"atHour": "6",
"groupSessionsPerUser": false,
"threadSessionsPerUser": true,
+ "worktreeEnabled": true,
}),
)
.unwrap();
@@ -14421,6 +14457,7 @@ streaming:
);
assert_eq!(config["group_sessions_per_user"].as_bool(), Some(false));
assert_eq!(config["thread_sessions_per_user"].as_bool(), Some(true));
+ assert_eq!(config["worktree"].as_bool(), Some(true));
}
#[test]
diff --git a/src/engines/hermes/pages/config.js b/src/engines/hermes/pages/config.js
index 5056376..6009e0d 100644
--- a/src/engines/hermes/pages/config.js
+++ b/src/engines/hermes/pages/config.js
@@ -12,6 +12,7 @@ const SESSION_RUNTIME_DEFAULTS = {
atHour: 4,
groupSessionsPerUser: true,
threadSessionsPerUser: false,
+ worktreeEnabled: false,
}
const COMPRESSION_DEFAULTS = {
@@ -498,6 +499,10 @@ export function render() {
${t('engine.hermesThreadSessionsPerUser')}
+
@@ -2727,6 +2732,7 @@ export function render() {
atHour: el.querySelector('#hm-session-at-hour')?.value || '4',
groupSessionsPerUser: !!el.querySelector('#hm-group-sessions-per-user')?.checked,
threadSessionsPerUser: !!el.querySelector('#hm-thread-sessions-per-user')?.checked,
+ worktreeEnabled: !!el.querySelector('#hm-worktree-enabled')?.checked,
}
runtimeSaving = true
runtimeError = null
diff --git a/src/locales/modules/engine.js b/src/locales/modules/engine.js
index 88b5b5c..3db1d88 100644
--- a/src/locales/modules/engine.js
+++ b/src/locales/modules/engine.js
@@ -497,7 +497,8 @@ export default {
hermesSessionAtHour: _('每日换新小时', 'Daily reset hour', '每日換新小時'),
hermesGroupSessionsPerUser: _('群聊按成员隔离会话', 'Isolate group sessions per user', '群聊依成員隔離會話'),
hermesThreadSessionsPerUser: _('线程也按成员隔离', 'Isolate thread sessions per user', '討論串也依成員隔離'),
- hermesSessionRuntimeFootnote: _('推荐保持群聊隔离开启。关闭后,同一群/频道会共用上下文和中断状态。', 'Keeping group isolation on is recommended. Turning it off shares context and interrupt state across the same group or channel.', '建議保持群聊隔離開啟。關閉後,同一群組/頻道會共用上下文和中斷狀態。'),
+ hermesWorktreeEnabled: _('CLI 会话默认使用 Git worktree 隔离', 'Use Git worktree isolation for CLI sessions by default', 'CLI 會話預設使用 Git worktree 隔離'),
+ hermesSessionRuntimeFootnote: _('推荐保持群聊隔离开启;多人或多 Agent 同仓库长跑时,可开启 worktree 隔离来减少文件冲突。', 'Keeping group isolation on is recommended. For multi-user or multi-agent long runs in the same repository, enable worktree isolation to reduce file conflicts.', '建議保持群聊隔離開啟;多人或多 Agent 同倉庫長跑時,可啟用 worktree 隔離以減少檔案衝突。'),
hermesTerminalConfigTitle: _('终端执行', 'Terminal execution', '終端執行'),
hermesTerminalConfigDesc: _('控制 Hermes 工具命令的执行环境、工作目录、超时和容器资源,避免长任务卡死或沙箱范围误配。', 'Control command execution backend, working directory, timeouts, and container resources to avoid stuck runs or sandbox misconfiguration.', '控制 Hermes 工具命令的執行環境、工作目錄、逾時和容器資源,避免長任務卡死或沙箱範圍誤配。'),
hermesTerminalConfigStatusReady: _('结构化配置', 'structured settings', '結構化設定'),
diff --git a/tests/hermes-config-page-ui.test.js b/tests/hermes-config-page-ui.test.js
index 3d2d52c..0f236bc 100644
--- a/tests/hermes-config-page-ui.test.js
+++ b/tests/hermes-config-page-ui.test.js
@@ -9,6 +9,20 @@ function extractEngineKeys() {
return [...source.matchAll(/['"](engine\.[A-Za-z0-9_.-]+)['"]/g)].map(match => match[1])
}
+test('Hermes 配置页会暴露会话安全结构化配置字段', () => {
+ for (const id of [
+ 'hm-runtime-save',
+ 'hm-session-reset-mode',
+ 'hm-session-idle-minutes',
+ 'hm-session-at-hour',
+ 'hm-group-sessions-per-user',
+ 'hm-thread-sessions-per-user',
+ 'hm-worktree-enabled',
+ ]) {
+ assert.match(source, new RegExp(`id="${id}"`), `缺少 ${id}`)
+ }
+})
+
test('Hermes 配置页会暴露工具循环防护结构化配置字段', () => {
for (const id of [
'hm-tool-guardrails-save',
@@ -430,7 +444,8 @@ test('Hermes 配置页新增结构化配置不会暴露翻译 key', () => {
key.includes('CheckpointsConfig') ||
key.includes('ApprovalsConfig') ||
key.includes('CronConfig') ||
- key.includes('LoggingConfig')
+ key.includes('LoggingConfig') ||
+ key.includes('Worktree')
)))
assert.ok(keys.size > 0, '应能提取新增结构化配置用到的 engine 翻译 key')
diff --git a/tests/hermes-session-runtime-config.test.js b/tests/hermes-session-runtime-config.test.js
index 35a7293..d35fe07 100644
--- a/tests/hermes-session-runtime-config.test.js
+++ b/tests/hermes-session-runtime-config.test.js
@@ -15,6 +15,7 @@ test('Hermes 会话运行时配置读取会提供稳定表单默认值', () => {
atHour: 4,
groupSessionsPerUser: true,
threadSessionsPerUser: false,
+ worktreeEnabled: false,
})
})
@@ -27,6 +28,7 @@ test('Hermes 会话运行时配置读取会回显 session_reset 与隔离开关'
},
group_sessions_per_user: false,
thread_sessions_per_user: true,
+ worktree: true,
})
assert.equal(values.sessionResetMode, 'daily')
@@ -34,6 +36,7 @@ test('Hermes 会话运行时配置读取会回显 session_reset 与隔离开关'
assert.equal(values.atHour, 3)
assert.equal(values.groupSessionsPerUser, false)
assert.equal(values.threadSessionsPerUser, true)
+ assert.equal(values.worktreeEnabled, true)
})
test('Hermes 会话运行时配置保存会保留无关 YAML 并写入 snake_case 字段', () => {
@@ -51,6 +54,7 @@ test('Hermes 会话运行时配置保存会保留无关 YAML 并写入 snake_cas
atHour: '6',
groupSessionsPerUser: false,
threadSessionsPerUser: true,
+ worktreeEnabled: true,
})
assert.deepEqual(next.model, { provider: 'anthropic', default: 'claude-sonnet-4-6' })
@@ -61,6 +65,7 @@ test('Hermes 会话运行时配置保存会保留无关 YAML 并写入 snake_cas
assert.equal(next.session_reset.custom_flag, 'keep-me')
assert.equal(next.group_sessions_per_user, false)
assert.equal(next.thread_sessions_per_user, true)
+ assert.equal(next.worktree, true)
})
test('Hermes 会话运行时配置保存会拒绝非法模式和范围', () => {