@reconcrap/boss-recommend-mcp 1.3.34 → 1.3.36

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/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,16 @@ 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_source, "env:BOSS_CHAT_HOME");
248
+ assert.equal(health.data_dir, getTestChatDataDir(workspaceRoot));
249
+ assert.equal(health.legacy_workspace_dir, path.join(workspaceRoot, ".boss-chat"));
250
+ assert.equal(health.migration_pending, false);
233
251
 
234
252
  const prepared = await prepareBossChatRun({
235
253
  workspaceRoot,
@@ -259,9 +277,10 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
259
277
  assert.equal(preflightTargetQuestion.recommended_argument_patch.target_count, "all");
260
278
  assert.equal(Array.isArray(preflightTargetQuestion.options), true);
261
279
 
262
- const stateAfterPrepare = readStubState(workspaceRoot);
263
- assert.equal(stateAfterPrepare.last_prepare_args.profile, "default");
264
- assert.equal(stateAfterPrepare.last_prepare_args.port, "9666");
280
+ const stateAfterPrepare = readStubState(workspaceRoot);
281
+ assert.equal(stateAfterPrepare.last_prepare_args.profile, "default");
282
+ assert.equal(stateAfterPrepare.last_prepare_args["data-dir"], getTestChatDataDir(workspaceRoot));
283
+ assert.equal(stateAfterPrepare.last_prepare_args.port, "9666");
265
284
  assert.equal(stateAfterPrepare.last_prepare_args.baseurl, "https://api.example.com/v1");
266
285
  assert.equal(stateAfterPrepare.last_prepare_args.apikey, "sk-test-key");
267
286
  assert.equal(stateAfterPrepare.last_prepare_args.model, "gpt-4.1-mini");
@@ -281,9 +300,10 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
281
300
  assert.equal(started.status, "ACCEPTED");
282
301
  assert.equal(Boolean(started.run_id), true);
283
302
 
284
- const stateAfterStart = readStubState(workspaceRoot);
285
- assert.equal(stateAfterStart.last_start_args.profile, "default");
286
- assert.equal(stateAfterStart.last_start_args.job, "算法工程师");
303
+ const stateAfterStart = readStubState(workspaceRoot);
304
+ assert.equal(stateAfterStart.last_start_args.profile, "default");
305
+ assert.equal(stateAfterStart.last_start_args["data-dir"], getTestChatDataDir(workspaceRoot));
306
+ assert.equal(stateAfterStart.last_start_args.job, "算法工程师");
287
307
  assert.equal(stateAfterStart.last_start_args["start-from"], "unread");
288
308
  assert.equal(stateAfterStart.last_start_args.criteria, "有 AI Agent 经验");
289
309
  assert.equal(stateAfterStart.last_start_args.targetCount, "2");
@@ -389,11 +409,99 @@ async function testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli() {
389
409
  }
390
410
  });
391
411
  assert.equal(canceled.run.state, "canceled");
