@reconcrap/boss-recommend-mcp 1.3.34 → 1.3.35

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/README.md CHANGED
@@ -16,7 +16,7 @@ Boss 推荐页自动化流水线 MCP(stdio)服务。
16
16
 
17
17
  - 单独运行 chat-only 任务,不需要再单独安装 `boss-chat`
18
18
  - 在 recommend screen 完成后,通过同一个父 run 自动进入 `chat_followup`
19
- - 继续把聊天页状态保存在工作区下的 `.boss-chat/`
19
+ - 继续把聊天页状态保存在用户目录下的 `~/.boss-recommend-mcp/boss-chat/`(可通过 `BOSS_CHAT_HOME` 覆盖)
20
20
 
21
21
  MCP 工具:
22
22
 
@@ -68,7 +68,7 @@ MCP 工具:
68
68
  - 在真正开始 search/screen 前,会进行最后一轮全参数总确认(岗位 + 全部筛选参数 + criteria + target_count + post_action + max_greet_count)
69
69
  - npm 全局安装后会自动执行 install:生成 skill、导出 MCP 模板,并自动尝试写入已检测到的外部 agent MCP 配置(含 Trae / trae-cn / Cursor / Claude / OpenClaw)
70
70
  - npm / npx 安装后会自动初始化 `screening-config.json` 模板(优先写入 workspace 的 `config/`,不可写时回退到用户目录)
71
- - npm 安装流程会预创建运行目录(跨平台):`~/.boss-recommend-mcp`、`~/.boss-recommend-mcp/runs`、`<workspace>/.boss-chat` 及其 `logs/runs/profiles/reports/artifacts`
71
+ - npm 安装流程会预创建运行目录(跨平台):`~/.boss-recommend-mcp`、`~/.boss-recommend-mcp/runs`、`~/.boss-recommend-mcp/boss-chat` 及其 `logs/runs/profiles/reports/artifacts/state`
72
72
  - `post_action` 必须在每次完整运行开始时确认一次
73
73
  - `target_count` 会在每次运行开始时询问一次(可留空,不设上限)
74
74
  - 当 `post_action=greet` 时,必须在运行开始时确认 `max_greet_count`
@@ -111,6 +111,7 @@ node src/cli.js start
111
111
 
