Commit e12064590a2df282d23b88c8c92dab0a8a92611d

Authored by yibo
1 parent 367b4d50

refactor(paths): 本体根目录收敛到 locator.AGENTS_ROOT 单一持有者

28 号把本体根从 system/templates/ 搬到 system/agents/,agent-locator 就是
为了当这个路径的唯一持有者而建的。但搬完之后仓库里仍有 13 处自己拼这个路径,
等于 locator 只是多了一份副本、没有真正收敛:

  server/routes/company.js            9 处(6 个 TEMPLATES_DIR + 3 个 DATA_DIR,
                                          同一文件里只有 1 处用了 locator)
  server/services/task-service.js     1 处
  server/src/core/skill-config-loader.js  1 处
  server/src/core/devloop/loop-controller.js  1 处
  scripts/extract-agent-prompts.js    1 处

危害不是「不好看」,而是下一次再搬时改 12 处、漏 1 处:漏掉的那处指向一个
不存在的目录,而本仓库读这类路径几乎都包在 fs.existsSync() 守卫里 ——
目录没了不报错,只是静默跳过。这个形状刚在 scripts/demo-harness-company.js
上实测过(base 本体没挂上,demo 跑出来的 agent 没有四段式骨架,脚本照常打印成功)。

局部变量名保留不动(TEMPLATES_DIR / DATA_DIR / SYSTEM_AGENTS_DIR / agentsRoot /
OUT_DIR),只把取值改成 locator.AGENTS_ROOT —— 名字是局部别名,无害;
「路径这个事实」则收敛成一处,下次再搬只改 agent-locator.js。

新增 server/tests/unit/agents-root-single-holder.test.js 锁住这条不变式:
扫 server/ packages/ scripts/ 下所有源码,禁止 path.resolve/join 自己拼
「以 ../ 开头、以 system/agents 结尾」的字面量。带「样本非空」前置断言,
防止扫描路径写错时退化成永远通过。改坏一处实测能精确点名到文件行号。