392
- });
393
- }
394
-
395
- async function testBossChatPrepareShouldRetryWhenChatPageIsNotReady() {
396
- await withBossChatWorkspace(async (workspaceRoot) => {
412
+ });
413
+ }
414
+
415
+ async function testBossChatRuntimeShouldMigrateLegacyWorkspaceDataOnce() {
416
+ await withBossChatWorkspace(async (workspaceRoot) => {
417
+ const legacyDir = path.join(workspaceRoot, ".boss-chat");
418
+ const legacyStatePath = path.join(legacyDir, "state", "default.json");
419
+ const legacyRunPath = path.join(legacyDir, "runs", "legacy-run.json");
420
+ fs.mkdirSync(path.dirname(legacyStatePath), { recursive: true });
421
+ fs.mkdirSync(path.dirname(legacyRunPath), { recursive: true });
422
+ fs.writeFileSync(legacyStatePath, JSON.stringify({ cursor: 7 }, null, 2));
423
+ fs.writeFileSync(legacyRunPath, JSON.stringify({ run_id: "legacy-run" }, null, 2));
424
+
425
+ const before = resolveBossChatRuntimeLayout(workspaceRoot);
426
+ assert.equal(before.data_dir, getTestChatDataDir(workspaceRoot));
427
+ assert.equal(before.legacy_workspace_dir, legacyDir);
428
+ assert.equal(before.migration_source_dir, legacyDir);
429
+ assert.equal(before.migration_pending, true);
430
+
431
+ const ready = ensureBossChatRuntimeReady(workspaceRoot);
432
+ assert.equal(ready.migration.attempted, true);
433
+ assert.equal(ready.migration.performed, true);
434
+ assert.equal(fs.existsSync(path.join(ready.data_dir, "state", "default.json")), true);
435
+ assert.equal(fs.existsSync(path.join(ready.data_dir, "runs", "legacy-run.json")), true);
436
+ assert.deepEqual(
437
+ JSON.parse(fs.readFileSync(path.join(ready.data_dir, "state", "default.json"), "utf8")),
438
+ { cursor: 7 }
439
+ );
440
+ assert.equal(fs.existsSync(legacyStatePath), true);
441
+
442
+ const after = resolveBossChatRuntimeLayout(workspaceRoot);
443
+ assert.equal(after.migration_pending, false);
444
+ assert.equal(after.migration_source_dir, null);
445
+
446
+ const secondReady = ensureBossChatRuntimeReady(workspaceRoot);
447
+ assert.equal(secondReady.migration.attempted, false);
448
+ assert.equal(secondReady.migration.performed, false);
449
+ });
450
+ }
451
+
452
+ function testBossChatRuntimeShouldResolveUserDirForRootWorkspace() {
453
+ const previousBossChatHome = process.env.BOSS_CHAT_HOME;
454
+ const previousRecommendHome = process.env.BOSS_RECOMMEND_HOME;
455
+ const runtimeRoot = fs.mkdtempSync(path.join(os.tmpdir(), "boss-chat-root-runtime-"));
456
+ try {
457
+ delete process.env.BOSS_CHAT_HOME;
458
+ process.env.BOSS_RECOMMEND_HOME = runtimeRoot;
459
+ const rootWorkspace = path.parse(process.cwd()).root;
460
+ const runtime = resolveBossChatRuntimeLayout(rootWorkspace);
461
+ assert.equal(runtime.data_dir, path.join(runtimeRoot, "boss-chat"));
462
+ assert.equal(runtime.legacy_workspace_dir, null);
463
+ assert.equal(runtime.migration_pending, false);
464
+
465
+ const ready = ensureBossChatRuntimeReady(rootWorkspace);
466
+ assert.equal(fs.existsSync(ready.data_dir), true);
467
+ assert.equal(fs.existsSync(path.join(ready.data_dir, "runs")), true);
468
+ } finally {
469
+ if (previousBossChatHome === undefined) {
470
+ delete process.env.BOSS_CHAT_HOME;
471
+ } else {
472
+ process.env.BOSS_CHAT_HOME = previousBossChatHome;
473
+ }
474
+ if (previousRecommendHome === undefined) {
475
+ delete process.env.BOSS_RECOMMEND_HOME;
476
+ } else {
477
+ process.env.BOSS_RECOMMEND_HOME = previousRecommendHome;
478
+ }
479
+ fs.rmSync(runtimeRoot, { recursive: true, force: true });
480
+ }
481
+ }
482
+
483
+ async function testBossChatWhereShouldPrintUserRuntimePath() {
484
+ await withBossChatWorkspace(async (workspaceRoot) => {
485
+ const previousWorkspaceRoot = process.env.BOSS_WORKSPACE_ROOT;
486
+ process.env.BOSS_WORKSPACE_ROOT = workspaceRoot;
487
+ try {
488
+ const logs = await captureConsoleLogs(async () => {
489
+ await runCli(["node", "src/cli.js", "where"]);
490
+ });
491
+ assert.equal(logs.some((line) => line.includes(`boss_chat_runtime=${getTestChatDataDir(workspaceRoot)}`)), true);
492
+ assert.equal(logs.some((line) => line.includes(`boss_chat_legacy_workspace_runtime=${path.join(workspaceRoot, ".boss-chat")}`)), true);
493
+ } finally {
494
+ if (previousWorkspaceRoot === undefined) {
495
+ delete process.env.BOSS_WORKSPACE_ROOT;
496
+ } else {
497
+ process.env.BOSS_WORKSPACE_ROOT = previousWorkspaceRoot;
498
+ }
499
+ }
500
+ });
501
+ }
502
+
503
+ async function testBossChatPrepareShouldRetryWhenChatPageIsNotReady() {
504
+ await withBossChatWorkspace(async (workspaceRoot) => {
397
505
  const previousPrepareFails = process.env.BOSS_CHAT_STUB_PREPARE_FAILS;
398
506
  process.env.BOSS_CHAT_STUB_PREPARE_FAILS = "2";
399
507
  try {
@@ -412,8 +520,53 @@ async function testBossChatPrepareShouldRetryWhenChatPageIsNotReady() {
412
520
  process.env.BOSS_CHAT_STUB_PREPARE_FAILS = previousPrepareFails;
413
521
  }
414
522
  }
415
- });
416
- }
523
+ });
524
+ }
525
+
526
+ function testVendorBossChatCliShouldResolveExplicitDataDir() {
527
+ const cwd = path.join(path.parse(process.cwd()).root, "workspace");
528
+ const args = vendorCliTestables.parseArgs(["start-run", "--data-dir", "/tmp/boss-chat-data"]);
529
+ assert.equal(args.dataDir, "/tmp/boss-chat-data");
530
+ const explicitResolved = vendorCliTestables.resolveDataDirDetails(args, { BOSS_CHAT_HOME: "/tmp/ignored" }, cwd);
531
+ assert.equal(explicitResolved.source, "arg:data-dir");
532
+ assert.equal(explicitResolved.path, path.resolve("/tmp/boss-chat-data"));
533
+ assert.equal(
534
+ vendorCliTestables.resolveDataDir(args, { BOSS_CHAT_HOME: "/tmp/ignored" }, cwd),
535
+ path.resolve("/tmp/boss-chat-data")
536
+ );
537
+ const envResolved = vendorCliTestables.resolveDataDirDetails({}, { BOSS_CHAT_HOME: "/tmp/from-env" }, cwd);
538
+ assert.equal(envResolved.source, "env:BOSS_CHAT_HOME");
539
+ assert.equal(envResolved.path, path.resolve("/tmp/from-env"));
540
+ assert.equal(
541
+ vendorCliTestables.resolveDataDir({}, { BOSS_CHAT_HOME: "/tmp/from-env" }, cwd),
542
+ path.resolve("/tmp/from-env")
543
+ );
544
+ const defaultResolved = vendorCliTestables.resolveDataDirDetails({}, {}, cwd);
545
+ assert.equal(defaultResolved.source, "default:user_home");
546
+ assert.equal(defaultResolved.path, path.join(os.homedir(), ".boss-recommend-mcp", "boss-chat"));
547
+ assert.equal(
548
+ vendorCliTestables.resolveDataDir({}, {}, cwd),
549
+ path.join(os.homedir(), ".boss-recommend-mcp", "boss-chat")
550
+ );
551
+
552
+ const unsafeRoot = vendorCliTestables.validateDataDir(path.parse(process.cwd()).root);
553
+ assert.equal(unsafeRoot.ok, false);
554
+ assert.equal(unsafeRoot.code, "UNSAFE_DATA_DIR");
555
+ assert.equal(unsafeRoot.message.includes("Refusing unsafe boss-chat data dir"), true);
556
+
557
+ const safePath = vendorCliTestables.validateDataDir(path.join(os.homedir(), ".boss-recommend-mcp", "boss-chat"));
558
+ assert.equal(safePath.ok, true);
559
+ }
560
+
561
+ function testVendorBossChatCliShouldUseRecommendHomeForDefaultDataDir() {
562
+ const resolved = vendorCliTestables.resolveDataDirDetails(
563
+ {},
564
+ { BOSS_RECOMMEND_HOME: "/tmp/recommend-home" },
565
+ path.join(path.parse(process.cwd()).root, "workspace")
566
+ );
567
+ assert.equal(resolved.source, "default:env:BOSS_RECOMMEND_HOME");
568
+ assert.equal(resolved.path, path.resolve("/tmp/recommend-home/boss-chat"));
569
+ }
417
570
 