112
112
  ```bash
113
113
  BOSS_RECOMMEND_HOME # 统一状态目录,默认 ~/.boss-recommend-mcp
114
+ BOSS_CHAT_HOME # 覆盖 boss-chat 运行态目录;默认 ~/.boss-recommend-mcp/boss-chat
114
115
  BOSS_RECOMMEND_SCREEN_CONFIG # 显式指定 screening-config.json 路径(最高优先级)
115
116
  BOSS_RECOMMEND_MCP_CONFIG_TARGETS # JSON 数组或系统 path 分隔路径列表,指定额外 mcp.json 目标文件
116
117
  BOSS_RECOMMEND_EXTERNAL_SKILL_DIRS # JSON 数组或系统 path 分隔路径列表,指定额外 skills 根目录
@@ -149,6 +150,8 @@ node src/cli.js run --instruction "推荐页筛选985男生,近14天没有,
149
150
  - `doctor` / `run` 默认优先读取 `~/.boss-recommend-mcp/screening-config.json`;如需强制其它路径,请设置 `BOSS_RECOMMEND_SCREEN_CONFIG`
150
151
  - 首次运行时,若仍检测到默认占位词(如 `replace-with-openai-api-key`),pipeline 会返回配置目录并要求用户修改后确认“已修改完成”再继续
151
152
  - 在 `npx` 临时目录(如 `AppData\\Local\\npm-cache\\_npx\\...`)执行时,不会再把该临时目录当作 `screening-config.json` 目标路径
153
+ - `boss-chat` 运行态默认固定写入 `~/.boss-recommend-mcp/boss-chat`;即使宿主把 MCP 进程启动在系统根目录,也不会再尝试写入 `/.boss-chat`
154
+ - 若当前工作区存在历史 `.boss-chat` 且新用户目录尚未初始化,首次运行会自动把 `logs/runs/profiles/reports/artifacts/state` 迁移到新目录,并保留旧目录作为只读历史来源
152
155
 
153
156
  配置样例见:
154
157
 
@@ -217,6 +220,7 @@ node src/cli.js chat run --job "算法工程师" --start-from unread --criteria
217
220
  - `baseUrl` / `apiKey` / `model` 不再单独传入,固定复用 recommend 的 `screening-config.json`
218
221
  - 若缺少 `follow_up.chat` 必填项,pipeline 会返回 `NEED_INPUT`
219
222
  - recommend 成功后,父 run 继续存活并进入 `chat_followup`;chat 结束后父 run 才会进入最终终态
223
+ - `boss-chat` 子任务状态统一写入 `~/.boss-recommend-mcp/boss-chat`(或 `BOSS_CHAT_HOME` 指定目录),不再依赖工作区 `cwd`
220
224
 
221
225
  ## Chat-only
222
226
 
@@ -230,11 +234,12 @@ node src/cli.js chat run --job "算法工程师" --start-from unread --criteria
230
234
  - MCP:
231
235
  - `boss_chat_health_check`
232
236
  - `prepare_boss_chat_run`
233
- - `start_boss_chat_run`
234
- - `get_boss_chat_run`
235
- - `pause_boss_chat_run`
236
- - `resume_boss_chat_run`
237
- - `cancel_boss_chat_run`
237
+ - `start_boss_chat_run`
238
+ - `get_boss_chat_run`
239
+ - `pause_boss_chat_run`
240
+ - `resume_boss_chat_run`
241
+ - `cancel_boss_chat_run`
242
+ - vendored `boss-chat` CLI 还支持 `--data-dir <path>` 与 `BOSS_CHAT_HOME`,优先级高于兼容旧行为的 `<cwd>/.boss-chat`
238
243
 
239
244
  chat-only 交互建议:
240
245
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reconcrap/boss-recommend-mcp",
3
- "version": "1.3.34",
3
+ "version": "1.3.35",
4
4
  "description": "Unified MCP pipeline for recommend-page filtering and screening on Boss Zhipin",
5
5
  "keywords": [
6
6
  "boss",
package/src/boss-chat.js CHANGED
@@ -1,8 +1,9 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import process from "node:process";
4
- import { spawn } from "node:child_process";
5
- import { fileURLToPath } from "node:url";
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { spawn } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
6
7
 
7
8
  import { getScreenConfigResolution, resolveSharedLlmTransportConfig } from "./adapters.js";
8
9
 
@@ -11,12 +12,14 @@ const packageRoot = path.resolve(path.dirname(currentFilePath), "..");
11
12
  const VENDORED_BOSS_CHAT_DIR = path.join(packageRoot, "vendor", "boss-chat-cli");
12
13
  const DEFAULT_BOSS_CHAT_POLL_MS = 1500;
13
14
  const PREPARE_BOSS_CHAT_MAX_ATTEMPTS = 3;
14
- const PREPARE_BOSS_CHAT_RETRY_DELAY_MS = 1200;
15
- const BOSS_CHAT_TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
16
- const CHAT_REQUIRED_FIELDS = ["job", "start_from", "target_count", "criteria"];
17
- export const TARGET_COUNT_CANONICAL_ALL = "all";
18
- export const TARGET_COUNT_ACCEPTED_EXAMPLES = [TARGET_COUNT_CANONICAL_ALL, -1, 20, "全部候选人"];
19
- const TARGET_COUNT_WRAPPER_KEYS = ["target_count", "targetCount", "value", "count", "limit"];
15
+ const PREPARE_BOSS_CHAT_RETRY_DELAY_MS = 1200;
16
+ const BOSS_CHAT_TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
17
+ const CHAT_REQUIRED_FIELDS = ["job", "start_from", "target_count", "criteria"];
18
+ const BOSS_CHAT_RUNTIME_SUBDIR = "boss-chat";
19
+ const BOSS_CHAT_RUNTIME_CHILD_DIRS = ["logs", "runs", "profiles", "reports", "artifacts", "state"];
20
+ export const TARGET_COUNT_CANONICAL_ALL = "all";
21
+ export const TARGET_COUNT_ACCEPTED_EXAMPLES = [TARGET_COUNT_CANONICAL_ALL, -1, 20, "全部候选人"];
22
+ const TARGET_COUNT_WRAPPER_KEYS = ["target_count", "targetCount", "value", "count", "limit"];
20
23
  const LLM_THINKING_LEVEL_FIELDS = [
21
24
  "llmThinkingLevel",
22
25
  "thinkingLevel",
@@ -28,13 +31,173 @@ function normalizeText(value) {
28
31
  return String(value || "").replace(/\s+/g, " ").trim();
29
32
  }
30
33
 
31
- function pathExists(targetPath) {
32
- try {
33
- return fs.existsSync(targetPath);
34
+ function pathExists(targetPath) {
35
+ try {
36
+ return fs.existsSync(targetPath);
34
37
  } catch {
35
38
  return false;
36
- }
37
- }
39
+ }
40
+ }
41
+
42
+ function getStateHome() {
43
+ return process.env.BOSS_RECOMMEND_HOME
44
+ ? path.resolve(process.env.BOSS_RECOMMEND_HOME)
45
+ : path.join(os.homedir(), ".boss-recommend-mcp");
46
+ }
47
+
48
+ function isRootDirectory(targetPath) {
49
+ const resolved = path.resolve(String(targetPath || ""));
50
+ const parsed = path.parse(resolved);
51
+ return resolved.toLowerCase() === String(parsed.root || "").toLowerCase();
52
+ }
53
+
54
+ function isSystemDirectoryWorkspaceRoot(workspaceRoot) {
55
+ const root = path.resolve(String(workspaceRoot || ""));
56
+ const normalized = root.replace(/\\/g, "/").toLowerCase();
57
+ if (process.platform === "win32") {
58
+ return (
59
+ normalized.endsWith("/windows")
60
+ || normalized.endsWith("/windows/system32")
61
+ || normalized.endsWith("/windows/syswow64")
62
+ || normalized.endsWith("/program files")
63
+ || normalized.endsWith("/program files (x86)")
64
+ );
65
+ }
66
+ return (
67
+ normalized === "/system"
68
+ || normalized.startsWith("/system/")
69
+ || normalized === "/usr"
70
+ || normalized.startsWith("/usr/")
71
+ || normalized === "/bin"
72
+ || normalized.startsWith("/bin/")
73
+ || normalized === "/sbin"
74
+ || normalized.startsWith("/sbin/")
75
+ );
76
+ }
77
+
78
+ function isEphemeralWorkspaceRoot(workspaceRoot) {
79
+ const normalized = path.resolve(String(workspaceRoot || ""))
80
+ .replace(/\\/g, "/")
81
+ .toLowerCase();
82
+ return (
83
+ normalized.includes("/appdata/local/npm-cache/_npx/")
84
+ || normalized.includes("/node_modules/@reconcrap/boss-recommend-mcp")
85
+ );
86
+ }
87
+
88
+ function isSafeBossChatLegacyWorkspaceRoot(workspaceRoot) {
89
+ const root = path.resolve(String(workspaceRoot || ""));
90
+ if (!root) return false;
91
+ const home = path.resolve(os.homedir());
92
+ return !(
93
+ isEphemeralWorkspaceRoot(root)
94
+ || isRootDirectory(root)
95
+ || root.toLowerCase() === home.toLowerCase()
96
+ || isSystemDirectoryWorkspaceRoot(root)
97
+ );
98
+ }
99
+
100
+ export function getBossChatDataDir() {
101
+ if (process.env.BOSS_CHAT_HOME) {
102
+ return path.resolve(process.env.BOSS_CHAT_HOME);
103
+ }
104
+ return path.join(getStateHome(), BOSS_CHAT_RUNTIME_SUBDIR);
105
+ }
106
+
107
+ export function getLegacyBossChatWorkspaceDataDir(workspaceRoot) {
108
+ if (!isSafeBossChatLegacyWorkspaceRoot(workspaceRoot)) return null;
109
+ return path.join(path.resolve(String(workspaceRoot)), ".boss-chat");
110
+ }
111
+
112
+ export function resolveBossChatRuntimeLayout(workspaceRoot) {
113
+ const dataDir = getBossChatDataDir();
114
+ const legacyWorkspaceDir = getLegacyBossChatWorkspaceDataDir(workspaceRoot);
115
+ const migrationSourceDir =
116
+ legacyWorkspaceDir && pathExists(legacyWorkspaceDir) && !pathExists(dataDir)
117
+ ? legacyWorkspaceDir
118
+ : null;
119
+ return {
120
+ workspace_root: workspaceRoot ? path.resolve(String(workspaceRoot)) : null,
121
+ data_dir: dataDir,
122
+ legacy_workspace_dir: legacyWorkspaceDir,
123
+ migration_source_dir: migrationSourceDir,
124
+ migration_pending: Boolean(migrationSourceDir),
125
+ directories: [
126
+ dataDir,
127
+ ...BOSS_CHAT_RUNTIME_CHILD_DIRS.map((name) => path.join(dataDir, name))
128
+ ]
129
+ };
130
+ }
131
+
132
+ export function ensureBossChatRuntimeReady(workspaceRoot) {
133
+ const runtime = resolveBossChatRuntimeLayout(workspaceRoot);
134
+ const created = [];
135
+ const existed = [];
136
+ const failed = [];
137
+ let migration = {
138
+ attempted: false,
139
+ performed: false,
140
+ source: runtime.migration_source_dir,
141
+ target: runtime.data_dir,
142
+ message: runtime.migration_source_dir
143
+ ? `Pending legacy boss-chat migration from ${runtime.migration_source_dir}`
144
+ : ""
145
+ };
146
+
147
+ if (runtime.migration_source_dir) {
148
+ try {
149
+ fs.cpSync(runtime.migration_source_dir, runtime.data_dir, {
150
+ recursive: true,
151
+ force: false,
152
+ errorOnExist: false
153
+ });
154
+ migration = {
155
+ attempted: true,
156
+ performed: true,
157
+ source: runtime.migration_source_dir,
158
+ target: runtime.data_dir,
159
+ message: `Migrated legacy boss-chat runtime from ${runtime.migration_source_dir} to ${runtime.data_dir}. Legacy source was preserved.`
160
+ };
161
+ } catch (error) {
162
+ migration = {
163
+ attempted: true,
164
+ performed: false,
165
+ source: runtime.migration_source_dir,
166
+ target: runtime.data_dir,
167
+ message: error?.message || "Legacy boss-chat migration failed."
168
+ };
169
+ failed.push({
170
+ path: runtime.data_dir,
171
+ message: `Legacy migration failed: ${migration.message}`
172
+ });
173
+ }
174
+ }
175
+
176
+ for (const directory of runtime.directories) {
177
+ try {
178
+ const existedBefore = pathExists(directory);
179
+ fs.mkdirSync(directory, { recursive: true });
180
+ if (existedBefore) {
181
+ existed.push(directory);
182
+ } else {
183
+ created.push(directory);
184
+ }
185
+ } catch (error) {
186
+ failed.push({
187
+ path: directory,
188
+ message: error?.message || String(error)
189
+ });
190
+ }
191
+ }
192
+
193
+ return {
194
+ ...runtime,
195
+ created,
196
+ existed,
197
+ failed,
198
+ migration
199
+ };
200
+ }
38
201
 
39
202
  function parsePositiveInteger(value, fallback = null) {
40
203
  const parsed = Number.parseInt(String(value ?? ""), 10);
@@ -469,9 +632,12 @@ function buildTargetCountNeedInputDiagnostics(input = {}, missingFields = []) {
469
632
  };
470
633
  }
471
634
 
472
- function buildBossChatCliArgs(command, input, resolvedConfig) {
473
- const args = [command, "--json"];
474
- if (command === "prepare-run") {
635
+ function buildBossChatCliArgs(command, input, resolvedConfig, runtimeLayout = null) {
636
+ const args = [command, "--json"];
637
+ if (runtimeLayout?.data_dir) {
638
+ args.push("--data-dir", runtimeLayout.data_dir);
639
+ }
640
+ if (command === "prepare-run") {
475
641
  const normalized = normalizeBossChatStartInput(input);
476
642
  args.push("--profile", normalized.profile);
477
643
  if (normalized.job) args.push("--job", normalized.job);
@@ -535,9 +701,9 @@ function buildBossChatCliArgs(command, input, resolvedConfig) {
535
701
  return args;
536
702
  }
537
703
 
538
- async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
539
- const cliPath = resolveBossChatCliPath(workspaceRoot);
540
- if (!cliPath) {
704
+ async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
705
+ const cliPath = resolveBossChatCliPath(workspaceRoot);
706
+ if (!cliPath) {
541
707
  return {
542
708
  ok: false,
543
709
  exitCode: -1,
@@ -549,12 +715,36 @@ async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
549
715
  code: "BOSS_CHAT_CLI_MISSING",
550
716
  message: "未找到 vendored boss-chat CLI。"
551
717
  }
552
- }
553
- };
554
- }
555
-
556
- let configResolution = null;
557
- if (command === "start-run" || command === "prepare-run") {
718
+ }
719
+ };
720
+ }
721
+
722
+ const runtimeLayout = ensureBossChatRuntimeReady(workspaceRoot);
723
+ const runtimeInitFailed = runtimeLayout.failed.some((item) => item.path === runtimeLayout.data_dir)
724
+ && !pathExists(runtimeLayout.data_dir);
725
+ if (runtimeInitFailed) {
726
+ return {
727
+ ok: false,
728
+ exitCode: 1,
729
+ stdout: "",
730
+ stderr: "",
731
+ payload: {
732
+ status: "FAILED",
733
+ error: {
734
+ code: "BOSS_CHAT_RUNTIME_INIT_FAILED",
735
+ message: runtimeLayout.failed
736
+ .filter((item) => item.path === runtimeLayout.data_dir)
737
+ .map((item) => item.message)
738
+ .join("; ") || "无法初始化 boss-chat runtime 目录。"
739
+ },
740
+ data_dir: runtimeLayout.data_dir,
741
+ migration: runtimeLayout.migration
742
+ }
743
+ };
744
+ }
745
+
746
+ let configResolution = null;
747
+ if (command === "start-run" || command === "prepare-run") {
558
748
  configResolution = resolveBossChatScreenConfig(workspaceRoot);
559
749
  if (!configResolution.ok) {
560
750
  return {
@@ -568,13 +758,13 @@ async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
568
758
  config_path: configResolution.config_path,
569
759
  config_dir: configResolution.config_dir
570
760
  }
571
- };
572
- }
573
- }
574
-
575
- const args = [cliPath, ...buildBossChatCliArgs(command, input, configResolution?.config || {})];
576
- const cwd = path.resolve(String(workspaceRoot || process.cwd()));
577
- return new Promise((resolve) => {
761
+ };
762
+ }
763
+ }
764
+
765
+ const args = [cliPath, ...buildBossChatCliArgs(command, input, configResolution?.config || {}, runtimeLayout)];
766
+ const cwd = path.resolve(String(workspaceRoot || process.cwd()));
767
+ return new Promise((resolve) => {
578
768
  const child = spawn(process.execPath, args, {
579
769
  cwd,
580
770
  env: process.env,
@@ -639,41 +829,52 @@ async function spawnBossChatCli({ workspaceRoot, command, input = {} }) {
639
829
  });
640
830
  }
641
831
 
642
- export function getBossChatHealthCheck(workspaceRoot, input = {}) {
643
- const cliDir = resolveBossChatCliDir(workspaceRoot);
644
- const cliPath = resolveBossChatCliPath(workspaceRoot);
645
- const configResolution = resolveBossChatScreenConfig(workspaceRoot);
646
- const resolvedPort = parsePositiveInteger(input.port)
647
- || (configResolution.ok ? configResolution.config.debugPort : 9222);
648
- if (!cliDir || !cliPath) {
832
+ export function getBossChatHealthCheck(workspaceRoot, input = {}) {
833
+ const cliDir = resolveBossChatCliDir(workspaceRoot);
834
+ const cliPath = resolveBossChatCliPath(workspaceRoot);
835
+ const configResolution = resolveBossChatScreenConfig(workspaceRoot);
836
+ const runtimeLayout = resolveBossChatRuntimeLayout(workspaceRoot);
837
+ const resolvedPort = parsePositiveInteger(input.port)
838
+ || (configResolution.ok ? configResolution.config.debugPort : 9222);
839
+ if (!cliDir || !cliPath) {
649
840
  return {
650
841
  status: "FAILED",
651
- error: {
652
- code: "BOSS_CHAT_CLI_MISSING",
653
- message: "未找到 vendored boss-chat CLI。"
654
- }
655
- };
656
- }
657
- if (!configResolution.ok) {
842
+ error: {
843
+ code: "BOSS_CHAT_CLI_MISSING",
844
+ message: "未找到 vendored boss-chat CLI。"
845
+ },
846
+ data_dir: runtimeLayout.data_dir,
847
+ legacy_workspace_dir: runtimeLayout.legacy_workspace_dir,
848
+ migration_pending: runtimeLayout.migration_pending
849
+ };
850
+ }
851
+ if (!configResolution.ok) {
658
852
  return {
659
853
  status: "FAILED",
660
- error: configResolution.error,
661
- config_path: configResolution.config_path,
662
- config_dir: configResolution.config_dir,
663
- cli_dir: cliDir,
664
- cli_path: cliPath
665
- };
666
- }
667
- return {
854
+ error: configResolution.error,
855
+ config_path: configResolution.config_path,
856
+ config_dir: configResolution.config_dir,
857
+ cli_dir: cliDir,
858
+ cli_path: cliPath,
859
+ data_dir: runtimeLayout.data_dir,
860
+ legacy_workspace_dir: runtimeLayout.legacy_workspace_dir,
861
+ migration_pending: runtimeLayout.migration_pending
862
+ };
863
+ }
864
+ return {
668
865
  status: "OK",
669
866
  server: "boss-chat",
670
- cli_dir: cliDir,
671
- cli_path: cliPath,
672
- config_path: configResolution.config_path,
673
- debug_port: resolvedPort,
674
- shared_llm_config: true
675
- };
676
- }
867
+ cli_dir: cliDir,
868
+ cli_path: cliPath,
869
+ config_path: configResolution.config_path,
870
+ debug_port: resolvedPort,
871
+ shared_llm_config: true,
872
+ data_dir: runtimeLayout.data_dir,
873
+ legacy_workspace_dir: runtimeLayout.legacy_workspace_dir,
874
+ migration_source_dir: runtimeLayout.migration_source_dir,
875
+ migration_pending: runtimeLayout.migration_pending
876
+ };
877
+ }
677
878
 
678
879
  export async function startBossChatRun({ workspaceRoot, input = {} }) {
679
880
  const missingFields = getMissingBossChatStartFields(input);
package/src/cli.js CHANGED
@@ -18,10 +18,12 @@ import {
18
18
  } from "./adapters.js";
19
19
  import {
20
20
  cancelBossChatRun,
21
+ ensureBossChatRuntimeReady,
21
22
  getBossChatHealthCheck,
22
23
  getBossChatRun,
23
24
  pauseBossChatRun,
24
25
  prepareBossChatRun,
26
+ resolveBossChatRuntimeLayout,
25
27
  resumeBossChatRun,
26
28
  startBossChatRun
27
29
  } from "./boss-chat.js";
@@ -742,30 +744,22 @@ function ensureUserConfig(options = {}) {
742
744
  throw lastError || new Error("No writable target for screening-config.json");
743
745
  }
744
746
 
745
- function getBossChatDataDir(workspaceRoot) {
746
- return path.join(path.resolve(String(workspaceRoot || process.cwd())), ".boss-chat");
747
- }
748
-
749
747
  function collectRuntimeDirectories(options = {}) {
750
748
  const workspaceRoot = getWorkspaceRoot(options);
751
749
  const stateHome = getStateHome();
752
- const bossChatRoot = getBossChatDataDir(workspaceRoot);
750
+ const runtime = resolveBossChatRuntimeLayout(workspaceRoot);
751
+ const bossChatRoot = runtime.data_dir;
753
752
  const recommendRuntimeDirs = [
754
753
  stateHome,
755
754
  path.join(stateHome, "runs")
756
755
  ];
757
- const bossChatRuntimeDirs = [
758
- bossChatRoot,
759
- path.join(bossChatRoot, "logs"),
760
- path.join(bossChatRoot, "runs"),
761
- path.join(bossChatRoot, "profiles"),
762
- path.join(bossChatRoot, "reports"),
763
- path.join(bossChatRoot, "artifacts")
764
- ];
756
+ const bossChatRuntimeDirs = runtime.directories || [bossChatRoot];
765
757
  return {
766
758
  workspaceRoot,
767
759
  stateHome,
768
760
  bossChatRoot,
761
+ legacyBossChatRoot: runtime.legacy_workspace_dir,
762
+ migrationPending: runtime.migration_pending,
769
763
  directories: dedupePaths([
770
764
  ...recommendRuntimeDirs,
771
765
  ...bossChatRuntimeDirs
@@ -774,19 +768,20 @@ function collectRuntimeDirectories(options = {}) {
774
768
  }
775
769
 
776
770
  function ensureRuntimeDirectories(options = {}) {
777
- const { workspaceRoot, stateHome, bossChatRoot, directories } = collectRuntimeDirectories(options);
778
- const created = [];
779
- const existed = [];
780
- const failed = [];
771
+ const { workspaceRoot, stateHome } = collectRuntimeDirectories(options);
772
+ const runtime = ensureBossChatRuntimeReady(workspaceRoot);
773
+ const recommendCreated = [];
774
+ const recommendExisted = [];
775
+ const failed = [...runtime.failed];
781
776
 
782
- for (const directory of directories) {
777
+ for (const directory of [stateHome, path.join(stateHome, "runs")]) {
783
778
  try {
784
779
  const existedBefore = fs.existsSync(directory);
785
780
  ensureDir(directory);
786
781
  if (existedBefore) {
787
- existed.push(directory);
782
+ recommendExisted.push(directory);
788
783
  } else {
789
- created.push(directory);
784
+ recommendCreated.push(directory);
790
785
  }
791
786
  } catch (error) {
792
787
  failed.push({
@@ -799,9 +794,12 @@ function ensureRuntimeDirectories(options = {}) {
799
794
  return {
800
795
  workspaceRoot,
801
796
  stateHome,
802
- bossChatRoot,
803
- created,
804
- existed,
797
+ bossChatRoot: runtime.data_dir,
798
+ legacyBossChatRoot: runtime.legacy_workspace_dir,
799
+ migrationPending: runtime.migration_pending,
800
+ migration: runtime.migration,
801
+ created: dedupePaths([...recommendCreated, ...runtime.created]),
802
+ existed: dedupePaths([...recommendExisted, ...runtime.existed]),
805
803
  failed
806
804
  };
807
805
  }
@@ -1343,10 +1341,13 @@ function printPaths() {
1343
1341
  const codexHome = getCodexHome();
1344
1342
  const stateHome = getStateHome();
1345
1343
  const calibrationResolution = getFeaturedCalibrationResolution(process.cwd());
1344
+ const bossChatRuntime = resolveBossChatRuntimeLayout(getWorkspaceRoot({}));
1346
1345
  console.log(`package_root=${packageRoot}`);
1347
1346
  console.log(`skill_sources=${bundledSkillNames.map((name) => getSkillSourceDir(name)).join(" | ")}`);
1348
1347
  console.log(`codex_home=${codexHome}`);
1349
1348
  console.log(`state_home=${stateHome}`);
1349
+ console.log(`boss_chat_runtime=${bossChatRuntime.data_dir}`);
1350
+ console.log(`boss_chat_legacy_workspace_runtime=${bossChatRuntime.legacy_workspace_dir || ""}`);
1350
1351
  console.log(`skill_targets=${bundledSkillNames.map((name) => path.join(codexHome, "skills", name)).join(" | ")}`);
1351
1352
  console.log(`config_target=${getUserConfigPath()}`);
1352
1353
  console.log(`legacy_config_target=${getLegacyUserConfigPath()}`);
@@ -1408,6 +1409,9 @@ function installAll(options = {}) {
1408
1409
  );
1409
1410
  console.log(`- recommend runtime: ${runtimeDirsResult.stateHome}`);
1410
1411
  console.log(`- boss-chat runtime: ${runtimeDirsResult.bossChatRoot}`);
1412
+ if (runtimeDirsResult.migration?.performed) {
1413
+ console.log(`- boss-chat migration: ${runtimeDirsResult.migration.message}`);
1414
+ }
1411
1415
  if (runtimeDirsResult.failed.length > 0) {
1412
1416
  for (const item of runtimeDirsResult.failed) {
1413
1417
  console.warn(`Runtime dir warning: ${item.path} -> ${item.message}`);
@@ -1628,6 +1632,9 @@ export async function runCli(argv = process.argv) {
1628
1632
  );
1629
1633
  console.log(`- recommend runtime: ${runtimeDirsResult.stateHome}`);
1630
1634
  console.log(`- boss-chat runtime: ${runtimeDirsResult.bossChatRoot}`);
1635
+ if (runtimeDirsResult.migration?.performed) {
1636
+ console.log(`- boss-chat migration: ${runtimeDirsResult.migration.message}`);
1637
+ }
1631
1638
  if (runtimeDirsResult.failed.length > 0) {
1632
1639
  for (const item of runtimeDirsResult.failed) {
1633
1640
  console.warn(`Runtime dir warning: ${item.path} -> ${item.message}`);
@@ -1716,12 +1723,15 @@ export const __testables = {
1716
1723
  buildBossChatCliInput,
1717
1724
  buildDefaultMcpArgs,
1718
1725
  buildMcpLaunchConfig,
1726
+ collectRuntimeDirectories,
1727
+ ensureBossChatRuntimeReady,
1728
+ ensureRuntimeDirectories,
1719
1729
  getBossChatCliRunTarget,
1720
1730
  getDefaultMcpPackageSpecifier,
1721
1731
  getRunFollowUp,
1722
1732
  installSkill,
1723
1733
  isInstalledPackageRoot,
1724
- ensureRuntimeDirectories,
1734
+ resolveBossChatRuntimeLayout,
1725
1735
  runBossChatCliCommand,
1726
1736
  runPipelineOnce
1727
1737
  };
@@ -4,16 +4,18 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { mkdir } from "node:fs/promises";
6
6
 
7
- import {
8
- cancelBossChatRun,
9
- getBossChatHealthCheck,
10
- getBossChatRun,
11
- pauseBossChatRun,
12
- prepareBossChatRun,
13
- resumeBossChatRun,
14
- startBossChatRun
15
- } from "./boss-chat.js";
16
- import { __testables as cliTestables } from "./cli.js";
7
+ import {
8
+ cancelBossChatRun,
9
+ ensureBossChatRuntimeReady,
10
+ getBossChatHealthCheck,
11
+ getBossChatRun,
12
+ pauseBossChatRun,
13
+ prepareBossChatRun,
14
+ resolveBossChatRuntimeLayout,
15
+ resumeBossChatRun,
16
+ startBossChatRun
17
+ } from "./boss-chat.js";
18
+ import { __testables as cliTestables, runCli } from "./cli.js";
17
19
  import { __testables as indexTestables } from "./index.js";
18
20
  import { BossChatApp } from "../vendor/boss-chat-cli/src/app.js";
19
21
  import { __testables as vendorCliTestables } from "../vendor/boss-chat-cli/src/cli.js";
@@ -56,8 +58,8 @@ async function callTool(workspaceRoot, name, args = {}, id = 1) {
56
58
  return response?.result?.structuredContent;
57
59
  }
58
60
 
59
- function createBossChatTestWorkspace() {
60
- const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-boss-chat-"));
61
+ function createBossChatTestWorkspace() {
62
+ const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "boss-recommend-boss-chat-"));
61
63
  const configDir = path.join(workspaceRoot, "config");
62
64
  const cliDir = path.join(workspaceRoot, "boss-chat-cli", "src");
63
65
  fs.mkdirSync(configDir, { recursive: true });
@@ -72,26 +74,13 @@ function createBossChatTestWorkspace() {
72
74
  debugPort: 9666
73
75
  }, null, 2));
74
76
 
75
- fs.writeFileSync(path.join(cliDir, "cli.js"), [
76
- "#!/usr/bin/env node",
77
- "const fs = require('node:fs');",
78
- "const path = require('node:path');",
79
- "const cwd = process.cwd();",
80
- "const statePath = path.join(cwd, '.boss-chat', 'stub-state.json');",
81
- "fs.mkdirSync(path.dirname(statePath), { recursive: true });",
82
- "const raw = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf8') : '{}';",
83
- "const state = JSON.parse(raw || '{}');",
84
- "state.counter = Number.isInteger(state.counter) ? state.counter : 0;",
85
- "state.prepare_calls = Number.isInteger(state.prepare_calls) ? state.prepare_calls : 0;",
86
- "if (!Number.isInteger(state.prepare_fail_budget)) {",
87
- " const configured = Number.parseInt(process.env.BOSS_CHAT_STUB_PREPARE_FAILS || '0', 10);",
88
- " state.prepare_fail_budget = Number.isFinite(configured) && configured > 0 ? configured : 0;",
89
- "}",
90
- "state.runs = state.runs && typeof state.runs === 'object' ? state.runs : {};",
91
- "state.get_calls = state.get_calls && typeof state.get_calls === 'object' ? state.get_calls : {};",
92
- "const argv = process.argv.slice(2);",
93
- "const command = String(argv[0] || '').trim();",
94
- "const options = {};",
77
+ fs.writeFileSync(path.join(cliDir, "cli.js"), [
78
+ "#!/usr/bin/env node",
79
+ "const fs = require('node:fs');",
80
+ "const path = require('node:path');",
81
+ "const argv = process.argv.slice(2);",
82
+ "const command = String(argv[0] || '').trim();",
83
+ "const options = {};",
95
84
  "for (let index = 1; index < argv.length; index += 1) {",
96
85
  " const token = String(argv[index] || '');",
97
86
  " if (!token.startsWith('--')) continue;",
@@ -102,11 +91,25 @@ function createBossChatTestWorkspace() {
102
91
  " index += 1;",
103
92
  " } else {",
104
93
  " options[key] = true;",
105
- " }",
106
- "}",
107
- "function saveAndPrint(payload) {",
108
- " fs.writeFileSync(statePath, JSON.stringify(state, null, 2));",
109
- " process.stdout.write(`${JSON.stringify(payload)}\\n`);",
94
+ " }",
95
+ "}",
96
+ "const cwd = process.cwd();",
97
+ "const dataDir = String(options['data-dir'] || process.env.BOSS_CHAT_HOME || path.join(cwd, '.boss-chat'));",
98
+ "const statePath = path.join(dataDir, 'stub-state.json');",
99
+ "fs.mkdirSync(path.dirname(statePath), { recursive: true });",
100
+ "const raw = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf8') : '{}';",
101
+ "const state = JSON.parse(raw || '{}');",
102
+ "state.counter = Number.isInteger(state.counter) ? state.counter : 0;",
103
+ "state.prepare_calls = Number.isInteger(state.prepare_calls) ? state.prepare_calls : 0;",
104
+ "if (!Number.isInteger(state.prepare_fail_budget)) {",
105
+ " const configured = Number.parseInt(process.env.BOSS_CHAT_STUB_PREPARE_FAILS || '0', 10);",
106
+ " state.prepare_fail_budget = Number.isFinite(configured) && configured > 0 ? configured : 0;",
107
+ "}",
108
+ "state.runs = state.runs && typeof state.runs === 'object' ? state.runs : {};",
109
+ "state.get_calls = state.get_calls && typeof state.get_calls === 'object' ? state.get_calls : {};",
110
+ "function saveAndPrint(payload) {",
111
+ " fs.writeFileSync(statePath, JSON.stringify(state, null, 2));",
112
+ " process.stdout.write(`${JSON.stringify(payload)}\\n`);",
110
113
  "}",
111
114
  "if (command === 'prepare-run') {",
112
115
  " state.prepare_calls += 1;",
@@ -186,29 +189,40 @@ function createBossChatTestWorkspace() {
186
189
  "process.exit(1);"
187
190
  ].join("\n"), "utf8");
188
191
 
189
- return workspaceRoot;
190
- }
191
-
192
- function readStubState(workspaceRoot) {
193
- const statePath = path.join(workspaceRoot, ".boss-chat", "stub-state.json");
194
- return JSON.parse(fs.readFileSync(statePath, "utf8"));
195
- }
196
-
197
- async function withBossChatWorkspace(testFn) {
198
- const workspaceRoot = createBossChatTestWorkspace();
199
- const previousScreenConfig = process.env.BOSS_RECOMMEND_SCREEN_CONFIG;
200
- process.env.BOSS_RECOMMEND_SCREEN_CONFIG = path.join(workspaceRoot, "config", "screening-config.json");
201
- try {
202
- await testFn(workspaceRoot);
203
- } finally {
204
- if (previousScreenConfig === undefined) {
205
- delete process.env.BOSS_RECOMMEND_SCREEN_CONFIG;
206
- } else {
207
- process.env.BOSS_RECOMMEND_SCREEN_CONFIG = previousScreenConfig;
208
- }
209
- fs.rmSync(workspaceRoot, { recursive: true, force: true });
210
- }
211
- }
192
+ return workspaceRoot;
193
+ }
194
+
195
+ function getTestChatDataDir(workspaceRoot) {
196
+ return resolveBossChatRuntimeLayout(workspaceRoot).data_dir;
197
+ }
198
+
199
+ function readStubState(workspaceRoot) {
200
+ const statePath = path.join(getTestChatDataDir(workspaceRoot), "stub-state.json");
201
+ return JSON.parse(fs.readFileSync(statePath, "utf8"));
202
+ }
203
+
204
+ async function withBossChatWorkspace(testFn) {
205
+ const workspaceRoot = createBossChatTestWorkspace();
206
+ const previousScreenConfig = process.env.BOSS_RECOMMEND_SCREEN_CONFIG;
207
+ const previousBossChatHome = process.env.BOSS_CHAT_HOME;
208
+ process.env.BOSS_RECOMMEND_SCREEN_CONFIG = path.join(workspaceRoot, "config", "screening-config.json");
209
+ process.env.BOSS_CHAT_HOME = path.join(workspaceRoot, "user-boss-chat");
210
+ try {
211
+ await testFn(workspaceRoot);
212
+ } finally {
213
+ if (previousScreenConfig === undefined) {
214
+ delete process.env.BOSS_RECOMMEND_SCREEN_CONFIG;
215
+ } else {
216
+ process.env.BOSS_RECOMMEND_SCREEN_CONFIG = previousScreenConfig;
217
+ }
218
+ if (previousBossChatHome === undefined) {
219
+ delete process.env.BOSS_CHAT_HOME;
220
+ } else {
221
+ process.env.BOSS_CHAT_HOME = previousBossChatHome;
222
+ }
223
+ fs.rmSync(workspaceRoot, { recursive: true, force: true });
224
+ }
225
+ }
212
226
 
213
227
  async function captureConsoleLogs(fn) {
214
228
  const messages = [];
@@ -224,12 +238,15 @@ async function captureConsoleLogs(fn) {
224
238
  return messages;
225
239
  }
226
240
 
227
- async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
228
- await withBossChatWorkspace(async (workspaceRoot) => {
229
- const health = getBossChatHealthCheck(workspaceRoot);
230
- assert.equal(health.status, "OK");
231
- assert.equal(health.shared_llm_config, true);
232
- assert.equal(health.debug_port, 9666);
241
+ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
242
+ await withBossChatWorkspace(async (workspaceRoot) => {
243
+ const health = getBossChatHealthCheck(workspaceRoot);
244
+ assert.equal(health.status, "OK");
245
+ assert.equal(health.shared_llm_config, true);
246
+ assert.equal(health.debug_port, 9666);
247
+ assert.equal(health.data_dir, getTestChatDataDir(workspaceRoot));
248
+ assert.equal(health.legacy_workspace_dir, path.join(workspaceRoot, ".boss-chat"));
249
+ assert.equal(health.migration_pending, false);
233
250
 
234
251
  const prepared = await prepareBossChatRun({
235
252
  workspaceRoot,
@@ -259,9 +276,10 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
259
276
  assert.equal(preflightTargetQuestion.recommended_argument_patch.target_count, "all");
260
277
  assert.equal(Array.isArray(preflightTargetQuestion.options), true);
261
278
 
262
- const stateAfterPrepare = readStubState(workspaceRoot);
263
- assert.equal(stateAfterPrepare.last_prepare_args.profile, "default");
264
- assert.equal(stateAfterPrepare.last_prepare_args.port, "9666");
279
+ const stateAfterPrepare = readStubState(workspaceRoot);
280
+ assert.equal(stateAfterPrepare.last_prepare_args.profile, "default");
281
+ assert.equal(stateAfterPrepare.last_prepare_args["data-dir"], getTestChatDataDir(workspaceRoot));
282
+ assert.equal(stateAfterPrepare.last_prepare_args.port, "9666");
265
283
  assert.equal(stateAfterPrepare.last_prepare_args.baseurl, "https://api.example.com/v1");
266
284
  assert.equal(stateAfterPrepare.last_prepare_args.apikey, "sk-test-key");
267
285
  assert.equal(stateAfterPrepare.last_prepare_args.model, "gpt-4.1-mini");
@@ -281,9 +299,10 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
281
299
  assert.equal(started.status, "ACCEPTED");
282
300
  assert.equal(Boolean(started.run_id), true);
283
301
 
284
- const stateAfterStart = readStubState(workspaceRoot);
285
- assert.equal(stateAfterStart.last_start_args.profile, "default");
286
- assert.equal(stateAfterStart.last_start_args.job, "算法工程师");
302
+ const stateAfterStart = readStubState(workspaceRoot);
303
+ assert.equal(stateAfterStart.last_start_args.profile, "default");
304
+ assert.equal(stateAfterStart.last_start_args["data-dir"], getTestChatDataDir(workspaceRoot));
305
+ assert.equal(stateAfterStart.last_start_args.job, "算法工程师");
287
306
  assert.equal(stateAfterStart.last_start_args["start-from"], "unread");
288
307
  assert.equal(stateAfterStart.last_start_args.criteria, "有 AI Agent 经验");
289
308
  assert.equal(stateAfterStart.last_start_args.targetCount, "2");
@@ -389,11 +408,99 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
389
408
  }
390
409
  });
391
410
  assert.equal(canceled.run.state, "canceled");
392
- });
393
- }
394
-
395
- async function testBossChatPrepareShouldRetryWhenChatPageIsNotReady() {
396
- await withBossChatWorkspace(async (workspaceRoot) => {
411
+ });
412
+ }
413
+
414
+ async function testBossChatRuntimeShouldMigrateLegacyWorkspaceDataOnce() {
415
+ await withBossChatWorkspace(async (workspaceRoot) => {
416
+ const legacyDir = path.join(workspaceRoot, ".boss-chat");
417
+ const legacyStatePath = path.join(legacyDir, "state", "default.json");
418
+ const legacyRunPath = path.join(legacyDir, "runs", "legacy-run.json");
419
+ fs.mkdirSync(path.dirname(legacyStatePath), { recursive: true });
420
+ fs.mkdirSync(path.dirname(legacyRunPath), { recursive: true });
421
+ fs.writeFileSync(legacyStatePath, JSON.stringify({ cursor: 7 }, null, 2));
422
+ fs.writeFileSync(legacyRunPath, JSON.stringify({ run_id: "legacy-run" }, null, 2));
423
+
424
+ const before = resolveBossChatRuntimeLayout(workspaceRoot);
425
+ assert.equal(before.data_dir, getTestChatDataDir(workspaceRoot));
426
+ assert.equal(before.legacy_workspace_dir, legacyDir);
427
+ assert.equal(before.migration_source_dir, legacyDir);
428
+ assert.equal(before.migration_pending, true);
429
+
430
+ const ready = ensureBossChatRuntimeReady(workspaceRoot);
431
+ assert.equal(ready.migration.attempted, true);
432
+ assert.equal(ready.migration.performed, true);
433
+ assert.equal(fs.existsSync(path.join(ready.data_dir, "state", "default.json")), true);
434
+ assert.equal(fs.existsSync(path.join(ready.data_dir, "runs", "legacy-run.json")), true);
435
+ assert.deepEqual(
436
+ JSON.parse(fs.readFileSync(path.join(ready.data_dir, "state", "default.json"), "utf8")),
437
+ { cursor: 7 }
438
+ );
439
+ assert.equal(fs.existsSync(legacyStatePath), true);
440
+
441
+ const after = resolveBossChatRuntimeLayout(workspaceRoot);
442
+ assert.equal(after.migration_pending, false);
443
+ assert.equal(after.migration_source_dir, null);
444
+
445
+ const secondReady = ensureBossChatRuntimeReady(workspaceRoot);
446
+ assert.equal(secondReady.migration.attempted, false);
447
+ assert.equal(secondReady.migration.performed, false);
448
+ });
449
+ }
450
+
451
+ function testBossChatRuntimeShouldResolveUserDirForRootWorkspace() {
452
+ const previousBossChatHome = process.env.BOSS_CHAT_HOME;
453
+ const previousRecommendHome = process.env.BOSS_RECOMMEND_HOME;
454
+ const runtimeRoot = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-root-runtime-"));
455
+ try {
456
+ delete process.env.BOSS_CHAT_HOME;
457
+ process.env.BOSS_RECOMMEND_HOME = runtimeRoot;
458
+ const rootWorkspace = path.parse(process.cwd()).root;
459
+ const runtime = resolveBossChatRuntimeLayout(rootWorkspace);
460
+ assert.equal(runtime.data_dir, path.join(runtimeRoot, "boss-chat"));
461
+ assert.equal(runtime.legacy_workspace_dir, null);
462
+ assert.equal(runtime.migration_pending, false);
463
+
464
+ const ready = ensureBossChatRuntimeReady(rootWorkspace);
465
+ assert.equal(fs.existsSync(ready.data_dir), true);
466
+ assert.equal(fs.existsSync(path.join(ready.data_dir, "runs")), true);
467
+ } finally {
468
+ if (previousBossChatHome === undefined) {
469
+ delete process.env.BOSS_CHAT_HOME;
470
+ } else {
471
+ process.env.BOSS_CHAT_HOME = previousBossChatHome;
472
+ }
473
+ if (previousRecommendHome === undefined) {
474
+ delete process.env.BOSS_RECOMMEND_HOME;
475
+ } else {
476
+ process.env.BOSS_RECOMMEND_HOME = previousRecommendHome;
477
+ }
478
+ fs.rmSync(runtimeRoot, { recursive: true, force: true });
479
+ }
480
+ }
481
+
482
+ async function testBossChatWhereShouldPrintUserRuntimePath() {
483
+ await withBossChatWorkspace(async (workspaceRoot) => {
484
+ const previousWorkspaceRoot = process.env.BOSS_WORKSPACE_ROOT;
485
+ process.env.BOSS_WORKSPACE_ROOT = workspaceRoot;
486
+ try {
487
+ const logs = await captureConsoleLogs(async () => {
488
+ await runCli(["node", "src/cli.js", "where"]);
489
+ });
490
+ assert.equal(logs.some((line) => line.includes(`boss_chat_runtime=${getTestChatDataDir(workspaceRoot)}`)), true);
491
+ assert.equal(logs.some((line) => line.includes(`boss_chat_legacy_workspace_runtime=${path.join(workspaceRoot, ".boss-chat")}`)), true);
492
+ } finally {
493
+ if (previousWorkspaceRoot === undefined) {
494
+ delete process.env.BOSS_WORKSPACE_ROOT;
495
+ } else {
496
+ process.env.BOSS_WORKSPACE_ROOT = previousWorkspaceRoot;
497
+ }
498
+ }
499
+ });
500
+ }
501
+
502
+ async function testBossChatPrepareShouldRetryWhenChatPageIsNotReady() {
503
+ await withBossChatWorkspace(async (workspaceRoot) => {
397
504
  const previousPrepareFails = process.env.BOSS_CHAT_STUB_PREPARE_FAILS;
398
505
  process.env.BOSS_CHAT_STUB_PREPARE_FAILS = "2";
399
506
  try {
@@ -412,8 +519,26 @@ async function testBossChatPrepareShouldRetryWhenChatPageIsNotReady() {
412
519
  process.env.BOSS_CHAT_STUB_PREPARE_FAILS = previousPrepareFails;
413
520
  }
414
521
  }
415
- });
416
- }
522
+ });
523
+ }
524
+
525
+ function testVendorBossChatCliShouldResolveExplicitDataDir() {
526
+ const cwd = path.join(path.parse(process.cwd()).root, "workspace");
527
+ const args = vendorCliTestables.parseArgs(["start-run", "--data-dir", "/tmp/boss-chat-data"]);
528
+ assert.equal(args.dataDir, "/tmp/boss-chat-data");
529
+ assert.equal(
530
+ vendorCliTestables.resolveDataDir(args, { BOSS_CHAT_HOME: "/tmp/ignored" }, cwd),
531
+ path.resolve("/tmp/boss-chat-data")
532
+ );
533
+ assert.equal(
534
+ vendorCliTestables.resolveDataDir({}, { BOSS_CHAT_HOME: "/tmp/from-env" }, cwd),
535
+ path.resolve("/tmp/from-env")
536
+ );
537
+ assert.equal(
538
+ vendorCliTestables.resolveDataDir({}, {}, cwd),
539
+ path.join(path.resolve(cwd), ".boss-chat")
540
+ );
541
+ }
417
542
 
418
543
  async function testBossChatPageShouldTreatBlankChatShellAsOnChatPage() {
419
544
  const fakeChromeClient = {
@@ -2879,9 +3004,12 @@ async function testBossChatReportStoreShouldWriteReadableMarkdownAndCsv() {
2879
3004
  assert.match(csvContent, /先看项目经历,再看实习时长/);
2880
3005
  }
2881
3006
 
2882
- async function main() {
2883
- await testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli();
2884
- await testBossChatPrepareShouldRetryWhenChatPageIsNotReady();
3007
+ async function main() {
3008
+ await testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli();
3009
+ await testBossChatRuntimeShouldMigrateLegacyWorkspaceDataOnce();
3010
+ testBossChatRuntimeShouldResolveUserDirForRootWorkspace();
3011
+ await testBossChatWhereShouldPrintUserRuntimePath();
3012
+ await testBossChatPrepareShouldRetryWhenChatPageIsNotReady();
2885
3013
  await testBossChatPageShouldTreatBlankChatShellAsOnChatPage();
2886
3014
  await testBossChatRecoverToChatIndexShouldForceNavigateAndWaitForCompleteLoad();
2887
3015
  await testBossChatPageShouldFallbackToEscapeWhenClosingCandidateDetail();
@@ -2889,8 +3017,9 @@ async function main() {
2889
3017
  await testBossChatPageShouldWaitForPanelsClosedInStrictConversationReady();
2890
3018
  await testBossChatPageShouldSurfaceCandidateDetailOverlayAndContentState();
2891
3019
  await testBossChatMcpToolsShouldValidateAndRoute();
2892
- await testBossChatCliShouldSupportRunAndFollowUpParsing();
2893
- await testVendorBossChatCliShouldWaitForHydratedChatShell();
3020
+ await testBossChatCliShouldSupportRunAndFollowUpParsing();
3021
+ testVendorBossChatCliShouldResolveExplicitDataDir();
3022
+ await testVendorBossChatCliShouldWaitForHydratedChatShell();
2894
3023
  await testVendorBossChatCliShouldRetryJobListDuringPromptRunProfile();
2895
3024
  testCliShouldPinInstalledPackageVersionInGeneratedMcpConfig();
2896
3025
  testVendorBossChatCliShouldParseSharedLlmTransportArgs();
@@ -178,13 +178,14 @@ function parseTargetCount(value) {
178
178
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
179
179
  }
180
180
 
181
- function parseArgs(argv) {
182
- const args = {
183
- command: 'run',
184
- profile: 'default',
185
- dryRun: false,
186
- noState: false,
187
- json: false,
181
+ function parseArgs(argv) {
182
+ const args = {
183
+ command: 'run',
184
+ profile: 'default',
185
+ dataDir: '',
186
+ dryRun: false,
187
+ noState: false,
188
+ json: false,
188
189
  runId: '',
189
190
  detachedWorker: false,
190
191
  overrides: {
@@ -213,13 +214,17 @@ function parseArgs(argv) {
213
214
  index += 1;
214
215
  }
215
216
 
216
- switch (name) {
217
- case 'profile':
218
- args.profile = value || args.profile;
219
- break;
220
- case 'dry-run':
221
- args.dryRun = true;
222
- break;
217
+ switch (name) {
218
+ case 'profile':
219
+ args.profile = value || args.profile;
220
+ break;
221
+ case 'data-dir':
222
+ case 'dataDir':
223
+ args.dataDir = String(value || '').trim();
224
+ break;
225
+ case 'dry-run':
226
+ args.dryRun = true;
227
+ break;
223
228
  case 'no-state':
224
229
  args.noState = true;
225
230
  break;
@@ -293,8 +298,16 @@ function parseArgs(argv) {
293
298
  if (positionals.length > 0) {
294
299
  args.command = positionals[0];
295
300
  }
296
- return args;
297
- }
301
+ return args;
302
+ }
303
+
304
+ function resolveDataDir(args = {}, env = process.env, cwd = process.cwd()) {
305
+ const explicit = String(args?.dataDir || '').trim();
306
+ if (explicit) return path.resolve(explicit);
307
+ const fromEnv = String(env?.BOSS_CHAT_HOME || '').trim();
308
+ if (fromEnv) return path.resolve(fromEnv);
309
+ return path.join(path.resolve(String(cwd || process.cwd())), '.boss-chat');
310
+ }
298
311
 
299
312
  function printUsage() {
300
313
  console.log('Usage: boss-chat <command> [options]');
@@ -308,11 +321,12 @@ function printUsage() {
308
321
  console.log(' resume-run Resume paused async run');
309
322
  console.log(' cancel-run Cancel async run');
310
323
  console.log('');
311
- console.log('Common options:');
312
- console.log(' --profile <name> Profile name (default: default)');
313
- console.log(' --json JSON output for agent integration');
314
- console.log(' --run-id <id> Target async run_id (for get/pause/resume/cancel)');
315
- console.log('');
324
+ console.log('Common options:');
325
+ console.log(' --profile <name> Profile name (default: default)');
326
+ console.log(' --data-dir <path> Runtime data dir (default: $BOSS_CHAT_HOME or <cwd>/.boss-chat)');
327
+ console.log(' --json JSON output for agent integration');
328
+ console.log(' --run-id <id> Target async run_id (for get/pause/resume/cancel)');
329
+ console.log('');
316
330
  console.log('Run options:');
317
331
  console.log(' --dry-run Evaluate and click, but do not request resume');
318
332
  console.log(' --no-state Disable in-run candidate deduplication');
@@ -1511,10 +1525,10 @@ async function executeRunCommand(args, dataDir) {
1511
1525
  }
1512
1526
  }
1513
1527
 
1514
- async function main() {
1515
- const args = parseArgs(process.argv.slice(2));
1516
- const dataDir = path.join(process.cwd(), '.boss-chat');
1517
- await mkdir(dataDir, { recursive: true });
1528
+ async function main() {
1529
+ const args = parseArgs(process.argv.slice(2));
1530
+ const dataDir = resolveDataDir(args);
1531
+ await mkdir(dataDir, { recursive: true });
1518
1532
 
1519
1533
  if (args.command === 'help') {
1520
1534
  printUsage();
@@ -1558,11 +1572,12 @@ async function main() {
1558
1572
  }
1559
1573
  }
1560
1574
 
1561
- export const __testables = {
1562
- parseArgs,
1563
- connectBossChatPage,
1564
- hasHydratedChatShell,
1565
- promptRunProfile,
1575
+ export const __testables = {
1576
+ parseArgs,
1577
+ resolveDataDir,
1578
+ connectBossChatPage,
1579
+ hasHydratedChatShell,
1580
+ promptRunProfile,
1566
1581
  resolveJobsWithRetry,
1567
1582
  waitForChatShellHydration,
1568
1583
  };