Commit 8f4c1b6369dcb58d402008636565e5104341ac44
1 parent
3353b174
fix(27-P3C): 收掉 P3C 欠着的四条
## ① system/tasklog/ 改名保留(§十二.5) 派单存储统一进 tasks 表后,旧目录改名为 system/tasklog.bak-unify/ 而不是 直接删 —— 跑顺一个版本再清。回填脚本改成**两处都认**(先原位置再备份位置, 顺序不能反:万一有人从备份恢复出一份新的 tasklog,那份才是要回填的)。 ## ② agent 提示词里的端口 3000 → 3010 packages/agent-core/base/ 下那几份是**所有 agent 的工具说明**,印着 curl http://localhost:3000/... 而实际端口是 3010。agent 照着打拿到的是 连接被拒、不是「端口错了」,它多半会去猜别的原因。 这不是假想:批 5 回填出来的历史派单正文里,有人手写了「(端口 3010, 不是 3000)」在纠正它 —— 靠人肉在每条任务书里打补丁。 守卫 agent-docs-port.test.js:扫 base 模板与 agent 本体,禁 localhost:3000。 判据刻意宽松(不校验「必须等于 config.port」—— 端口可被 PORT 覆盖,写死会在 部署环境误报)。带「样本非空」前置断言。第一版扫出 5 条误报,全是某 agent 目录下 Python venv 里 playwright 的 ThirdPartyNotices —— 那是第三方文件不是 提示词,已把 venv/site-packages/workspace/conversations/train 一并跳过。 ## ③ 边界守卫不再把并行会话的改动算到 agent 头上(§15.5 第 1 条) 守卫的前提是「窗口内新增的仓库改动 = 这个节点写的」。多会话共用 worktree 时 这个前提是假的 —— git status 的差集里没有「谁写的」这一维。实测已发生: 批 3 的开发改动把一条 run 判成 boundary_escape 卡住。 可检测的信号:节点**开跑那一刻**仓库就已经有源码目录的未提交改动 → 有人正在 这个 worktree 上开发 → 窗口内新增的源码改动无法归属 → **降级为只记证据、 不判罚**(与既有的 unattributable 桶同一语义),而不是关掉守卫:证据照留, 人一眼看得出「这次没判罚,因为当时仓库是脏的」。 BOUNDARY_GUARD=strict 强制判罚(CI / 干净环境),=record 一律只记。 守卫 8 例,含关键对照:仓库脏时不判、干净时同样一处越界仍判 fail —— 守卫没被削弱,只是不再乱扣帽子。 ## ④ TaskTimeline 的标签不再撒谎 conversationCount 在批 5 之后只会是 0/1(「有没有起过执行会话」),而界面 写的是 "Convs: N" —— 读的人会以为这条任务只有一次对话。改成有/无的表达。 agents 的语义变化在注释里说明。 注:innovator-hunt/todo.md 与 reddit-scraper/todo.md 里的端口也改了, 但它们是 gitignored 的运行态文件,不入库。 全量:134 文件 / 1268 测试全绿,web:build 通过。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Showing
10 changed files
with
233 additions
and
14 deletions
.gitignore
| ... | ... | @@ -18,6 +18,8 @@ bot.log |
| 18 | 18 | # 运行态**目录**的规则见文件末尾的白名单块(28 §5.5):深度 2 现在是任意公司 id, |
| 19 | 19 | # 没法再像以前那样逐个枚举 conversations/ logs/ workspace/ … |
| 20 | 20 | system/tasklog/ |
| 21 | +# 批 5 把派单存储统一进 tasks 表后,旧目录改名保留一版再清(27-P3C §十二.5) | |
| 22 | +system/tasklog.bak-unify/ | |
| 21 | 23 | |
| 22 | 24 | # ── Agent 自写的运行态文件(27-P3B W13 提出,28 批次 0 根治)───────────── |
| 23 | 25 | # system agent 的 Claude CLI workDir **就是它自己的源码目录** system/agents/<id>/, | ... | ... |
packages/agent-core/base/common/tools-base.md
| ... | ... | @@ -13,7 +13,7 @@ |
| 13 | 13 | |
| 14 | 14 | ### dispatch_task — 向下级 Agent 分发任务 |
| 15 | 15 | ```bash |
| 16 | -curl -X POST http://localhost:3000/api/agent/task/dispatch \ | |
| 16 | +curl -X POST http://localhost:3010/api/agent/task/dispatch \ | |
| 17 | 17 | -H "Content-Type: application/json" \ |
| 18 | 18 | -d '{"from":"YOUR_AGENT_ID","to":"TARGET_AGENT_ID","task":"任务描述","priority":"P1","goalId":"G-001","nodeId":"G-001.1.1","autoExecute":true}' |
| 19 | 19 | ``` |
| ... | ... | @@ -29,7 +29,7 @@ curl -X POST http://localhost:3000/api/agent/task/dispatch \ |
| 29 | 29 | |
| 30 | 30 | ### report_task — 向上级汇报任务完成 |
| 31 | 31 | ```bash |
| 32 | -curl -X POST http://localhost:3000/api/agent/task/report \ | |
| 32 | +curl -X POST http://localhost:3010/api/agent/task/report \ | |
| 33 | 33 | -H "Content-Type: application/json" \ |
| 34 | 34 | -d '{"taskId":"T-xxx","from":"YOUR_AGENT_ID","to":"SENDER_AGENT_ID","status":"done","summary":"完成摘要","goalId":"G-001","nodeId":"G-001.1.1"}' |
| 35 | 35 | ``` |
| ... | ... | @@ -39,5 +39,5 @@ curl -X POST http://localhost:3000/api/agent/task/report \ |
| 39 | 39 | |
| 40 | 40 | ### 查看 Agent 列表 |
| 41 | 41 | ```bash |
| 42 | -curl -s http://localhost:3000/api/agent/list | |
| 42 | +curl -s http://localhost:3010/api/agent/list | |
| 43 | 43 | ``` | ... | ... |
packages/agent-core/base/cxo/agent-base.md
| ... | ... | @@ -10,12 +10,12 @@ |
| 10 | 10 | |
| 11 | 11 | ```bash |
| 12 | 12 | # Step 1: 通过 agent-creator 创建员工 |
| 13 | -curl -X POST http://localhost:3000/api/agent/task/dispatch \ | |
| 13 | +curl -X POST http://localhost:3010/api/agent/task/dispatch \ | |
| 14 | 14 | -H "Content-Type: application/json" \ |
| 15 | 15 | -d '{"from":"YOUR_ID","to":"agent-creator","task":"创建 XX Agent。要求:id, type, 能力描述, 性格特征","priority":"P0","autoExecute":true}' |
| 16 | 16 | |
| 17 | 17 | # Step 2: 创建完成后,分发任务给新员工 |
| 18 | -curl -X POST http://localhost:3000/api/agent/task/dispatch \ | |
| 18 | +curl -X POST http://localhost:3010/api/agent/task/dispatch \ | |
| 19 | 19 | -H "Content-Type: application/json" \ |
| 20 | 20 | -d '{"from":"YOUR_ID","to":"NEW_AGENT_ID","task":"具体任务","priority":"P0","autoExecute":true}' |
| 21 | 21 | ``` | ... | ... |
scripts/migrate-runs-to-goal-task.js
| ... | ... | @@ -220,7 +220,21 @@ function main() { |
| 220 | 220 | // 批 5:system/tasklog/*.json → tasks 表(executor='agent') |
| 221 | 221 | // ════════════════════════════════════════════════════════════════ |
| 222 | 222 | |
| 223 | -const TASKLOG_DIR = path.join(REPO_ROOT, 'system', 'tasklog'); | |
| 223 | +/** | |
| 224 | + * 派单旧存储的位置。 | |
| 225 | + * | |
| 226 | + * 批 5 把它统一进 `tasks` 表之后,目录**改名**为 `system/tasklog.bak-unify/` 而不是直接删 | |
| 227 | + * (§十二.5:跑顺一个版本再清)。所以这里两处都认:先看原位置(还没改名的库), | |
| 228 | + * 再看备份位置(改过名的)。**顺序不能反** —— 万一有人从备份恢复出一份新的 tasklog, | |
| 229 | + * 那份才是要回填的。 | |
| 230 | + */ | |
| 231 | +function tasklogDir() { | |
| 232 | + for (const d of ['tasklog', 'tasklog.bak-unify']) { | |
| 233 | + const full = path.join(REPO_ROOT, 'system', d); | |
| 234 | + if (fs.existsSync(full)) return full; | |
| 235 | + } | |
| 236 | + return path.join(REPO_ROOT, 'system', 'tasklog'); // 都不在时返回原位置,下面 existsSync 会跳过 | |
| 237 | +} | |
| 224 | 238 | |
| 225 | 239 | /** |
| 226 | 240 | * 派单任务落在哪家公司名下。 |
| ... | ... | @@ -258,7 +272,7 @@ function tasklogAlreadyMigrated(db, taskId) { |
| 258 | 272 | function migrateTasklog(db, file) { |
| 259 | 273 | let meta; |
| 260 | 274 | try { |
| 261 | - meta = JSON.parse(fs.readFileSync(path.join(TASKLOG_DIR, file), 'utf-8')); | |
| 275 | + meta = JSON.parse(fs.readFileSync(path.join(tasklogDir(), file), 'utf-8')); | |
| 262 | 276 | } catch (e) { |
| 263 | 277 | stats.tasklogBad += 1; |
| 264 | 278 | log(`⚠️ ${file} 解析失败,跳过: ${e.message}`); |
| ... | ... | @@ -309,6 +323,7 @@ function migrateTasklog(db, file) { |
| 309 | 323 | } |
| 310 | 324 | |
| 311 | 325 | function migrateAllTasklog(db) { |
| 326 | + const TASKLOG_DIR = tasklogDir(); | |
| 312 | 327 | if (!fs.existsSync(TASKLOG_DIR)) { log('system/tasklog/ 不存在,跳过派单回填'); return; } |
| 313 | 328 | const files = fs.readdirSync(TASKLOG_DIR).filter((f) => f.endsWith('.json')).sort(); |
| 314 | 329 | stats.tasklogFiles = files.length; | ... | ... |
server/services/graph-engine.js
| ... | ... | @@ -2989,6 +2989,40 @@ function beginBoundaryGuard(ctx) { |
| 2989 | 2989 | */ |
| 2990 | 2990 | const AGENT_REGISTRY_TREE_RE = /^system\/agents\//; |
| 2991 | 2991 | |
| 2992 | +/** | |
| 2993 | + * 「这个 worktree 还有别人在写」—— 判罚前必须先回答的问题。 | |
| 2994 | + * | |
| 2995 | + * 守卫的前提是「本次调用窗口内新出现的仓库改动 = 这个节点写的」。多会话共用同一个 | |
| 2996 | + * worktree 时这个前提**是假的**:`git status` 的差集里没有「谁写的」这一维, | |
| 2997 | + * 一个并行的开发会话改 `server/services/x.js`,会被原样记到当时正在跑的那个节点头上。 | |
| 2998 | + * 实测已经发生过:27-P3C 批 3 的开发改动把一条 run 判成了 `boundary_escape` 卡住 | |
| 2999 | + * (§15.5 第 1 条)。 | |
| 3000 | + * | |
| 3001 | + * **可检测的信号**:如果节点**开跑那一刻**宿主仓库就已经有源码目录的未提交改动, | |
| 3002 | + * 那说明有人正在这个 worktree 上开发 —— 此后窗口内新增的源码改动,是他的可能性 | |
| 3003 | + * 至少不低于是 agent 的。这不是猜,是快照里读得出来的事实。 | |
| 3004 | + * | |
| 3005 | + * 这种情况下**降级为只记证据、不判罚**(与 `unattributable` 桶同一语义), | |
| 3006 | + * 而不是关掉守卫:证据照留,人一眼能看出「这次没判罚,因为当时仓库是脏的」。 | |
| 3007 | + * | |
| 3008 | + * `BOUNDARY_GUARD` 可覆盖:`strict` = 无论仓库脏不脏都判罚(CI / 干净环境用); | |
| 3009 | + * `record` = 一律只记不判。缺省是上面那条自动判断。 | |
| 3010 | + */ | |
| 3011 | +const JUDGED_SOURCE_RE = /^(server|web|packages|scripts)\//; | |
| 3012 | + | |
| 3013 | +function sharedWorktreeReason(before) { | |
| 3014 | + const mode = process.env.BOUNDARY_GUARD; | |
| 3015 | + if (mode === 'strict') return null; | |
| 3016 | + if (mode === 'record') return 'BOUNDARY_GUARD=record:本次一律只记证据、不判罚'; | |
| 3017 | + const dirty = String(before == null ? '' : before) | |
| 3018 | + .split('\n').map((l) => l.trim()).filter(Boolean) | |
| 3019 | + .map((l) => l.replace(/^\S{1,2}\s+/, '').trim().replace(/^"|"$/g, '')) | |
| 3020 | + .filter((p) => JUDGED_SOURCE_RE.test(p)); | |
| 3021 | + if (!dirty.length) return null; | |
| 3022 | + return `本节点开跑时宿主仓库已有 ${dirty.length} 处源码未提交改动(如 ${dirty[0]}),` | |
| 3023 | + + '说明有别的会话正在这个 worktree 上开发;窗口内新增的源码改动无法归属到本节点,只记证据不判罚'; | |
| 3024 | +} | |
| 3025 | + | |
| 2992 | 3026 | function splitUnattributable(entries) { |
| 2993 | 3027 | const escapes = []; |
| 2994 | 3028 | const unattributable = []; |
| ... | ... | @@ -3019,7 +3053,21 @@ function applyBoundaryGuard(ctx, before, outcome, injectedAfter = undefined) { |
| 3019 | 3053 | if (before === null || before === undefined) return outcome; |
| 3020 | 3054 | const after = injectedAfter === undefined ? hostRepoStatus() : injectedAfter; |
| 3021 | 3055 | if (after === null) return outcome; |
| 3022 | - const { escapes, unattributable } = splitUnattributable(diffRepoStatus(before, after)); | |
| 3056 | + let { escapes, unattributable } = splitUnattributable(diffRepoStatus(before, after)); | |
| 3057 | + | |
| 3058 | + // 多会话共用 worktree 时,源码改动无法归属到本节点 —— 全部降级为「只记证据」 | |
| 3059 | + const sharedReason = escapes.length ? sharedWorktreeReason(before) : null; | |
| 3060 | + if (sharedReason) { | |
| 3061 | + timelineNote(ctx, 'node.boundary', | |
| 3062 | + `本次调用窗口内有 ${escapes.length} 处仓库改动,但无法归属到本节点,不计入越界`, { | |
| 3063 | + node_run_id: ctx.nodeRunId, attempt: ctx.attempt, violation: null, | |
| 3064 | + entries: escapes.slice(0, 20), reason: sharedReason, | |
| 3065 | + note: '守卫的前提是「窗口内新增的改动 = 本节点写的」,而 git status 的差集里没有' | |
| 3066 | + + '「谁写的」这一维。要在干净 worktree 上强制判罚,设 BOUNDARY_GUARD=strict。', | |
| 3067 | + }); | |
| 3068 | + unattributable = unattributable.concat(escapes); | |
| 3069 | + escapes = []; | |
| 3070 | + } | |
| 3023 | 3071 | |
| 3024 | 3072 | if (unattributable.length) { |
| 3025 | 3073 | timelineNote(ctx, 'node.boundary', |
| ... | ... | @@ -3824,6 +3872,7 @@ module.exports = { |
| 3824 | 3872 | applyBoundaryGuard, |
| 3825 | 3873 | // 归属判定:agent 运行态文件不算越界(真跑冒烟的断点就在这,见函数上方注释) |
| 3826 | 3874 | splitUnattributable, |
| 3875 | + sharedWorktreeReason, | |
| 3827 | 3876 | |
| 3828 | 3877 | // 选边 / 熔断 / 透传 |
| 3829 | 3878 | selectFailEdge, | ... | ... |
server/tests/unit/agent-docs-port.test.js
0 → 100644
| 1 | +/** | |
| 2 | + * agent 提示词里印的端口必须是真的。 | |
| 3 | + * | |
| 4 | + * 为什么要一条守卫:`packages/agent-core/base/` 下那几份是**所有 agent 的工具说明** | |
| 5 | + * (composed 进每个 agent 的提示词),里面印着 `curl http://localhost:3000/api/agent/...`, | |
| 6 | + * 而实际端口是 **3010**(server/config.js)。agent 照着打就连不上,而它拿到的是 | |
| 7 | + * 连接被拒、不是一句「端口错了」—— 它多半会去猜别的原因。 | |
| 8 | + * | |
| 9 | + * 这不是假想:批 5 回填出来的历史派单正文里,有人手写了 | |
| 10 | + * 「完成后调用 report_task 汇报给 innovator(端口 3010,不是 3000)」来纠正它 —— | |
| 11 | + * 说明这个错误已经真咬过人,而且是靠人肉在每条任务书里打补丁。 | |
| 12 | + * | |
| 13 | + * 判据刻意宽松:只禁 `localhost:3000`(那个确定是错的),不去校验「必须等于 | |
| 14 | + * config.port」—— 端口可被 PORT 覆盖,写死校验反而会在部署环境里误报。 | |
| 15 | + */ | |
| 16 | +import { describe, it, expect } from 'vitest'; | |
| 17 | +import { createRequire } from 'module'; | |
| 18 | +import fs from 'fs'; | |
| 19 | +import path from 'path'; | |
| 20 | +const require_ = createRequire(import.meta.url); | |
| 21 | + | |
| 22 | +const ROOT = path.resolve(__dirname, '../../..'); | |
| 23 | +const SCAN = ['packages/agent-core/base', 'system/agents']; | |
| 24 | +// agent 目录下会长出运行态产物:Python venv、node_modules、下载的浏览器驱动…… | |
| 25 | +// 那些是**第三方文件,不是提示词**,扫进来只会拿 playwright 的 ThirdPartyNotices 当违例 | |
| 26 | +// (实测第一版就是这么红的)。守卫要盯的是人写给 agent 看的东西。 | |
| 27 | +const SKIP_DIR = /(^|\/)(node_modules|\.git|dist|coverage|venv|\.venv|site-packages|__pycache__|workspace|conversations|train)(\/|$)/; | |
| 28 | + | |
| 29 | +function walk(dir, out = []) { | |
| 30 | + if (!fs.existsSync(dir) || SKIP_DIR.test(dir)) return out; | |
| 31 | + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { | |
| 32 | + const full = path.join(dir, e.name); | |
| 33 | + if (e.isDirectory()) walk(full, out); | |
| 34 | + else if (/\.(md|json|txt)$/.test(e.name)) out.push(full); | |
| 35 | + } | |
| 36 | + return out; | |
| 37 | +} | |
| 38 | + | |
| 39 | +describe('agent 提示词里的端口', () => { | |
| 40 | + const files = SCAN.flatMap((d) => walk(path.join(ROOT, d))); | |
| 41 | + | |
| 42 | + it('扫描样本非空 —— 防止路径写错时本测试退化成永远通过', () => { | |
| 43 | + // 少了这条前置断言,SCAN 里的路径一旦写错就会以「零违例」的形式全绿, | |
| 44 | + // 而一条永远通过的守卫和没有守卫是一回事。 | |
| 45 | + expect(files.length).toBeGreaterThan(50); | |
| 46 | + }); | |
| 47 | + | |
| 48 | + it('不许再出现 localhost:3000(实际端口是 3010)', () => { | |
| 49 | + const offenders = []; | |
| 50 | + for (const f of files) { | |
| 51 | + const src = fs.readFileSync(f, 'utf-8'); | |
| 52 | + src.split('\n').forEach((line, i) => { | |
| 53 | + if (line.includes('localhost:3000') || line.includes('127.0.0.1:3000')) { | |
| 54 | + offenders.push(`${path.relative(ROOT, f)}:${i + 1} → ${line.trim().slice(0, 90)}`); | |
| 55 | + } | |
| 56 | + }); | |
| 57 | + } | |
| 58 | + expect(offenders).toEqual([]); | |
| 59 | + }); | |
| 60 | + | |
| 61 | + it('config.port 的缺省确实是 3010(守卫的前提,变了要先改这里)', () => { | |
| 62 | + const cfg = require_('../../config'); | |
| 63 | + expect(cfg.port).toBe(3010); | |
| 64 | + }); | |
| 65 | +}); | ... | ... |
server/tests/unit/boundary-guard-shared-worktree.test.js
0 → 100644
| 1 | +/** | |
| 2 | + * 边界守卫在**多会话共用 worktree** 时不许乱判罚(27-P3C §15.5 第 1 条)。 | |
| 3 | + * | |
| 4 | + * 守卫的前提是「本次调用窗口内新出现的仓库改动 = 这个节点写的」。 | |
| 5 | + * 多会话共用同一个 worktree 时这个前提是假的 —— `git status` 的差集里 | |
| 6 | + * **没有「谁写的」这一维**。实测已经发生过:批 3 的开发改动把一条 run 判成 | |
| 7 | + * boundary_escape 卡住,而那些文件是并行的开发会话写的。 | |
| 8 | + * | |
| 9 | + * 可检测的信号:节点**开跑那一刻**仓库就已经有源码目录的未提交改动 → | |
| 10 | + * 有人正在这个 worktree 上开发 → 窗口内新增的源码改动无法归属 → 只记证据不判罚。 | |
| 11 | + */ | |
| 12 | +import { describe, it, expect, afterEach } from 'vitest'; | |
| 13 | +import { createRequire } from 'module'; | |
| 14 | +const require_ = createRequire(import.meta.url); | |
| 15 | +const engine = require_('../../services/graph-engine'); | |
| 16 | + | |
| 17 | +const OLD = process.env.BOUNDARY_GUARD; | |
| 18 | +afterEach(() => { | |
| 19 | + if (OLD === undefined) delete process.env.BOUNDARY_GUARD; | |
| 20 | + else process.env.BOUNDARY_GUARD = OLD; | |
| 21 | +}); | |
| 22 | + | |
| 23 | +describe('边界守卫 · 多会话 worktree', () => { | |
| 24 | + it('开跑时仓库干净 → 照常判罚(CI / 干净环境不受影响)', () => { | |
| 25 | + delete process.env.BOUNDARY_GUARD; | |
| 26 | + expect(engine.sharedWorktreeReason('')).toBeNull(); | |
| 27 | + // 只有 agent 运行态的脏,不算「有人在开发」 | |
| 28 | + expect(engine.sharedWorktreeReason('?? system/agents/foo/\n M data/companies/x/company.json')).toBeNull(); | |
| 29 | + }); | |
| 30 | + | |
| 31 | + it('开跑时源码目录已脏 → 认定有别的会话在写,降级为只记证据', () => { | |
| 32 | + delete process.env.BOUNDARY_GUARD; | |
| 33 | + const reason = engine.sharedWorktreeReason(' M server/services/task-scheduler.js\n M web/src/views/GraphConsole.vue'); | |
| 34 | + expect(reason).toBeTruthy(); | |
| 35 | + expect(reason).toContain('server/services/task-scheduler.js'); // 点名证据,不是一句空话 | |
| 36 | + }); | |
| 37 | + | |
| 38 | + it('路径含空格/中文被 git 加了引号时也认得出来', () => { | |
| 39 | + delete process.env.BOUNDARY_GUARD; | |
| 40 | + expect(engine.sharedWorktreeReason(' M "server/services/新 文件.js"')).toBeTruthy(); | |
| 41 | + }); | |
| 42 | + | |
| 43 | + it('未跟踪的新源码文件(?? 两字符状态位)同样算', () => { | |
| 44 | + delete process.env.BOUNDARY_GUARD; | |
| 45 | + expect(engine.sharedWorktreeReason('?? server/services/brand-new.js')).toBeTruthy(); | |
| 46 | + }); | |
| 47 | + | |
| 48 | + it('BOUNDARY_GUARD=strict 强制判罚,脏仓库也不放过', () => { | |
| 49 | + process.env.BOUNDARY_GUARD = 'strict'; | |
| 50 | + expect(engine.sharedWorktreeReason(' M server/services/task-scheduler.js')).toBeNull(); | |
| 51 | + }); | |
| 52 | + | |
| 53 | + it('BOUNDARY_GUARD=record 一律只记不判,干净仓库也是', () => { | |
| 54 | + process.env.BOUNDARY_GUARD = 'record'; | |
| 55 | + expect(engine.sharedWorktreeReason('')).toBeTruthy(); | |
| 56 | + }); | |
| 57 | + | |
| 58 | + it('降级之后 outcome 不被改成 fail,且证据仍在 escapes 之外留档', () => { | |
| 59 | + delete process.env.BOUNDARY_GUARD; | |
| 60 | + const notes = []; | |
| 61 | + const ctx = { | |
| 62 | + nodeRunId: 'rn_1', attempt: 1, projectRoot: '/tmp/proj', | |
| 63 | + // timelineNote 走的是 sse,这里只验 outcome 不被降级;证据面由上面几条覆盖 | |
| 64 | + }; | |
| 65 | + const before = ' M server/services/task-scheduler.js'; // 开跑时就脏 | |
| 66 | + const after = before + '\n M server/services/graph-engine.js'; // 窗口内又多一处 | |
| 67 | + const out = engine.applyBoundaryGuard(ctx, before, { status: 'success', output: 'ok' }, after); | |
| 68 | + expect(out.status).toBe('success'); // ← 修之前这里会被判成 fail | |
| 69 | + expect(out.failReason).toBeFalsy(); | |
| 70 | + expect(notes).toEqual([]); | |
| 71 | + }); | |
| 72 | + | |
| 73 | + it('对照:仓库开跑时是干净的 → 同样一处越界仍然判 fail', () => { | |
| 74 | + delete process.env.BOUNDARY_GUARD; | |
| 75 | + const out = engine.applyBoundaryGuard( | |
| 76 | + { nodeRunId: 'rn_2', attempt: 1, projectRoot: '/tmp/proj' }, | |
| 77 | + '', { status: 'success', output: 'ok' }, ' M server/services/graph-engine.js', | |
| 78 | + ); | |
| 79 | + expect(out.status).toBe('fail'); | |
| 80 | + expect(out.failReason).toBe('boundary_escape'); | |
| 81 | + }); | |
| 82 | +}); | ... | ... |
system/agents/hr/agent.md
| ... | ... | @@ -67,7 +67,7 @@ node bin/lemon.js task dispatch --from <你自己ID> --to innovator-001 --task " |
| 67 | 67 | 当被心跳唤醒或收到 CTO/COO 查询时,执行管理检查: |
| 68 | 68 | ```bash |
| 69 | 69 | # 查看公司整体状态和所有员工心跳 |
| 70 | -curl -s http://localhost:3000/api/company/<公司ID>/status | node -e " | |
| 70 | +curl -s http://localhost:3010/api/company/<公司ID>/status | node -e " | |
| 71 | 71 | const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); |
| 72 | 72 | d.agents.forEach(a => console.log(a.id, a.heartbeatActive ? '✅在线' : '❌离线', 'interval:'+a.heartbeatInterval+'min')); |
| 73 | 73 | " | ... | ... |
system/agents/qa-judge/agent.md
| ... | ... | @@ -415,7 +415,7 @@ Step 6: 溯源分类 → 粗心错误 vs 能力缺陷判定 |
| 415 | 415 | |
| 416 | 416 | **REWORK 场景**: |
| 417 | 417 | ```bash |
| 418 | -curl -X POST http://localhost:3000/api/agent/task/dispatch \ | |
| 418 | +curl -X POST http://localhost:3010/api/agent/task/dispatch \ | |
| 419 | 419 | -H "Content-Type: application/json" \ |
| 420 | 420 | -d '{ |
| 421 | 421 | "from": "ceo", |
| ... | ... | @@ -431,7 +431,7 @@ curl -X POST http://localhost:3000/api/agent/task/dispatch \ |
| 431 | 431 | |
| 432 | 432 | **RETRAIN_REQUIRED 场景**: |
| 433 | 433 | ```bash |
| 434 | -curl -X POST http://localhost:3000/api/agent/task/dispatch \ | |
| 434 | +curl -X POST http://localhost:3010/api/agent/task/dispatch \ | |
| 435 | 435 | -H "Content-Type: application/json" \ |
| 436 | 436 | -d '{ |
| 437 | 437 | "from": "ceo", | ... | ... |
web/src/views/TaskTimeline.vue
| ... | ... | @@ -48,10 +48,16 @@ |
| 48 | 48 | </span> |
| 49 | 49 | </div> |
| 50 | 50 | |
| 51 | + <!-- 27-P3C 批 5:任务列表改读 tasks 表之后,这里两个字段的语义变了 —— | |
| 52 | + · conversationCount 从「扫出来的会话文件数」变成「有没有起过执行会话」(0/1)。 | |
| 53 | + 再写 "Convs: 1" 是**标签在撒谎**(读的人会以为这条任务只有一次对话), | |
| 54 | + 所以改成有/无的表达,没有就不占位。真要看内容点进去走 timeline。 | |
| 55 | + · agents 从「会话里出现过的 agent 集合」变成「谁派的 + 派给谁」,最多两个, | |
| 56 | + 上面那段 slice(0,4) + "+N" 因此不会再触发;留着不碍事,也不值得为它冒改动风险。 --> | |
| 51 | 57 | <div class="task-card-meta"> |
| 52 | - Convs: {{ task.conversationCount }} | |
| 53 | - <template v-if="task.children && task.children.length"> · 🔗 {{ task.children.length }} subtask(s)</template> | |
| 54 | - · {{ formatTime(task.updatedAt) }} | |
| 58 | + <template v-if="task.conversationCount">💬 有执行会话 · </template> | |
| 59 | + <template v-if="task.children && task.children.length">🔗 {{ task.children.length }} subtask(s) · </template> | |
| 60 | + {{ formatTime(task.updatedAt) }} | |
| 55 | 61 | </div> |
| 56 | 62 | </div> |
| 57 | 63 | ... | ... |