418
571
  async function testBossChatPageShouldTreatBlankChatShellAsOnChatPage() {
419
572
  const fakeChromeClient = {
@@ -2879,9 +3032,12 @@ async function testBossChatReportStoreShouldWriteReadableMarkdownAndCsv() {
2879
3032
  assert.match(csvContent, /先看项目经历,再看实习时长/);
2880
3033
  }
2881
3034
 
2882
- async function main() {
2883
- await testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli();
2884
- await testBossChatPrepareShouldRetryWhenChatPageIsNotReady();
3035
+ async function main() {
3036
+ await testBossChatAdapterShouldResolveSharedConfigAndInvokeLocalCli();
3037
+ await testBossChatRuntimeShouldMigrateLegacyWorkspaceDataOnce();
3038
+ testBossChatRuntimeShouldResolveUserDirForRootWorkspace();
3039
+ await testBossChatWhereShouldPrintUserRuntimePath();
3040
+ await testBossChatPrepareShouldRetryWhenChatPageIsNotReady();
2885
3041
  await testBossChatPageShouldTreatBlankChatShellAsOnChatPage();
2886
3042
  await testBossChatRecoverToChatIndexShouldForceNavigateAndWaitForCompleteLoad();
2887
3043
  await testBossChatPageShouldFallbackToEscapeWhenClosingCandidateDetail();
@@ -2889,8 +3045,10 @@ async function main() {
2889
3045
  await testBossChatPageShouldWaitForPanelsClosedInStrictConversationReady();
2890
3046
  await testBossChatPageShouldSurfaceCandidateDetailOverlayAndContentState();
2891
3047
  await testBossChatMcpToolsShouldValidateAndRoute();
2892
- await testBossChatCliShouldSupportRunAndFollowUpParsing();
2893
- await testVendorBossChatCliShouldWaitForHydratedChatShell();
3048
+ await testBossChatCliShouldSupportRunAndFollowUpParsing();
3049
+ testVendorBossChatCliShouldResolveExplicitDataDir();
3050
+ testVendorBossChatCliShouldUseRecommendHomeForDefaultDataDir();
3051
+ await testVendorBossChatCliShouldWaitForHydratedChatShell();
2894
3052
  await testVendorBossChatCliShouldRetryJobListDuringPromptRunProfile();
2895
3053
  testCliShouldPinInstalledPackageVersionInGeneratedMcpConfig();
2896
3054
  testVendorBossChatCliShouldParseSharedLlmTransportArgs();