线索来自并行会话(它在准备改 company.js 时发现同一文件里 7 份定义)。
实际比它看到的更多:它只查了 TEMPLATES_DIR,同文件另有 3 个 DATA_DIR、
另外 3 个文件各 1 处。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
scripts/extract-agent-prompts.js
@@ -21,7 +21,7 @@ const path = require(&#39;path&#39;); @@ -21,7 +21,7 @@ const path = require(&#39;path&#39;);
21 21
22 const ROLES = ['pm', 'architect', 'coder', 'tester', 'integrator', 'triager']; 22 const ROLES = ['pm', 'architect', 'coder', 'tester', 'integrator', 'triager'];
23 const SRC_DIR = path.resolve(__dirname, '../docs/prd/newfunction'); 23 const SRC_DIR = path.resolve(__dirname, '../docs/prd/newfunction');
24 -const OUT_DIR = path.resolve(__dirname, '../system/agents'); 24 +const OUT_DIR = require('../server/services/agent-locator').AGENTS_ROOT;
25 25
26 function decodeHtmlEntities(s) { 26 function decodeHtmlEntities(s) {
27 return s 27 return s
server/routes/company.js
@@ -133,7 +133,7 @@ router.patch(&#39;/templates/:templateId&#39;, (req, res) =&gt; { @@ -133,7 +133,7 @@ router.patch(&#39;/templates/:templateId&#39;, (req, res) =&gt; {
133 133
134 const path = require('path'); 134 const path = require('path');
135 const fs = require('fs'); 135 const fs = require('fs');
136 - const TEMPLATES_DIR = path.resolve(__dirname, '../../system/agents'); 136 + const TEMPLATES_DIR = locator.AGENTS_ROOT;
137 const templateDir = path.join(TEMPLATES_DIR, templateId); 137 const templateDir = path.join(TEMPLATES_DIR, templateId);
138 138
139 if (!fs.existsSync(templateDir)) { 139 if (!fs.existsSync(templateDir)) {
@@ -178,7 +178,7 @@ router.delete(&#39;/templates/:templateId&#39;, (req, res) =&gt; { @@ -178,7 +178,7 @@ router.delete(&#39;/templates/:templateId&#39;, (req, res) =&gt; {
178 const { templateId } = req.params; 178 const { templateId } = req.params;
179 const path = require('path'); 179 const path = require('path');
180 const fs = require('fs'); 180 const fs = require('fs');
181 - const TEMPLATES_DIR = path.resolve(__dirname, '../../system/agents'); 181 + const TEMPLATES_DIR = locator.AGENTS_ROOT;
182 const templateDir = path.join(TEMPLATES_DIR, templateId); 182 const templateDir = path.join(TEMPLATES_DIR, templateId);
183 183
184 if (!fs.existsSync(templateDir)) { 184 if (!fs.existsSync(templateDir)) {
@@ -257,7 +257,7 @@ router.post(&#39;/templates/:templateId/generate&#39;, async (req, res) =&gt; { @@ -257,7 +257,7 @@ router.post(&#39;/templates/:templateId/generate&#39;, async (req, res) =&gt; {
257 const path = require('path'); 257 const path = require('path');
258 const fs = require('fs'); 258 const fs = require('fs');
259 const config = require('../config'); 259 const config = require('../config');
260 - const TEMPLATES_DIR = path.resolve(__dirname, '../../system/agents'); 260 + const TEMPLATES_DIR = locator.AGENTS_ROOT;
261 const templateDir = path.join(TEMPLATES_DIR, templateId); 261 const templateDir = path.join(TEMPLATES_DIR, templateId);
262 262
263 if (!fs.existsSync(templateDir)) { 263 if (!fs.existsSync(templateDir)) {
@@ -294,7 +294,7 @@ router.post(&#39;/templates/:templateId/train&#39;, async (req, res) =&gt; { @@ -294,7 +294,7 @@ router.post(&#39;/templates/:templateId/train&#39;, async (req, res) =&gt; {
294 const path = require('path'); 294 const path = require('path');
295 const fs = require('fs'); 295 const fs = require('fs');
296 const config = require('../config'); 296 const config = require('../config');
297 - const TEMPLATES_DIR = path.resolve(__dirname, '../../system/agents'); 297 + const TEMPLATES_DIR = locator.AGENTS_ROOT;
298 const templateDir = path.join(TEMPLATES_DIR, templateId); 298 const templateDir = path.join(TEMPLATES_DIR, templateId);
299 299
300 if (!fs.existsSync(templateDir)) { 300 if (!fs.existsSync(templateDir)) {
@@ -356,7 +356,7 @@ router.post(&#39;/templates/:templateId/skills/upload&#39;, skillUpload.array(&#39;files&#39;, 5 @@ -356,7 +356,7 @@ router.post(&#39;/templates/:templateId/skills/upload&#39;, skillUpload.array(&#39;files&#39;, 5
356 try { 356 try {
357 const { templateId } = req.params; 357 const { templateId } = req.params;
358 const targetSubdir = req.body.targetDir || '.claude/skills'; // default to .claude/skills/ (Claude Code ACP reads from here) 358 const targetSubdir = req.body.targetDir || '.claude/skills'; // default to .claude/skills/ (Claude Code ACP reads from here)
359 - const TEMPLATES_DIR = path.resolve(__dirname, '../../system/agents'); 359 + const TEMPLATES_DIR = locator.AGENTS_ROOT;
360 const templateDir = path.join(TEMPLATES_DIR, templateId); 360 const templateDir = path.join(TEMPLATES_DIR, templateId);
361 361
362 if (!fs.existsSync(templateDir)) { 362 if (!fs.existsSync(templateDir)) {
@@ -482,7 +482,7 @@ router.post(&#39;/templates/:templateId/provision&#39;, async (req, res) =&gt; { @@ -482,7 +482,7 @@ router.post(&#39;/templates/:templateId/provision&#39;, async (req, res) =&gt; {
482 // 确定工作目录 482 // 确定工作目录
483 // 原来写的是 `../../data/system/templates` —— 一个从来不存在的路径(`data/` 多余), 483 // 原来写的是 `../../data/system/templates` —— 一个从来不存在的路径(`data/` 多余),
484 // 所以不带 agentDir 的调用一直是 404。顺手改正到合并后的本体目录。 484 // 所以不带 agentDir 的调用一直是 404。顺手改正到合并后的本体目录。
485 - const TEMPLATES_DIR = path.resolve(__dirname, '../../system/agents'); 485 + const TEMPLATES_DIR = locator.AGENTS_ROOT;
486 let workDir; 486 let workDir;
487 if (agentDir) { 487 if (agentDir) {
488 // 公司 agent 目录 488 // 公司 agent 目录
@@ -595,7 +595,7 @@ router.get(&#39;/templates/:templateId/files&#39;, (req, res) =&gt; { @@ -595,7 +595,7 @@ router.get(&#39;/templates/:templateId/files&#39;, (req, res) =&gt; {
595 try { 595 try {
596 const path = require('path'); 596 const path = require('path');
597 const fs = require('fs'); 597 const fs = require('fs');
598 - const DATA_DIR = path.resolve(__dirname, '../../system/agents'); 598 + const DATA_DIR = locator.AGENTS_ROOT;
599 const templateDir = path.join(DATA_DIR, req.params.templateId); 599 const templateDir = path.join(DATA_DIR, req.params.templateId);
600 600
601 if (!fs.existsSync(templateDir)) { 601 if (!fs.existsSync(templateDir)) {
@@ -651,7 +651,7 @@ router.get(&#39;/templates/:templateId/files/content&#39;, (req, res) =&gt; { @@ -651,7 +651,7 @@ router.get(&#39;/templates/:templateId/files/content&#39;, (req, res) =&gt; {
651 try { 651 try {
652 const path = require('path'); 652 const path = require('path');
653 const fs = require('fs'); 653 const fs = require('fs');
654 - const DATA_DIR = path.resolve(__dirname, '../../system/agents'); 654 + const DATA_DIR = locator.AGENTS_ROOT;
655 const filePath = req.query.path; 655 const filePath = req.query.path;
656 656
657 if (!filePath) return res.status(400).json({ error: 'path 参数为必填' }); 657 if (!filePath) return res.status(400).json({ error: 'path 参数为必填' });
@@ -682,7 +682,7 @@ router.put(&#39;/templates/:templateId/files/content&#39;, (req, res) =&gt; { @@ -682,7 +682,7 @@ router.put(&#39;/templates/:templateId/files/content&#39;, (req, res) =&gt; {
682 try { 682 try {
683 const path = require('path'); 683 const path = require('path');
684 const fs = require('fs'); 684 const fs = require('fs');
685 - const DATA_DIR = path.resolve(__dirname, '../../system/agents'); 685 + const DATA_DIR = locator.AGENTS_ROOT;
686 const filePath = req.body.path; 686 const filePath = req.body.path;
687 687
688 if (!filePath) return res.status(400).json({ error: 'path 参数为必填' }); 688 if (!filePath) return res.status(400).json({ error: 'path 参数为必填' });
server/services/task-service.js
@@ -3,7 +3,7 @@ const path = require(&#39;path&#39;); @@ -3,7 +3,7 @@ const path = require(&#39;path&#39;);
3 const locator = require('./agent-locator'); 3 const locator = require('./agent-locator');
4 const { listCompanyAgents } = require('./company-service'); 4 const { listCompanyAgents } = require('./company-service');
5 5
6 -const SYSTEM_AGENTS_DIR = path.resolve(__dirname, '../../system/agents'); 6 +const SYSTEM_AGENTS_DIR = require('./agent-locator').AGENTS_ROOT;
7 const TASK_META_DIR = path.resolve(__dirname, '../../system/tasklog'); 7 const TASK_META_DIR = path.resolve(__dirname, '../../system/tasklog');
8 8
9 /** 9 /**
server/src/core/devloop/loop-controller.js
@@ -35,7 +35,7 @@ unitRegistry.attachSseBroker(sseBroker); @@ -35,7 +35,7 @@ unitRegistry.attachSseBroker(sseBroker);
35 bugTracker.attachSseBroker(sseBroker); 35 bugTracker.attachSseBroker(sseBroker);
36 bugTracker.attachTimelineLogger(timelineLogger); 36 bugTracker.attachTimelineLogger(timelineLogger);
37 37
38 -const TEMPLATES_DIR = path.resolve(__dirname, '../../../../system/agents'); 38 +const TEMPLATES_DIR = require('../../../services/agent-locator').AGENTS_ROOT;
39 39
40 /** 40 /**
41 * 新建项目: 41 * 新建项目:
server/src/core/skill-config-loader.js
@@ -23,7 +23,7 @@ const path = require(&#39;path&#39;); @@ -23,7 +23,7 @@ const path = require(&#39;path&#39;);
23 function loadConfigFromSkills(skillsCodeDir, backendsConfig = {}) { 23 function loadConfigFromSkills(skillsCodeDir, backendsConfig = {}) {
24 // 扫描所有 Agent 专属的技能挂载目录 (符合 Local File-System First) 24 // 扫描所有 Agent 专属的技能挂载目录 (符合 Local File-System First)
25 const dirsToScan = []; 25 const dirsToScan = [];
26 - const agentsRoot = path.resolve(__dirname, '../../../system/agents'); 26 + const agentsRoot = require('../../services/agent-locator').AGENTS_ROOT;
27 if (fs.existsSync(agentsRoot)) { 27 if (fs.existsSync(agentsRoot)) {
28 const agentFolders = fs.readdirSync(agentsRoot, { withFileTypes: true }); 28 const agentFolders = fs.readdirSync(agentsRoot, { withFileTypes: true });
29 for (const f of agentFolders) { 29 for (const f of agentFolders) {
server/tests/unit/agents-root-single-holder.test.js 0 → 100644
  1 +/**
  2 + * 「本体根目录」这个事实只能有一个持有者:`agent-locator.AGENTS_ROOT`。
  3 + *
  4 + * 为什么需要这条元测试(28 号改造的直接教训):
  5 + * 28 号把本体根从 `system/templates/` 搬到 `system/agents/`,搬之前这个路径在
  6 + * 5 个文件里被硬编码了 12 份(`server/routes/company.js` 一个文件里就有 9 份 ——
  7 + * 6 个 `TEMPLATES_DIR` + 3 个 `DATA_DIR`,只有 1 处用了权威访问器)。
  8 + *
  9 + * 12 份副本的危害不是「不好看」,而是**下一次再搬时改 11 处、漏 1 处**:
  10 + * 漏掉的那处会指向一个不存在的目录,而本仓库读这类路径几乎都包在
  11 + * `fs.existsSync()` 守卫里 —— 目录没了不报错,只是静默跳过(并行会话在
  12 + * `scripts/demo-harness-company.js` 实测过这个形状:base 本体没挂上,
  13 + * demo 跑出来的 agent 没有四段式骨架,脚本照常打印成功)。
  14 + *
  15 + * 同一个病在本仓库已有多个实例:`canDispatch` 的取值域被前端复述过一份、
  16 + * `ACTIVE_ROOT_RUN_STATUS` 在两个文件里逐字重复、`isUngraduatedStatus()` 的取值域
  17 + * 一度按「≠ active 就挡」判而误伤 36 个本体。**同一个事实只能有一处持有者 —— 路径也是事实。**
  18 + */
  19 +
  20 +const fs = require('fs');
  21 +const path = require('path');
  22 +
  23 +const REPO_ROOT = path.resolve(__dirname, '../../..');
  24 +
  25 +/** 只扫真正会被执行的源码,跳过依赖、构建产物、测试自身与 locator 本身 */
  26 +const SCAN_DIRS = ['server', 'packages', 'scripts'];
  27 +const SKIP_DIR = /(^|\/)(node_modules|dist|\.git|coverage)(\/|$)/;
  28 +const SKIP_FILE = /\.test\.js$|agent-locator\.js$/;
  29 +
  30 +/**
  31 + * 「拼出本体根目录」的形状:path.resolve/join 里出现以 ../ 开头、以 system/agents 结尾的字面量。
  32 + * 只认路径构造,不认注释里提到的字符串 —— 注释里写 `system/agents/` 是正常的文档行为。
  33 + */
  34 +const HARDCODED = /path\.(?:resolve|join)\([^)]*['"`][^'"`]*\.\.\/[^'"`]*system\/agents['"`]/;
  35 +
  36 +function walk(dir, out = []) {
  37 + let entries;
  38 + try {
  39 + entries = fs.readdirSync(dir, { withFileTypes: true });
  40 + } catch {
  41 + return out; // 目录不存在(如仓库裁剪过)就跳过,不让本测试成为噪声源
  42 + }
  43 + for (const e of entries) {
  44 + const full = path.join(dir, e.name);
  45 + if (SKIP_DIR.test(full)) continue;
  46 + if (e.isDirectory()) walk(full, out);
  47 + else if (e.name.endsWith('.js') && !SKIP_FILE.test(e.name)) out.push(full);
  48 + }
  49 + return out;
  50 +}
  51 +
  52 +describe('本体根目录只有一个持有者(28 §5.1)', () => {
  53 + const files = SCAN_DIRS.flatMap((d) => walk(path.join(REPO_ROOT, d)));
  54 +
  55 + it('扫描样本非空 —— 防止路径写错时本测试退化成永远通过', () => {
  56 + // 没有这一条,上面 walk() 的容错会让「一个文件都没扫到」表现为全绿。
  57 + expect(files.length).toBeGreaterThan(100);
  58 + });
  59 +
  60 + it('没有任何文件自己拼 system/agents 根路径(一律走 locator.AGENTS_ROOT)', () => {
  61 + const offenders = [];
  62 + for (const f of files) {
  63 + const src = fs.readFileSync(f, 'utf-8');
  64 + src.split('\n').forEach((line, i) => {
  65 + if (HARDCODED.test(line)) {
  66 + offenders.push(`${path.relative(REPO_ROOT, f)}:${i + 1} → ${line.trim()}`);
  67 + }
  68 + });
  69 + }
  70 + expect(offenders).toEqual([]);
  71 + });
  72 +
  73 + it('locator.AGENTS_ROOT 指向真实存在的目录', () => {
  74 + const { AGENTS_ROOT } = require('../../services/agent-locator');
  75 + expect(fs.existsSync(AGENTS_ROOT)).toBe(true);
  76 + expect(path.basename(AGENTS_ROOT)).toBe('agents');
  77 + });
  78 +});