Commit e12064590a2df282d23b88c8c92dab0a8a92611d
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>
Showing
6 changed files
with
91 additions
and
13 deletions
scripts/extract-agent-prompts.js
| @@ -21,7 +21,7 @@ const path = require('path'); | @@ -21,7 +21,7 @@ const path = require('path'); | ||
| 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('/templates/:templateId', (req, res) => { | @@ -133,7 +133,7 @@ router.patch('/templates/:templateId', (req, res) => { | ||
| 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('/templates/:templateId', (req, res) => { | @@ -178,7 +178,7 @@ router.delete('/templates/:templateId', (req, res) => { | ||
| 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('/templates/:templateId/generate', async (req, res) => { | @@ -257,7 +257,7 @@ router.post('/templates/:templateId/generate', async (req, res) => { | ||
| 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('/templates/:templateId/train', async (req, res) => { | @@ -294,7 +294,7 @@ router.post('/templates/:templateId/train', async (req, res) => { | ||
| 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('/templates/:templateId/skills/upload', skillUpload.array('files', 5 | @@ -356,7 +356,7 @@ router.post('/templates/:templateId/skills/upload', skillUpload.array('files', 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('/templates/:templateId/provision', async (req, res) => { | @@ -482,7 +482,7 @@ router.post('/templates/:templateId/provision', async (req, res) => { | ||
| 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('/templates/:templateId/files', (req, res) => { | @@ -595,7 +595,7 @@ router.get('/templates/:templateId/files', (req, res) => { | ||
| 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('/templates/:templateId/files/content', (req, res) => { | @@ -651,7 +651,7 @@ router.get('/templates/:templateId/files/content', (req, res) => { | ||
| 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('/templates/:templateId/files/content', (req, res) => { | @@ -682,7 +682,7 @@ router.put('/templates/:templateId/files/content', (req, res) => { | ||
| 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('path'); | @@ -3,7 +3,7 @@ const path = require('path'); | ||
| 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('path'); | @@ -23,7 +23,7 @@ const path = require('path'); | ||
| 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 | +}); |
-
mentioned in commit be8de85b743a8e9ec651f1eb79e531ddd10fafa1