// team.js —— 多 Agent 员工团队生成(需求 → 带约束的 AI 员工团队 → 打包 zip) // 移植自「提示词提取」项目的「生成员工」视图。纯逻辑(prompt / 归一化 / 序列化 / 脚手架) // 原样搬运;LLM 通道改走本应用的 router 分组(主进程 fetch /v1/chat/completions), // 打包改用零依赖 zip.createZip。 "use strict"; const router = require("./router"); const { createZip } = require("./zip"); const { EMPLOYEE_SYSTEM_PROMPT, buildEmployeeInstruction, TEAM_CONVENTIONS, ORCHESTRATOR_WORKFLOW, TASK_CHARTER, SECURITY_CHECKLIST, } = require("./team-prompt"); // ============================================================================ // 以下 区块自 提示词提取/server.js 原样提取(extractJson / callChat / // 归一化 / 序列化 / 脚手架构建函数 + 脚本常量),未改动逻辑。 // ============================================================================ function extractJson(text) { if (!text) throw new Error('模型返回为空'); let t = text.trim(); const fence = t.match(/```(?:json)?\s*([\s\S]*?)```/i); if (fence) t = fence[1].trim(); // 截取第一个 { 到最后一个 } const start = t.indexOf('{'); const end = t.lastIndexOf('}'); if (start !== -1 && end !== -1 && end > start) t = t.slice(start, end + 1); const tryParse = (s) => { try { return JSON.parse(s); } catch (_) { return undefined; } }; let r = tryParse(t); if (r !== undefined) return r; // 修复 LLM 常见错误:去掉对象/数组结尾的尾随逗号(如 {"a":1,} 或 [1,2,]) r = tryParse(t.replace(/,\s*([}\]])/g, '$1')); if (r !== undefined) return r; throw new Error('模型输出的 JSON 格式有误,请重试'); } // 流式调用:空闲超时——120 秒收不到任何数据就判定挂起并中止 const CHAT_IDLE_MS = 120000; function chatTimeoutError() { return Object.assign(new Error('模型长时间无响应(可能渠道不稳或模型太慢),请重试或换个更快的模型'), { status: 504 }); } async function callChat(gwBase, apiKey, model, messages, maxTokens) { const ctrl = new AbortController(); let idle; const resetIdle = () => { clearTimeout(idle); idle = setTimeout(() => ctrl.abort(), CHAT_IDLE_MS); }; resetIdle(); let upstream; try { upstream = await fetch(`${gwBase}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, body: JSON.stringify({ model, temperature: 0.4, max_tokens: maxTokens, messages, stream: true }), signal: ctrl.signal, }); } catch (e) { clearTimeout(idle); if (e.name === 'AbortError') throw chatTimeoutError(); throw e; } if (!upstream.ok) { clearTimeout(idle); const detail = await upstream.text().catch(() => ''); const err = new Error(`上游模型调用失败 (${upstream.status})`); err.status = upstream.status === 401 ? 401 : 502; err.detail = detail.slice(0, 300); throw err; } const reader = upstream.body.getReader(); const decoder = new TextDecoder(); let buf = ''; let content = ''; try { while (true) { const { done, value } = await reader.read(); if (done) break; resetIdle(); // 收到数据就重置空闲计时 buf += decoder.decode(value, { stream: true }); let nl; while ((nl = buf.indexOf('\n')) >= 0) { const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1); if (!line.startsWith('data:')) continue; const data = line.slice(5).trim(); if (!data || data === '[DONE]') continue; try { const j = JSON.parse(data); const delta = j.choices?.[0]?.delta; if (delta?.content) content += delta.content; else if (j.choices?.[0]?.message?.content) content += j.choices[0].message.content; } catch (_) { /* 跳过 keep-alive / 非 JSON 行 */ } } } } catch (e) { if (e.name === 'AbortError') throw chatTimeoutError(); throw e; } finally { clearTimeout(idle); } if (!content) throw new Error('模型未返回内容(可能超时或渠道异常)'); return content; } const SCOPE_RULE = '仅在当前项目范围内工作,不访问项目外的文件或系统'; function ensureConstraints(agent) { const list = Array.isArray(agent.constraints) ? agent.constraints.slice() : []; if (!list.some((c) => /当前项目|本项目|current.?project|项目范围|项目内/.test(c))) list.unshift(SCOPE_RULE); return list; } function cleanId(id, fallback) { const s = String(id || fallback || 'agent').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); return s || 'agent'; } const asArr = (v) => (Array.isArray(v) ? v : []); // 去重合并(保序) const mergeUniq = (...arrs) => { const out = [], seen = new Set(); arrs.flat().forEach((x) => { const k = String(x).trim(); if (k && !seen.has(k)) { seen.add(k); out.push(k); } }); return out; }; // 剥掉 glob 后缀,得到可比较的路径基(src/app/api/** → src/app/api) const stripGlob = (p) => String(p).replace(/\/\*\*?$/, '').replace(/\/\*+$/, '').replace(/\*+$/, '').replace(/\/+$/, ''); // —— 包管理器一致性表:声明的包管理器 → 对应锁文件 + 安装命令 —— const PM_TABLE = { npm: { lock: 'package-lock.json', install: 'npm ci' }, pnpm: { lock: 'pnpm-lock.yaml', install: 'pnpm install --frozen-lockfile' }, yarn: { lock: 'yarn.lock', install: 'yarn install --immutable' }, }; // 服务端强制口径一致(不能只靠 prompt): // 1) 包管理器:ci_steps 命令 + owner_paths 锁文件名 统一成 team.package_manager 声明的那个 // 2) 鉴权方案:auth_scheme 所需的 env_vars 缺了自动补全 // 每次改写记入 team.consistency_warnings,最终渲染进 CONVENTIONS.md function enforceConsistency(team) { team.consistency_warnings = asArr(team.consistency_warnings); const warn = (msg) => { if (!team.consistency_warnings.includes(msg)) team.consistency_warnings.push(msg); }; // —— 包管理器一致性 —— team.ci_steps = asArr(team.ci_steps); team.employees = asArr(team.employees); const pm = String(team.package_manager || '').toLowerCase().trim(); if (PM_TABLE[pm]) { const target = PM_TABLE[pm]; const others = Object.keys(PM_TABLE).filter((k) => k !== pm); // ci_steps:其它 JS 包管理器的命令改写成声明的那个 others.forEach((other) => { let touched = false; team.ci_steps = team.ci_steps.map((step) => { let s = String(step); const before = s; if (other === 'npm') s = s.replace(/\bnpm ci\b/g, target.install).replace(/\bnpm install\b/g, target.install); else if (other === 'pnpm') s = s.replace(/\bpnpm install(\s+--frozen-lockfile)?\b/g, target.install); else s = s.replace(/\byarn install(\s+--immutable)?\b/g, target.install); s = s.replace(new RegExp(`\\b${other} run\\b`, 'g'), `${pm} run`); s = s.replace(new RegExp(`\\b${other} test\\b`, 'g'), `${pm} test`); if (s !== before) touched = true; return s; }); if (touched) warn(`ci_steps 中的 ${other} 命令已统一为 ${pm}`); }); // owner_paths:错误锁文件名替换成声明包管理器的锁文件 team.employees.forEach((e) => { e.owner_paths = asArr(e.owner_paths).map((p) => { let s = String(p); others.forEach((other) => { if (s.includes(PM_TABLE[other].lock)) { s = s.split(PM_TABLE[other].lock).join(target.lock); warn(`${e.id} 的 owner_paths 中的锁文件 ${PM_TABLE[other].lock} 已替换为 ${target.lock}`); } }); return s; }); }); } else if (pm && pm !== 'none') { // 非 JS 包管理器(pip/cargo/go 等):只检测混入 JS 包管理器命令,不改写 const mixed = team.ci_steps.filter((s) => /\b(npm|pnpm|yarn)\b/.test(String(s))); if (mixed.length) warn(`包管理器为 ${pm},但 ci_steps 混入了 JS 包管理器命令(${mixed.join(';')}),请人工确认`); } // —— auth_scheme ↔ env_vars 自动补全 —— const auth = String(team.auth_scheme || '').toLowerCase(); if (auth && auth !== 'none') { const need = []; if (auth.includes('jwt')) need.push('JWT_SECRET', 'JWT_EXPIRES_IN'); if (auth.includes('nextauth')) need.push('NEXTAUTH_SECRET', 'NEXTAUTH_URL'); if ((auth.includes('session') || auth.includes('cookie')) && !auth.includes('nextauth')) need.push('SESSION_SECRET', 'SESSION_COOKIE_NAME', 'SESSION_TTL'); team.env_vars = asArr(team.env_vars); const have = new Set(team.env_vars.map((v) => String((v && v.name) || '').toUpperCase())); need.forEach((name) => { if (!have.has(name)) { team.env_vars.push({ name, desc: `(鉴权方案 ${team.auth_scheme} 所需,自动补全)` }); warn(`env_vars 缺少鉴权方案「${team.auth_scheme}」所需的 ${name},已自动补全`); } }); } // —— 职责完整性兜底(模型漏配也要闭环)—— team.db_models = asArr(team.db_models); const stackStr = asArr(team.stack).join(' '); const findByIdName = (re) => team.employees.find((e) => re.test(`${e.id || ''} ${e.name || ''}`)); const contractOwnerEmp = team.employees.find((e) => e.id === team.contract_owner); // 1) 数据库 schema/迁移归属:有数据存储但没人拥有迁移路径 → 按栈推断目录并指派 const hasDatastore = team.db_models.length > 0 || /prisma|postgres|mysql|sqlite|mongo|typeorm|sequelize|drizzle|django|rails|flyway|liquibase/i.test(stackStr); if (hasDatastore && team.employees.length) { const dbOwned = team.employees.some((e) => asArr(e.owner_paths).some((p) => /prisma|migrations?|db\/|schema/i.test(String(p))) ); if (!dbOwned) { const s = stackStr.toLowerCase(); const dbPath = s.includes('prisma') ? 'prisma/**' : s.includes('django') ? '**/migrations/**' : s.includes('rails') ? 'db/**' : 'migrations/**'; const target = findByIdName(/db|database|data|数据/i) || contractOwnerEmp || team.employees[0]; target.owner_paths = asArr(target.owner_paths); if (!target.owner_paths.includes(dbPath)) target.owner_paths.push(dbPath); warn(`数据库 schema/迁移路径(${dbPath})职责无人认领,已指派给 ${target.id}`); } } // 2) README/docs 归属:没人拥有 README.md 且没人拥有 docs 顶层(docs/coordination 是共享区,不算) if (team.employees.length) { const hasReadme = team.employees.some((e) => asArr(e.owner_paths).some((p) => String(p).includes('README.md')) ); const hasDocsTop = team.employees.some((e) => asArr(e.owner_paths).some((p) => { const base = stripGlob(p); return /^docs(\/|$)/.test(base) && !/^docs\/coordination(\/|$)/.test(base); }) ); if (!hasReadme && !hasDocsTop) { const target = findByIdName(/devops|ops|infra/i) || contractOwnerEmp || team.employees[0]; target.owner_paths = asArr(target.owner_paths); // 只归窄职责:README + docs 顶层文档。绝不给 docs/**(会吞掉 docs/security 等专职子目录和共享区) ['README.md', 'docs/*.md'].forEach((p) => { if (!target.owner_paths.includes(p)) target.owner_paths.push(p); }); warn(`README.md 与 docs/*.md(顶层文档)职责无人认领,已指派给 ${target.id}(协作文档维护者)`); } } // 2.5) owner 覆盖面补齐:常见工程文件无人认领时补给对应专业角色(带免冲突守卫,绝不制造新重叠) { const allBases = []; team.employees.forEach((e) => asArr(e.owner_paths).forEach((p) => allBases.push(stripGlob(p)))); const unowned = (path) => { const b = stripGlob(path); return !allBases.some((x) => x === b || x.startsWith(b + '/') || b.startsWith(x + '/')); }; const grant = (emp, paths, why) => { if (!emp) return; emp.owner_paths = asArr(emp.owner_paths); const added = []; paths.forEach((p) => { if (unowned(p) && !emp.owner_paths.includes(p)) { emp.owner_paths.push(p); allBases.push(stripGlob(p)); added.push(p); } }); if (added.length) warn(`${why}无人认领,已补给 ${emp.id}:${added.join('、')}`); }; const stackStr = `${asArr(team.stack).join(' ')} ${team.package_manager || ''}`; // 安全:安全文档产出区 grant(findByIdName(/security|安全/i), ['docs/security/**'], '安全文档(威胁模型/审核报告)'); // 运维:锁文件 / .gitignore / Docker / 部署 const devops = findByIdName(/devops|ops|infra/i) || contractOwnerEmp; const LOCKS = { npm: 'package-lock.json', pnpm: 'pnpm-lock.yaml', yarn: 'yarn.lock' }; if (LOCKS[team.package_manager]) grant(devops, [LOCKS[team.package_manager]], '依赖锁文件'); grant(devops, ['.gitignore'], '.gitignore'); if (/docker/i.test(stackStr)) grant(devops, ['Dockerfile', 'docker-compose.*', 'docker/**', 'deploy/**'], 'Docker/部署文件'); // 测试:测试配置与工具目录(按栈出现的工具补) const tester = findByIdName(/test|qa|测试/i); if (/playwright/i.test(stackStr)) grant(tester, ['playwright/**', 'playwright.config.*'], 'Playwright 配置'); if (/vitest/i.test(stackStr)) grant(tester, ['vitest.config.*', 'vitest.setup.*'], 'Vitest 配置'); if (/\bjest\b/i.test(stackStr)) grant(tester, ['jest.config.*', 'jest.setup.*'], 'Jest 配置'); if (tester) grant(tester, ['__tests__/**', 'src/test-utils/**'], '测试目录/工具'); } // 3) 安全协作:security 角色必须与鉴权/上传/权限相关角色(至少后端)双向 collaborators const sec = findByIdName(/security|安全/i); if (sec) { sec.collaborators = asArr(sec.collaborators); const SEC_DUTY_RE = /鉴权|登录|会话|auth|session|token|权限|rbac|上传|upload/i; team.employees.forEach((e) => { if (e.id === sec.id) return; const text = `${e.duty || ''} ${asArr(e.responsibilities).join(' ')} ${e.prompt || ''}`; if (!SEC_DUTY_RE.test(text) && !/back/.test(String(e.id))) return; e.collaborators = asArr(e.collaborators); let touched = false; if (!e.collaborators.includes(sec.id)) { e.collaborators.push(sec.id); touched = true; } if (!sec.collaborators.includes(e.id)) { sec.collaborators.push(e.id); touched = true; } if (touched) warn(`已建立 ${e.id} ↔ ${sec.id} 安全协作关系`); }); } // 4)+5) git_rules:提交示例贴合本项目 + 共享协作区规则 // ⚠️ 先深拷贝——team.git_rules 可能就是 TEAM_CONVENTIONS.git_rules 的引用,绝不能改常量本身 team.git_rules = [...(asArr(team.git_rules).length ? team.git_rules : TEAM_CONVENTIONS.git_rules)]; const writeEp = asArr(team.api_endpoints).find((ep) => /^(post|put)$/i.test(String((ep && ep.method) || ''))); let scope = ''; if (writeEp && writeEp.path) { // 取 path 最后一个静态段(跳过 :id / {id} 等参数段)并单数化 const segs = String(writeEp.path).split(/[/?#]/).filter((x) => x && !/[:{}\[\]<>]/.test(x) && !x.includes('*')); scope = String(segs[segs.length - 1] || '').toLowerCase().replace(/[^a-z0-9-]/g, ''); if (/ies$/.test(scope)) scope = scope.replace(/ies$/, 'y'); else if (/s$/.test(scope) && !/ss$/.test(scope)) scope = scope.replace(/s$/, ''); } if (!scope) scope = String(team.db_models[0] || '').toLowerCase().replace(/[^a-z0-9-]/g, ''); if (!scope) scope = 'core'; const exDesc = (writeEp && writeEp.desc) || '完成核心功能'; team.git_rules = team.git_rules.map((r) => /提交信息格式/.test(String(r)) ? `提交信息格式:type(scope): 摘要(如 feat(${scope}): ${exDesc})` : r ); if (!team.git_rules.some((r) => /docs\/coordination/.test(String(r)))) { team.git_rules.push( 'docs/coordination/** 为共享协作区:所有角色均可写入交接单 / 验收 / 裁决记录,不属于任何角色的 owner_paths' ); } return team; } // 结构性归一(/api/employees 与 /api/pack 共用):id 清洗去重、职责归属(owner_paths)互斥、跨角色引用收敛、总控只读兜底。 // 权限模型(开放写权限模式):allowed_paths 只有两档——执行角色 ['**']、orchestrator []; // 职责/审核归属由 owner_paths 表达(不限制编辑,只决定谁需要复核)。 // 注意:这里不动 e.model —— pack 时不能覆写模型。 const OPEN_WRITE_RULE = '开放写权限:可修改项目内任意文件;跨职责改动必须在完成报告中说明并请对应 owner 复核'; function reconcileTeam(team) { team.employees = asArr(team.employees); const seen = new Set(['orchestrator']); // 保留字:员工不得占用总控 id(否则文件名/清单/校验三处错位) team.employees.forEach((e, i) => { e.id = cleanId(e.id, `employee-${i + 1}`); while (seen.has(e.id)) e.id += '-x'; seen.add(e.id); // 兼容旧字段:没给 owner_paths 时把 allowed_paths 当职责归属用 e.owner_paths = asArr(e.owner_paths).length ? asArr(e.owner_paths) : asArr(e.allowed_paths); e.review_paths = asArr(e.review_paths); e.collaborators = asArr(e.collaborators); }); // docs 结构性收窄(在互斥/嵌套检测之前做,避免产生脏冲突记录): // - 宽泛 docs/** → docs/*.md(顶层文档):子目录归专职角色(如 docs/security),coordination 是共享区,谁都不许整锅端 // - 误配的 docs/coordination/** 直接剔除(共享区不归任何单一角色) team.consistency_warnings = asArr(team.consistency_warnings); team.employees.forEach((e) => { e.owner_paths = e.owner_paths .map((p) => { if (stripGlob(p) === 'docs') { team.consistency_warnings.push(`${e.id} 的 docs/** 过宽,已收窄为 docs/*.md(子目录归专职角色,docs/coordination 为共享区)`); return 'docs/*.md'; } return p; }) .filter((p) => { if (/^docs\/coordination(\/|$)/.test(stripGlob(p) + '/')) { team.consistency_warnings.push(`${e.id} 的 ${p} 已剔除:docs/coordination 是共享协作区,不归任何单一角色`); return false; } return true; }); }); // 强制 owner_paths 跨角色互不重叠。相同路径按「专业匹配优先」裁定归属, // 而非先占者赢(否则模型把 prisma/** 同时给 backend 与 database 时,排前面的 backend 会抢走它)。 const SPECIALTY = [ { path: /(^|\/)(prisma|migrations?|schema|seeds?)(\/|$)|(^|\/)db(\/|$)/i, role: /db|database|warehouse/i }, { path: /^security(\/|$)|^docs\/security(\/|$)/i, role: /security|安全/i }, { path: /^(tests?|e2e|playwright|__tests__|spec)(\/|$)/i, role: /test|qa/i }, { path: /^(\.github|scripts|infra|docker|deploy)(\/|$)/i, role: /devops|ops|infra/i }, ]; const specialistFor = (key) => { const rule = SPECIALTY.find((s) => s.path.test(key)); if (!rule) return null; const emp = team.employees.find((e) => rule.role.test(e.id + ' ' + (e.name || ''))); return emp ? emp.id : null; }; const conflicts = []; // 先汇总每个 key 的全部认领者,再统一裁定 const claims = new Map(); team.employees.forEach((e) => e.owner_paths.forEach((p) => { const key = stripGlob(p); if (!key) return; if (!claims.has(key)) claims.set(key, []); if (!claims.get(key).includes(e.id)) claims.get(key).push(e.id); }) ); const winnerOf = new Map(); claims.forEach((ids, key) => { const spec = specialistFor(key); winnerOf.set(key, spec && ids.includes(spec) ? spec : ids[0]); // 专业角色在认领者中则它赢,否则先占者 }); team.employees.forEach((e) => { const kept = []; e.owner_paths.forEach((p) => { const key = stripGlob(p); if (!key) return; const win = winnerOf.get(key); if (win !== e.id) { conflicts.push({ path: key, role: e.id, ownedBy: win, nested: false }); team.consistency_warnings.push(`owner_paths 重叠:${key} 按专业归属裁定给 ${win},${e.id} 降级为只读审查`); if (!e.review_paths.includes(p)) e.review_paths.push(p); } else if (!kept.includes(p)) { kept.push(p); } }); e.owner_paths = kept; }); // 嵌套消解:父路径吞子路径(如前端 src/app 吞后端 src/app/api)——父路径自动降级为只读,子路径保留 const bases = []; team.employees.forEach((e) => e.owner_paths.forEach((p) => bases.push({ p, base: stripGlob(p), role: e.id }))); const demoteParents = new Map(); bases.forEach((a) => bases.forEach((b) => { if (a.role !== b.role && a.base && b.base && b.base.startsWith(a.base + '/')) { conflicts.push({ path: a.base, role: a.role, child: b.base, ownedBy: b.role, nested: true }); if (!demoteParents.has(a.role)) demoteParents.set(a.role, new Set()); demoteParents.get(a.role).add(a.p); team.consistency_warnings.push(`owner_paths 父子重叠:${a.role} 的 ${a.p} 包含 ${b.role} 的 ${b.base},父路径已降级为只读审查`); } })); demoteParents.forEach((paths, roleId) => { const e = team.employees.find((x) => x.id === roleId); if (!e) return; e.owner_paths = e.owner_paths.filter((p) => !paths.has(p)); paths.forEach((p) => { if (!e.review_paths.includes(p)) e.review_paths.push(p); }); }); team.path_conflicts = conflicts; // 开放写权限模式定稿:执行角色一律 allowed_paths=['**'],并注入开放写权限约束(去重) team.employees.forEach((e) => { e.allowed_paths = ['**']; e.constraints = asArr(e.constraints); if (!e.constraints.includes(OPEN_WRITE_RULE)) e.constraints.push(OPEN_WRITE_RULE); }); // 总控强制只读(文件名/键/校验三处对齐) if (team.orchestrator) { team.orchestrator.id = 'orchestrator'; team.orchestrator.allowed_paths = []; // 总控只读,不写任何文件 team.orchestrator.owner_paths = []; // 总控无职责归属路径(只审不改) team.orchestrator.review_paths = asArr(team.orchestrator.review_paths); if (!team.orchestrator.review_paths.length) team.orchestrator.review_paths = ['**']; } // 契约 owner:模型给的若无效,兜底选后端类角色 / 第一个员工 const ids = new Set(team.employees.map((e) => e.id)); if (!team.contract_owner || !ids.has(team.contract_owner)) { const be = team.employees.find((e) => /back|api|server|后端/i.test(e.id + e.name)); team.contract_owner = (be && be.id) || (team.employees[0] && team.employees[0].id) || ''; } // 🔴 收敛所有跨角色引用回真实 id 集合(消除 collaborators/owner 悬空引用,如 qa-engineer vs testing-engineer) team.employees.forEach((e) => { e.collaborators = e.collaborators.filter((c) => ids.has(c) && c !== e.id); }); team.api_endpoints = asArr(team.api_endpoints).map((ep) => ({ ...ep, owner: ep && ids.has(ep.owner) ? ep.owner : team.contract_owner, // 无效 owner 兜底成契约 owner })); return team; } function normalizeTeam(team, model, requirement) { team.stack = asArr(team.stack); team.employees = asArr(team.employees); team.employees.forEach((e) => { e.model = model; // 员工统一用运行模型,避免模型名不一致 e.constraints = ensureConstraints(e); e.responsibilities = asArr(e.responsibilities); e.handoff_in = e.handoff_in || ''; e.handoff_out = e.handoff_out || ''; }); if (team.orchestrator) { team.orchestrator.model = model; team.orchestrator.constraints = ensureConstraints(team.orchestrator); team.orchestrator.responsibilities = asArr(team.orchestrator.responsibilities); team.orchestrator.workflow = ORCHESTRATOR_WORKFLOW; // 注入调度状态机 } // 结构性归一(id / 路径互斥 / 引用收敛 / 总控只读)——与 /api/pack 共用同一套 reconcileTeam(team); team.contract_spec = team.contract_spec || '(由 contract owner 按本项目定义对外契约——Web:响应体/分页/错误码/鉴权;CLI:命令与参数;库:公开 API 与版本策略;数据管道:输入输出 schema 与调度。)'; team.ci_steps = asArr(team.ci_steps); team.env_vars = asArr(team.env_vars); team.db_models = asArr(team.db_models); team.interface_kind = team.interface_kind || (team.api_endpoints.length ? 'rest' : 'none'); team.auth_scheme = team.auth_scheme || ''; team.package_manager = team.package_manager || ''; // 完成定义 = 基线 + 模型追加 team.definition_of_done = mergeUniq(TEAM_CONVENTIONS.definition_of_done, asArr(team.dod_extra)); team.output_format = TEAM_CONVENTIONS.output_format; team.git_rules = TEAM_CONVENTIONS.git_rules; team.handoff_protocol = TEAM_CONVENTIONS.handoff_protocol; // 安全清单:涉及鉴权/权限/敏感数据时启用基线 + 模型追加 const needSecurity = /鉴权|权限|登录|账号|支付|敏感|隐私|rbac|auth|token|jwt|role/i.test( (requirement || '') + JSON.stringify(team.stack) + JSON.stringify(team.employees.map((e) => e.duty)) ); // 基线里的「会话/令牌」项按本项目选定的 auth_scheme 定稿,不再写死「JWT 或 Cookie」二选一 const baseline = needSecurity ? SECURITY_CHECKLIST.map((x) => /会话 \/ 令牌/.test(x) && team.auth_scheme && team.auth_scheme !== 'none' ? `会话 / 令牌:本项目统一采用 ${team.auth_scheme}——设置过期、可注销失效、防重放` : x ) : []; team.security_checklist = mergeUniq(baseline, asArr(team.security_checklist)); // 服务端强制口径一致:包管理器命令/锁文件 + 鉴权 env(改写记录进 consistency_warnings) enforceConsistency(team); delete team.dod_extra; return team; } // 生成注入进每个 agent prompt 的「协同规范」块 function ownerName(team) { const o = team.employees.find((e) => e.id === team.contract_owner); return o ? `${o.name}(${o.id})` : team.contract_owner || '后端负责人'; } function sharedProtocol(team, isLead) { const L = []; L.push('\n\n---\n## 协同规范(所有角色必须严格遵守)'); L.push('\n### 一、每次任务完成后固定输出以下报告'); TEAM_CONVENTIONS.output_format.forEach((x) => L.push(`- ${x}`)); L.push('\n### 二、完成定义(DoD)——全部满足才算完成'); team.definition_of_done.forEach((x) => L.push(`- ${x}`)); L.push('\n### 三、Git / 文件边界'); (asArr(team.git_rules).length ? team.git_rules : TEAM_CONVENTIONS.git_rules).forEach((x) => L.push(`- ${x}`)); L.push('\n### 四、任务交接协议'); TEAM_CONVENTIONS.handoff_protocol.forEach((x) => L.push(`- ${x}`)); L.push(`\n### 五、接口契约`); L.push(`- 契约 owner:${ownerName(team)},只有 owner 能定稿 / 变更契约。`); L.push(`- 契约约定:${team.contract_spec}`); L.push(`- 其他角色需遵循契约,有异议先与 owner 或总控确认,不擅自改动。`); if (team.security_checklist.length) { L.push('\n### 六、安全 / 权限审核清单(相关改动必须自检)'); team.security_checklist.forEach((x) => L.push(`- ${x}`)); } if (isLead) { L.push('\n### 七、调度流程(总控按此状态机推进)'); ((team.orchestrator && team.orchestrator.workflow) || ORCHESTRATOR_WORKFLOW).forEach((x, i) => L.push(`${i + 1}. ${x}`)); L.push('\n遇到跨角色冲突、职责归属不清、契约争议,由你(总控)裁决并驱动返工闭环。你是只读角色,不写任何文件。'); L.push('\n### 八、范围纪律(防跑飞——总控是第一道闸)'); L.push('- 分派任何任务前,先产出该任务的《任务契约》(套 templates/task-charter.md),其中「明确不做 / out of scope」至少写 2 条:根据**本次客户需求**推导「这个任务明确不该碰的东西」。写不出 out of scope,说明任务还没想清楚,不许分派。'); L.push('- 审核与打回一律对照该任务的《任务契约》:产出必须能在「目标」里找到依据、且未命中「out of scope」。打回时理由必须引用《任务契约》或 DoD 的**具体条款**,并写入 docs/coordination/task-ledger.md,不凭感觉打回。'); L.push('- 同一任务打回 ≥ 2 次时,不再简单退回,改为重新澄清《任务契约》(判断是不是任务边界本身没定义清楚),避免无限返工。'); L.push('- 任务状态在 docs/coordination/task-ledger.md 流转(assigned → in-progress → review → rework → accepted),仅当状态为 accepted 才算关闭。'); } else { L.push('\n> 遇到跨角色冲突或职责归属不清,提交总控 Agent 裁决;跨职责修改务必走报告 + 复核(在完成报告中说明并请对应 owner 复核)。'); L.push('\n### 七、范围纪律(防跑飞——动工前必读)'); L.push('- 动工前先读本任务的《任务契约》(templates/task-charter.md 的本任务实例)。只做「目标」授权的事。'); L.push('- 判定跑飞的通用标准:你即将做的事若无法在《任务契约》的「目标」里找到依据,或命中了「明确不做 / out of scope」,即为跑飞——**立即停下**,把情况写进 docs/coordination/task-ledger.md 并交总控裁决,不要自行扩大范围。'); L.push('- 「顺手」优化 / 重构 / 补功能若不在本任务目标内,一律不做,先报总控。连续两次都要越界才能完成,说明任务定义有问题,同样交总控。'); } return L.join('\n'); } // ---- 序列化:TOML(Codex)/ Markdown(Claude Code)---- const esc = (s) => String(s == null ? '' : s).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\r?\n/g, '\\n'); const arrToml = (a) => '[' + (a || []).map((x) => `"${esc(x)}"`).join(', ') + ']'; // TOML 多行基本字符串会解释反斜杠转义:先转义 \ 再转义 """,避免 prompt 里的 C:\ 或 \d 破坏解析 const multiline = (s) => '"""\n' + String(s || '').replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"') + '\n"""'; function toToml(agent, role, protocol) { const lines = []; lines.push(`name = "${esc(agent.name)}"`); lines.push(`model = "${esc(agent.model)}"`); lines.push(`role = "${role}"`); lines.push(`duty = "${esc(agent.duty)}"`); lines.push(`scope = "current-project-only"`); if (role === 'orchestrator') { lines.push(`readonly = true`); lines.push(`allowed_paths = []`); lines.push(`review_paths = ${arrToml(agent.review_paths)}`); if (agent.workflow) lines.push(`workflow = ${arrToml(agent.workflow)}`); } else { // 开放写权限模式:allowed_paths 恒为 ["**"](技术可编辑全部);owner_paths 表达职责/审核归属 lines.push(`allowed_paths = ${arrToml(asArr(agent.allowed_paths).length ? agent.allowed_paths : ['**'])}`); lines.push(`owner_paths = ${arrToml(agent.owner_paths)}`); if ((agent.review_paths || []).length) lines.push(`review_paths = ${arrToml(agent.review_paths)}`); lines.push(`collaborators = ${arrToml(agent.collaborators)}`); if (agent.handoff_in) lines.push(`handoff_in = "${esc(agent.handoff_in)}"`); if (agent.handoff_out) lines.push(`handoff_out = "${esc(agent.handoff_out)}"`); } lines.push(`responsibilities = ${arrToml(agent.responsibilities)}`); lines.push(`constraints = ${arrToml(agent.constraints)}`); lines.push(''); lines.push(`prompt = ${multiline((agent.prompt || '') + protocol)}`); return lines.join('\n') + '\n'; } function toMarkdown(agent, role, protocol) { const desc = `${(agent.duty || '').replace(/\n/g, ' ')}(仅在当前项目内使用)`; // Claude Code 的 model 字段只认 claude 系(sonnet|opus|haiku|claude-*|inherit)。 // 网关模型名(gpt-* 等)写进去 agent 会启动失败——不匹配时省略该行 = 继承会话模型。 const claudeModel = /^(sonnet|opus|haiku|inherit|claude-)/i.test(String(agent.model || '')) ? agent.model : null; const fm = [ '---', `name: ${agent.id}`, `description: ${desc}`, ...(claudeModel ? [`model: ${claudeModel}`] : []), role === 'orchestrator' ? 'tools: Read, Grep, Glob, Task' : 'tools: Read, Edit, Write, Bash, Grep, Glob', '---', '', ].join('\n'); const pathList = (a) => (a || []).map((p) => `\`${p}\``).join('、'); const paths = role === 'orchestrator' ? [ '', '## 文件边界', '- 只读,不写任何文件', ...((agent.review_paths || []).length ? [`- 只读审查:${pathList(agent.review_paths)}`] : []), ] : [ '', '## 文件边界', '- 技术可编辑:全部(开放写权限模式)', `- 职责归属(owner_paths):${(agent.owner_paths || []).length ? pathList(agent.owner_paths) : '—'}`, ...((agent.review_paths || []).length ? [`- 只读审查:${pathList(agent.review_paths)}`] : []), ]; const collab = role === 'orchestrator' ? [] : [ '', '## 协作', ...(agent.collaborators || []).map((c) => `- 与 \`${c}\` 协作`), ...(agent.handoff_in ? [`- 接收:${agent.handoff_in}`] : []), ...(agent.handoff_out ? [`- 交付:${agent.handoff_out}`] : []), ]; const body = [ agent.prompt || '', '', '## 职责', ...(agent.responsibilities || []).map((r) => `- ${r}`), ...paths, ...collab, '', '## 约束', ...(agent.constraints || []).map((c) => `- ${c}`), protocol, '', ].join('\n'); return fm + body; } function buildConventions(team) { const L = []; L.push(`# ${team.project_name || '项目'} · 团队协同规范`); L.push('\n> 本文件是所有 Agent 的共同契约。每个 agent 的 prompt 里也内置了同一套规范。\n'); L.push('## 统一产出格式(每次任务完成后必须输出)'); team.output_format.forEach((x) => L.push(`- ${x}`)); L.push('\n## 完成定义(DoD)'); team.definition_of_done.forEach((x) => L.push(`- ${x}`)); L.push('\n## Git / 文件边界'); team.git_rules.forEach((x) => L.push(`- ${x}`)); L.push('\n## 任务交接协议'); team.handoff_protocol.forEach((x) => L.push(`- ${x}`)); if (team.auth_scheme && team.auth_scheme !== 'none') L.push(`\n## 鉴权方案(全局唯一)\n- ${team.auth_scheme}`); if (team.package_manager && team.package_manager !== 'none') L.push(`\n## 包管理器(全局唯一)\n- ${team.package_manager}(锁文件、CI 命令、.gitignore 均以此为准)`); L.push('\n## 接口契约'); L.push(`- Owner:${ownerName(team)}`); L.push(`- 约定:${team.contract_spec}`); if ((team.api_endpoints || []).length) { L.push('- 接口清单(完整版见 templates/api-contract.md):'); team.api_endpoints.slice(0, 24).forEach((e) => L.push(` - ${e.method || ''} ${e.path || ''} — ${e.desc || ''}`)); } if ((team.db_models || []).length) L.push(`- 数据模型:${team.db_models.join('、')}(结构变更必须附 up/down 迁移)`); if (team.security_checklist.length) { L.push('\n## 安全 / 权限审核清单'); team.security_checklist.forEach((x) => L.push(`- ${x}`)); } if (team.orchestrator && team.orchestrator.workflow) { L.push('\n## 总控调度流程'); team.orchestrator.workflow.forEach((x, i) => L.push(`${i + 1}. ${x}`)); } L.push('\n## 协作关系'); team.employees.forEach((e) => { const c = (e.collaborators || []).join(', '); L.push(`- **${e.name}** \`${e.id}\`${c ? ` ↔ ${c}` : ''}`); }); const pl = (a) => (a || []).map((p) => `\`${p}\``).join('、') || '—'; L.push('\n## 职责归属(owner_paths,审核归属而非编辑限制)'); L.push('> 本包为开放写权限模式:执行角色 allowed_paths=["**"](技术上可改任意文件),orchestrator 只读。owner_paths 只决定「谁对这片文件负责、跨职责改动需要谁复核」。'); team.employees.forEach((e) => { L.push(`- **${e.name}** \`${e.id}\`|职责归属:${pl(e.owner_paths)}${(e.review_paths || []).length ? `|只读审查:${pl(e.review_paths)}` : ''}`); }); if (team.orchestrator) L.push(`- **${team.orchestrator.name}**(总控)|只读,不写任何文件`); const exactC = (team.path_conflicts || []).filter((c) => !c.nested); const nestC = (team.path_conflicts || []).filter((c) => c.nested); if (exactC.length) { L.push('\n> ⚠️ 已自动消解的重复职责归属(冲突方降级为只读审查):'); exactC.forEach((c) => L.push(`> - \`${c.path}\` 职责归 ${c.ownedBy},${c.role} 改为只读审查`)); } if (nestC.length) { L.push('\n> 🛑 职责嵌套冲突(父路径吞子路径,请由总控收窄父路径后再开工):'); nestC.forEach((c) => L.push(`> - \`${c.path}\`(${c.role})包含了 \`${c.child}\`(${c.ownedBy})——把 ${c.role} 收窄到不含该子路径`)); } if (asArr(team.consistency_warnings).length) { L.push('\n## ⚠️ 自动一致性修正'); L.push('> 以下是生成器为保证全局口径一致(包管理器 / 鉴权方案等)自动做的修正,供团队知悉:'); team.consistency_warnings.forEach((w) => L.push(`- ${w}`)); } L.push('\n## 落地产物(本包已生成,配合执行层约束)'); L.push('- `.github/CODEOWNERS` — 路径 → 审核归属角色(代表「谁需要审核」,不代表「谁能改」)'); L.push('- `.github/workflows/ci.yml` — CI 跑 DoD(lint / 测试 / 构建),devops 按技术栈补全'); L.push('- `agent-paths.json` + `scripts/check-allowed-paths.sh` — 提交前跨职责提醒(执行角色只警告不阻断;orchestrator 只读,写文件即阻断)'); L.push('- `templates/` — 交接单 / 接口契约 / 验收报告 模板'); return L.join('\n') + '\n'; } // CLAUDE.md:Claude Code 每次会话自动加载的入口(对应 Codex 的 AGENTS.md)。保持精简,细则放 CONVENTIONS.md。 function buildClaudeMd(team) { const L = [ `# ${team.project_name || '项目'} · 多 Agent 协作`, '', '本项目采用多角色协作开发,**完整协同规范见 `CONVENTIONS.md`(必读)**。', '', '## 你(主线程)就是总控', '- 按 CONVENTIONS.md 的「总控调度流程」推进:需求分析 → 拆任务 → 契约收敛 → 用 Task 委派给对应 subagent → 交叉审核 → 验收。', '- 总控不直接写业务代码,实现工作委派给 `.claude/agents/` 下的角色;`orchestrator` 子代理(只读工具集)可用于独立审核。', '- 开放写权限模式:执行角色技术上可改任意文件;跨职责(他人 owner_paths)改动必须在完成报告中说明并请对应 owner 复核。', '', '## 角色速查(委派时选对人)', ...team.employees.map( (e) => `- \`${e.id}\`:${(e.duty || '').replace(/\n/g, ' ')}|职责归属:${asArr(e.owner_paths).slice(0, 4).join('、') || '—'}` ), '', `接口契约 owner:\`${team.contract_owner || '—'}\`(契约冻结后改动走 templates/decision-record.md 裁决)。`, '', ]; return L.join('\n'); } function buildReadme(team, tool) { const isClaude = tool === 'claude'; const L = []; L.push(`# ${team.project_name || 'AI 员工团队'}`); L.push(`\n> ${team.summary || ''}\n`); L.push(`**技术栈**:${(team.stack || []).join(' · ')}`); L.push(`\n**目标工具**:${isClaude ? 'Claude Code' : 'Codex'}`); L.push('\n## 员工清单\n'); if (team.orchestrator) L.push(`- **${team.orchestrator.name}**(总控)\`${team.orchestrator.id}\` — ${team.orchestrator.duty}`); team.employees.forEach((e) => L.push(`- **${e.name}** \`${e.id}\` — ${e.duty}`)); L.push('\n## 目录放置\n'); if (isClaude) { L.push('把 `.claude/agents/*.md` 放到你项目根目录下的 `.claude/agents/` 里,Claude Code 会自动识别为可调用的 subagent。'); L.push('把 `CLAUDE.md` 和 `CONVENTIONS.md` 放到项目根目录:Claude Code 每次会话自动加载 `CLAUDE.md`(总控入口),细则见 `CONVENTIONS.md`。'); L.push('主线程即充当总控:先读需求,按 `orchestrator.md` 的调度流程拆分任务,再用 Task 分派给对应 subagent。'); } else { L.push('把 `agents/*.toml` 放到 Codex 的 agents 配置目录,把 `AGENTS.md` 放到项目根目录(Codex 会自动读取作为团队公共规范)。'); L.push('`_orchestrator.toml` 是总控:只做拆分任务、契约收敛、分派、审核、验收,不直接写业务代码。'); } L.push('\n## 如何跑起来\n'); if (isClaude) { L.push('1. 在项目根目录启动 Claude Code(自动加载 `CLAUDE.md`),**主线程即总控**:读取需求,按调度流程(见 `CONVENTIONS.md`)拆任务、定契约。'); L.push('2. 总控用 Task 把任务 + 契约 + DoD 分派给对应 subagent。'); } else { L.push('1. 先让**总控**读取需求,按调度流程(见 `CONVENTIONS.md`)拆任务、定契约。'); L.push('2. 总控把任务 + 契约 + DoD 分派给对应员工。'); } L.push('3. 每个员工完成后按「统一产出格式」回报,总控交叉审核。'); L.push('4. 未达 DoD 的返工,全部通过后由总控做集成验收。'); // 示例任务贴合本项目:优先取第一个 POST/PUT 接口的描述,其次任意接口,再次数据模型,最后兜底 const eps = asArr(team.api_endpoints); const writeEp = eps.find((e) => /^(post|put)$/i.test(String((e && e.method) || ''))); const topic = (writeEp && writeEp.desc) || (eps[0] && eps[0].desc) || asArr(team.db_models)[0] || `${team.project_name || '本项目'}的第一个核心功能`; L.push('\n**示例任务**(交给总控):'); L.push('```'); L.push(`实现「${topic}」功能:`); L.push(`- 契约 owner(\`${team.contract_owner || '契约负责人'}\`)先定稿相关契约(入参/出参/错误码/权限约定)`); if (asArr(team.db_models).length) L.push('- 数据负责人准备数据结构与迁移脚本'); L.push('- 实现角色开发功能并互相联调'); L.push('- 测试补用例,达 DoD 后总控验收'); L.push('```'); L.push('\n## ⚠️ 模型名'); if (isClaude) { L.push('Claude Code 的 `model` 字段只认 claude 系(`sonnet` / `opus` / `haiku` / `claude-*`)。生成时若未指定 claude 系模型,agent 文件已**省略 model 字段 = 自动继承会话模型**(推荐)。需要固定某档时在 frontmatter 加一行如 `model: sonnet`。'); } else { L.push(`各文件里的 \`model = "${team.employees[0] ? team.employees[0].model : ''}"\` 请改成你本地 Codex 运行器实际支持的模型名,否则可能调用失败。`); } L.push('\n> 定位:这是一份**多 Agent 协作配置包**(规范 + 角色 + 模板 + 工具化约束),不是可直接运行的项目骨架。`package.json` 等由 **devops-engineer 作为第一个 bootstrap 任务**初始化后,CI 才会真正跑起来(未初始化时 CI 不会硬失败)。'); L.push('\n## ⚠️ 解压须知(先看这个)'); L.push('本包含 `.github/`、`.env.example`、`.gitignore` 等**隐藏文件**。访达/图形工具拖拽会把它们弄丢,导致审查时看到「旧版残留」假象。'); L.push('```bash'); L.push('unzip -o agents-xxx.zip -d 全新目录/ # 必须命令行完整解压到空目录'); L.push('```'); L.push('\n## 初始化(一条命令)'); L.push('```bash'); L.push('bash scripts/setup.sh'); L.push('```'); L.push('它会:`git init` + 给脚本加可执行权限 + `git config core.hooksPath scripts/hooks`(启用提交信息校验 & 跨职责提醒)+ 建好 `docs/coordination/` 子目录。'); L.push('之后每个角色开工前 `export AGENT_ROLE=<你的角色>`,commit 时自动做角色检查:执行角色为**警告模式**(跨职责改动只提醒复核,不阻断);`orchestrator` 为只读角色,写文件会被阻断。'); L.push('\n## 落地产物'); L.push(isClaude ? '- `CLAUDE.md`:Claude Code 每次会话自动加载的总控入口\n- `CONVENTIONS.md`:完整协同规范(CLAUDE.md 指向它;含职责归属 owner_paths、契约、安全清单、调度流程)' : '- `AGENTS.md` / `CONVENTIONS.md`:团队公共规范(Codex 自动读根目录 AGENTS.md;含职责归属 owner_paths、契约、安全清单、调度流程)'); L.push('- `.github/CODEOWNERS`:路径 → 审核归属角色(由 owner_paths 生成,代表「谁需要审核」而非「谁能改」;把 @role-id 换成真实 GitHub handle)'); L.push('- `.github/workflows/ci.yml`:CI 跑 DoD(lint/test/build),devops-engineer 按技术栈补全命令'); L.push('- `scripts/setup.sh`:一键 bootstrap(git init + 权限 + 钩子 + 目录)'); L.push('- `agent-paths.json` + `scripts/check-allowed-paths.sh`:角色检查(开放写权限模式:执行角色跨职责改动只警告提醒复核、不阻断;orchestrator 只读,写文件阻断)'); L.push('- `scripts/hooks/commit-msg` + `scripts/hooks/pre-commit`:提交信息校验 & 跨职责提醒钩子'); L.push('- `.gitignore` / `.env.example`:忽略清单 / 环境变量'); L.push('- `docs/coordination/`:任务状态(requirements / contract-freeze / handoffs / acceptance / decisions)'); L.push('- `templates/`:交接单 / 接口契约(清单 + 每接口完整定义模板)/ 验收报告 / 裁决记录'); L.push('- `scripts/verify-consistency.sh`:一致性自检(agent 文件 / CODEOWNERS / CI 与 `agent-paths.json` 口径一致,发现旧版残留)'); L.push('- `agent-manifest.json`:本次生成的角色与文件清单(自检 / 清理旧残留的依据)'); L.push('\n> 审查 / 复检前先跑 `bash scripts/verify-consistency.sh`:输出 ✗ 说明目录里是旧版本残留(而非本包问题),按提示清理后再审。'); L.push('\n## 更多'); L.push('- `CONVENTIONS.md`:完整协同规范 + 职责归属(owner_paths)+ 冲突消解。每个 agent 的 prompt 已内置同一套规范。'); return L.join('\n') + '\n'; } // ---- 落地产物(工具化约束)---- function buildCodeowners(team) { const L = [ '# 路径 → 审核归属角色(由 owner_paths 生成,互不重叠)', '# 开放写权限模式:本文件代表「谁需要审核」,不代表「谁能改」——任何执行角色都可修改任意文件,', '# 但跨职责改动须在完成报告中说明并请对应 owner 复核。', '# 把 @role-id 换成真实 GitHub handle 即可启用 GitHub 代码所有者审查', '', ]; // 已知点目录(basename 以点开头但其实是目录) const DOT_DIRS = /^(\.github|\.claude|\.vscode|\.idea)$/i; // 无扩展名的已知文件(不匹配「含点」规则的常见文件) const KNOWN_FILES = /^(Dockerfile|Makefile|LICENSE|Procfile|Rakefile|Gemfile|CMakeLists\.txt|\.gitignore|\.env\.example)$/i; // 每条 allowed_path → CODEOWNERS 行的路径写法(文件/目录/通配 分情况,不能先 stripGlob) const toOwnerPath = (p) => { const raw = String(p).replace(/^\/+|\/+$/g, ''); if (!raw) return ''; // 1) 目录 glob(src/app/(public)/**、src/components/*)→ 归一成 /<目录>/** if (/\/\*\*$/.test(raw)) return `/${raw.slice(0, -3)}/**`; if (/\/\*$/.test(raw)) return `/${raw.slice(0, -2)}/**`; // 2) 文件通配(next.config.*、**/*.test.ts)→ 原样保留(GitHub CODEOWNERS 支持 * 通配) if (raw.includes('*')) return `/${raw}`; const base = raw.split('/').pop(); // 3) 已知点目录(.github 等)→ 目录 if (DOT_DIRS.test(base)) return `/${raw}/**`; // 4) basename 含扩展名点(globals.css)或已知无扩展名文件(Dockerfile)→ 文件,不加 /** if (/\.[^./]+$/.test(base) || KNOWN_FILES.test(base)) return `/${raw}`; // 5) 其余按目录处理 return `/${raw}/**`; }; team.employees.forEach((e) => (e.owner_paths || []).forEach((p) => { // 共享协作区防御性过滤(reconcileTeam 已剔除,这里兜底) if (/^docs\/coordination(\/|$)/.test(stripGlob(p) + '/')) return; const line = toOwnerPath(p); if (line) L.push(`${line} @${e.id}`); }) ); // 共享协作区:所有角色可写,不设单一 owner。 // CODEOWNERS 按「最后匹配生效」,下面这条无 owner 条目会清除上方任何模式对该区的归属。 L.push(''); L.push('# docs/coordination/** 为共享协作区(交接单/验收/裁决,所有角色可写)——显式声明无单一 owner'); L.push('/docs/coordination/**'); return L.join('\n') + '\n'; } // agent-paths.json(开放写权限模式新结构): // { "mode": "open-write", "orchestrator": [], "roles": { "": { "allowed": ["**"], "owner": [...] } } } // allowed = 技术可编辑范围(执行角色恒 ["**"]);owner = 职责/审核归属(保留原始 glob,脚本按 glob 匹配)。 // 消费方脚本向后兼容旧结构(roles. 是数组时视为 owner,且 allowed 同值)。 function buildAgentPaths(team) { const roles = {}; team.employees.forEach((e) => { roles[e.id] = { allowed: asArr(e.allowed_paths).length ? e.allowed_paths : ['**'], owner: asArr(e.owner_paths), }; }); return JSON.stringify({ mode: 'open-write', orchestrator: [], roles }, null, 2) + '\n'; } const CHECK_SCRIPT = `#!/usr/bin/env bash # 角色检查(开放写权限模式): # - orchestrator:只读角色,暂存区有任何文件 → 逐个报错并阻断(exit 1) # - 执行角色(allowed 含 "**"):一律放行(exit 0);改动落在其他角色 owner_paths 内时 # 打印跨职责警告,提醒在完成报告中说明并请对应 owner 复核(docs/coordination/** 共享区不警告) # - 兼容旧包 / 自定义收窄:allowed 不含 "**" 时沿用旧的越界阻断逻辑 # 用法: bash scripts/check-allowed-paths.sh set -euo pipefail ROLE="\${1:-}" [ -z "$ROLE" ] && { echo "用法: $0 "; exit 2; } # 必须在 git 仓库里 if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then echo "✗ 当前不是 git 仓库,先 git init(否则无法检查暂存区)"; exit 2 fi # 角色清单:可见文件名优先(agent-paths.json),兼容旧隐藏名 PF=agent-paths.json; [ -f "$PF" ] || PF=.agent-paths.json if [ ! -f "$PF" ]; then echo "✗ 缺少 agent-paths.json(解压可能丢了文件,重新 unzip -o 到空目录)"; exit 2; fi # 解析:优先 node,回退 python3(不把整套约束锁死在 Node 环境)。 # 兼容两种结构:新 roles.={allowed:[...],owner:[...]};旧 roles.=[...](视为 owner 且 allowed 同值)。 # 用法:_read roles | _read allowed | _read owner | _read others (其他角色 owner,行格式 \\t) _read() { if command -v node >/dev/null 2>&1; then node -e "const m=require('./'+process.argv[1]).roles||{};const norm=v=>Array.isArray(v)?{allowed:v,owner:v}:{allowed:(v&&v.allowed)||[],owner:(v&&v.owner)||[]};const mode=process.argv[2]||'roles';const r=process.argv[3]||'';if(mode==='roles')process.stdout.write(Object.keys(m).join(' '));else if(mode==='allowed')process.stdout.write(norm(m[r]).allowed.join('\\n'));else if(mode==='owner')process.stdout.write(norm(m[r]).owner.join('\\n'));else{const out=[];Object.keys(m).forEach(k=>{if(k===r)return;norm(m[k]).owner.forEach(p=>out.push(k+'\\t'+p));});process.stdout.write(out.join('\\n'));}" "$PF" "\${1:-roles}" "\${2:-}" elif command -v python3 >/dev/null 2>&1; then python3 -c "import json,sys;m=json.load(open(sys.argv[1])).get('roles',{});norm=lambda v:{'allowed':v,'owner':v} if isinstance(v,list) else {'allowed':(v or {}).get('allowed',[]),'owner':(v or {}).get('owner',[])};mode=sys.argv[2] if len(sys.argv)>2 else 'roles';r=sys.argv[3] if len(sys.argv)>3 else '';sys.stdout.write(' '.join(m.keys()) if mode=='roles' else chr(10).join(norm(m.get(r))['allowed']) if mode=='allowed' else chr(10).join(norm(m.get(r))['owner']) if mode=='owner' else chr(10).join(k+chr(9)+p for k in m if k!=r for p in norm(m.get(k))['owner']))" "$PF" "\${1:-roles}" "\${2:-}" else echo "✗ 需要 node 或 python3 来解析 agent-paths.json" >&2; exit 2 fi } FILES=$(git diff --cached --name-only || true) # —— 总控只读:写任何文件都阻断 —— if [ "$ROLE" = "orchestrator" ]; then [ -z "$FILES" ] && { echo "无暂存改动"; exit 0; } while IFS= read -r f; do [ -z "$f" ] && continue echo "✗ 总控为只读角色,不得写文件: $f" done <<< "$FILES" exit 1 fi KNOWN=$(_read roles) case " \${KNOWN} " in *" \${ROLE} "*) : ;; *) echo "✗ 未知角色: \${ROLE} (可用角色: \${KNOWN} orchestrator)"; exit 2 ;; esac [ -z "$FILES" ] && { echo "无暂存改动"; exit 0; } ALLOWED=$(_read allowed "$ROLE") MY_OWNER=$(_read owner "$ROLE") # glob 匹配:** 允许跨目录;单个 * 不允许跨目录(docs/*.md 不得匹配 docs/security/x.md) matches() { local f="$1" p="$2" base fs ps base="\${p%/}"; base="\${base%/\\*\\*}"; base="\${base%/\\*}" if [[ "$base" != *'*'* ]]; then # base 不含通配才按目录前缀匹配 [[ "$f" == "$base" ]] && return 0 # 精确文件/目录 [[ "$f" == "$base"/* ]] && return 0 # 目录前缀(含深层) fi if [[ "$f" == $p ]]; then # 按 glob 匹配(next.config.*、src/routes/**/+server.ts 等) [[ "$p" == *'**'* ]] && return 0 # ** 模式:允许跨目录 fs="\${f//[^\\/]/}"; ps="\${p//[^\\/]/}" # 单星模式:要求路径深度一致,防 * 跨目录 [ "\${#fs}" -eq "\${#ps}" ] && return 0 return 1 fi return 1 } # —— 开放写权限模式(allowed 含 "**"):全放行,仅对落在其他角色 owner_paths 的改动打跨职责警告 —— if printf '%s\\n' "$ALLOWED" | grep -qx '\\*\\*'; then OTHERS=$(_read others "$ROLE") warned=0 while IFS= read -r f; do [ -z "$f" ] && continue # 共享协作区:docs/coordination/** 所有角色均可写入(交接单/验收/裁决),不算跨职责 case "$f" in docs/coordination/*) continue ;; esac # 落在本角色 owner_paths 内 → 本职责改动,不警告 mine=0 while IFS= read -r p; do [ -z "$p" ] && continue if matches "$f" "$p"; then mine=1; break; fi done <<< "$MY_OWNER" [ "$mine" -eq 1 ] && continue while IFS=$'\\t' read -r oid p; do [ -z "\${oid:-}" ] && continue [ -z "\${p:-}" ] && continue if matches "$f" "$p"; then echo "⚠ 跨职责修改: $f 属 @\${oid} 职责范围,请在完成报告中说明并请其复核" warned=1 break fi done <<< "$OTHERS" done <<< "$FILES" if [ "$warned" -eq 1 ]; then echo "✓ 放行(开放写权限模式,⚠ 为跨职责提醒)" else echo "✓ 放行(开放写权限模式,无跨职责改动)" fi exit 0 fi # —— 兼容旧包 / 自定义收窄:allowed 不含 "**" → 按旧逻辑阻断越界 —— bad=0 while IFS= read -r f; do [ -z "$f" ] && continue ok=0 # 共享协作区:docs/coordination/** 所有角色均可写入(交接单/验收/裁决),全局放行 case "$f" in docs/coordination/*) ok=1 ;; esac while IFS= read -r p; do [ -z "$p" ] && continue if matches "$f" "$p"; then ok=1; break; fi done <<< "$ALLOWED" [ "$ok" -ne 1 ] && { echo "✗ 越界: $f 不在 [$ROLE] 的 allowed_paths"; bad=1; } done <<< "$FILES" [ "$bad" -eq 0 ] && echo "✓ 所有改动都在 [$ROLE] 边界内" exit $bad `; // 一致性自检脚本:agent 文件 / CODEOWNERS / CI / env 与 agent-paths.json + agent-manifest.json 交叉校验 const VERIFY_SCRIPT = `#!/usr/bin/env bash # 一致性自检(在包根目录运行):bash scripts/verify-consistency.sh # 作用:判定目录是「自洽的新包」还是「混入旧版残留 / 解压丢了隐藏文件」 set -uo pipefail bad=0; lost=0 # —— 0) 关键文件在场检查(缺失多半是解压/拷贝丢了隐藏文件)—— PF=agent-paths.json; [ -f "$PF" ] || PF=.agent-paths.json MF=agent-manifest.json; [ -f "$MF" ] || MF=.agent-manifest.json for pair in "\${PF}|agent-paths.json" ".github/CODEOWNERS|.github/CODEOWNERS" ".github/workflows/ci.yml|.github/workflows/ci.yml" ".gitignore|.gitignore" ".env.example|.env.example" "\${MF}|agent-manifest.json"; do f="\${pair%%|*}"; label="\${pair#*|}" [ -f "$f" ] || { echo "✗ 缺少 \${label}"; lost=1; bad=1; } done if [ "$lost" -eq 1 ]; then echo " ► 这些缺失多半是解压/拷贝时丢了隐藏文件(访达拖拽最常见)" echo " ► 正确做法:命令行完整解压到全新目录 → unzip -o 包.zip -d 新目录/" fi [ -f "$PF" ] || exit 1 # —— 解析器:node 优先,python3 回退 —— # 兼容两种结构:新 roles.={allowed:[...],owner:[...]}(取 owner=职责/审核归属);旧 roles.=[...](即 owner) _pf() { # 参数:角色 id → 输出该角色 owner 路径;留空则输出全部角色名 if command -v node >/dev/null 2>&1; then node -e "const m=require('./'+process.argv[1]).roles||{};const own=v=>Array.isArray(v)?v:((v&&v.owner)||[]);const r=process.argv[2]||'';process.stdout.write(r?own(m[r]).join('\\n'):Object.keys(m).join(' '))" "$PF" "\${1:-}" elif command -v python3 >/dev/null 2>&1; then python3 -c "import json,sys;m=json.load(open(sys.argv[1])).get('roles',{});own=lambda v:v if isinstance(v,list) else (v or {}).get('owner',[]);r=sys.argv[2] if len(sys.argv)>2 and sys.argv[2] else '';sys.stdout.write(chr(10).join(own(m.get(r))) if r else ' '.join(m.keys()))" "$PF" "\${1:-}" else echo "✗ 需要 node 或 python3" >&2; exit 2; fi } _mf() { # 参数:manifest 字段名;数组输出为空格分隔 [ -f "$MF" ] || return 0 if command -v node >/dev/null 2>&1; then node -e "const m=require('./'+process.argv[1]);const v=m[process.argv[2]];process.stdout.write(Array.isArray(v)?v.join(' '):String(v==null?'':v))" "$MF" "$1" elif command -v python3 >/dev/null 2>&1; then python3 -c "import json,sys;m=json.load(open(sys.argv[1]));v=m.get(sys.argv[2],'');sys.stdout.write(' '.join(v) if isinstance(v,list) else str(v))" "$MF" "$1" fi } _own_all() { # 输出全部「角色owner路径」行(互斥校验用) if command -v node >/dev/null 2>&1; then node -e "const m=require('./'+process.argv[1]).roles||{};const own=v=>Array.isArray(v)?v:((v&&v.owner)||[]);const out=[];for(const[r,v]of Object.entries(m))for(const p of own(v))out.push(r+'\\t'+p);process.stdout.write(out.join('\\n'))" "$PF" elif command -v python3 >/dev/null 2>&1; then python3 -c "import json,sys;m=json.load(open(sys.argv[1])).get('roles',{});own=lambda v:v if isinstance(v,list) else (v or {}).get('owner',[]);sys.stdout.write(chr(10).join(r+chr(9)+p for r,v in m.items() for p in own(v)))" "$PF" fi } ROLES=$(_pf) # —— a) 角色 ↔ agent 文件(orchestrator 固定对应 _orchestrator.toml / orchestrator.md,单独查)—— for r in $ROLES; do case "$r" in orchestrator) continue ;; esac [ -f "agents/\${r}.toml" ] || [ -f ".claude/agents/\${r}.md" ] || { echo "✗ 角色 \${r} 缺少 agent 文件(agents/\${r}.toml 或 .claude/agents/\${r}.md)—— agent 文件或 agent-paths.json 有一方是旧版"; bad=1; } done [ -f "agents/_orchestrator.toml" ] || [ -f ".claude/agents/orchestrator.md" ] || { echo "✗ 缺少总控 agent 文件(agents/_orchestrator.toml 或 .claude/agents/orchestrator.md)"; bad=1; } for d in agents .claude/agents; do [ -d "$d" ] || continue for f in "$d"/*.toml "$d"/*.md; do [ -e "$f" ] || continue b=$(basename "$f"); b="\${b%.toml}"; b="\${b%.md}" case "$b" in _orchestrator|orchestrator) continue ;; esac case " \${ROLES} " in *" \${b} "*) : ;; *) echo "✗ 旧版本残留文件: \${f}(不在当前角色清单里)"; bad=1 ;; esac done done # —— b) CODEOWNERS ↔ 角色集合(双向)—— if [ -f .github/CODEOWNERS ]; then CO_ROLES=$(grep -v '^#' .github/CODEOWNERS | grep -oE '@[a-z0-9][a-z0-9-]*' | sed 's/^@//' | sort -u | tr '\\n' ' ') for r in $CO_ROLES; do case " \${ROLES} orchestrator " in *" \${r} "*) : ;; *) echo "✗ CODEOWNERS 含未知角色 @\${r} —— CODEOWNERS 是旧版残留"; bad=1 ;; esac done for r in $ROLES; do case "$r" in orchestrator) continue ;; esac [ -n "$(_pf "$r")" ] || continue # 无 owner_paths(审核归属)的角色不出现在 CODEOWNERS case " \${CO_ROLES} " in *" \${r} "*) : ;; *) echo "✗ CODEOWNERS 缺少角色 @\${r} —— CODEOWNERS 是旧版残留"; bad=1 ;; esac done fi # —— c) manifest 交叉校验:包管理器 / 必需环境变量 / 角色集 —— if [ -f "$MF" ]; then PM=$(_mf package_manager) if [ -n "$PM" ] && [ "$PM" != "none" ] && [ -f .github/workflows/ci.yml ]; then for other in npm pnpm yarn; do [ "$other" = "$PM" ] && continue grep -qE "(^|[^a-zA-Z@/-])\${other} (ci|install|run|test|add)" .github/workflows/ci.yml && { echo "✗ ci.yml 用了 \${other},但本包声明包管理器为 \${PM} —— ci.yml 是旧版残留"; bad=1; } done fi REQ=$(_mf required_env) if [ -n "$REQ" ] && [ -f .env.example ]; then for v in $REQ; do grep -q "^\${v}=" .env.example || { echo "✗ .env.example 缺少 \${v} —— .env.example 是旧版残留"; bad=1; } done fi MROLES=$(_mf roles) if [ -n "$MROLES" ]; then for r in $MROLES; do case "$r" in orchestrator) continue ;; esac case " \${ROLES} " in *" \${r} "*) : ;; *) echo "✗ agent-paths.json 缺少角色 \${r} —— agent-paths.json 是旧版残留"; bad=1 ;; esac done for r in $ROLES; do case "$r" in orchestrator) continue ;; esac case " \${MROLES} " in *" \${r} "*) : ;; *) echo "✗ manifest 不含角色 \${r} —— agent-paths.json 与 manifest 不同代"; bad=1 ;; esac done fi fi # —— d) CI 命令 ↔ 锁文件(无 manifest 时的兜底交叉检)—— if [ -f .github/workflows/ci.yml ]; then PMS="" grep -qE '(^|[^a-zA-Z@/-])npm (ci|install|run|test)' .github/workflows/ci.yml && PMS="$PMS npm" grep -qE '(^|[^a-zA-Z@/-])pnpm ' .github/workflows/ci.yml && PMS="$PMS pnpm" grep -qE '(^|[^a-zA-Z@/-])yarn ' .github/workflows/ci.yml && PMS="$PMS yarn" ALL_PATHS=$(for r in $ROLES; do _pf "$r"; echo; done) # 各角色 owner 路径(锁文件扫描按职责归属) echo "$ALL_PATHS" | grep -q 'package-lock\\.json' && PMS="$PMS npm" echo "$ALL_PATHS" | grep -q 'pnpm-lock\\.yaml' && PMS="$PMS pnpm" echo "$ALL_PATHS" | grep -q 'yarn\\.lock' && PMS="$PMS yarn" N=$(printf '%s\\n' $PMS | sort -u | grep -c . || true) [ "\${N:-0}" -gt 1 ] && { echo "✗ CI 命令与锁文件指向不同包管理器(\${PMS# })—— 有一方是旧版残留"; bad=1; } fi # —— e) owner_paths 跨角色互斥:相同路径或父子重叠必须失败(职责归属必须唯一)—— OROLES=(); OPATHS=(); i=0 while IFS=$(printf '\\t') read -r r p; do [ -z "$r" ] && continue OROLES[i]="$r"; OPATHS[i]="$p"; i=$((i+1)) done < ` ${String(s).replace(/[\r\n]+/g, ' ')}`).join('\n') : ' echo "devops-engineer:把本项目的 lint/测试/构建命令填进来(对应 ci_steps)"'; return `name: CI on: [push, pull_request] jobs: dod: runs-on: ubuntu-latest # runner 预装了 node/python/go/rust/java 等;如需特定版本,devops 自行加对应 setup step steps: - uses: actions/checkout@v4 - name: DoD 门禁 run: | # 项目骨架未就绪时不硬失败(这是协作配置包,待 devops bootstrap 后生效) if git ls-files 2>/dev/null | grep -qE '(^|/)(package\\.json|go\\.mod|requirements\\.txt|pyproject\\.toml|setup\\.(py|cfg)|Cargo\\.toml|Gemfile|pom\\.xml|build\\.gradle|composer\\.json|pubspec\\.yaml|[^/]+\\.(csproj|sln)|Package\\.swift|mix\\.exs|CMakeLists\\.txt|Makefile|deno\\.jsonc?|build\\.zig)$'; then ${run} else echo "ℹ️ 尚无项目骨架 —— 协作配置包,待 devops-engineer 初始化后 CI 生效。" fi `; } const TPL_HANDOFF = `# 交接单 - 来源角色: - 目标角色: - 交接物: - 约定格式: - 验收点: - 关联契约 / 迁移: - 备注 / 已知风险: `; // 契约(按 interface_kind 自适应:API 项目出接口表,CLI/库/其他出对应契约) function buildContract(team) { const kind = team.interface_kind || 'none'; const isApi = /rest|graphql|grpc/i.test(kind); const L = ['# 契约(由 contract owner 定稿,冻结后改动走裁决记录)', '', `> interface_kind = ${kind}`, '']; L.push('## 约定'); L.push(team.contract_spec || '(按本项目定义对外契约)'); if (isApi) { L.push('\n## 接口列表'); if ((team.api_endpoints || []).length) { L.push('| 方法 | 路径 | 说明 | 负责角色 |'); L.push('|------|------|------|----------|'); team.api_endpoints.forEach((e) => L.push(`| ${e.method || ''} | ${e.path || ''} | ${(e.desc || '').replace(/\|/g, '\\|')} | ${e.owner || ''} |`)); } else { L.push('(⚠️ contract owner 补全全部接口,冻结后不得擅改)'); } L.push('\n## 每个接口的完整定义(按此模板逐个补全)'); L.push('```'); L.push('### '); L.push('- 权限码 / 请求 DTO / 响应 DTO / 错误码 / 分页排序过滤 / 请求示例 / 响应示例'); L.push('```'); } else { L.push('\n## 对外契约(非 HTTP API 项目,按下面补全)'); if (kind === 'cli') L.push('- 命令列表、参数与选项、退出码、stdin/stdout/stderr 约定、使用示例'); else if (kind === 'library') L.push('- 公开 API(函数 / 类 / 类型)、语义化版本策略、错误 / 异常约定、调用示例'); else L.push('- 本项目对外暴露的接口 / 模块 / 数据 schema、约定与示例'); if ((team.api_endpoints || []).length) { L.push('\n### 已识别的对外项'); team.api_endpoints.forEach((e) => L.push(`- ${e.method || ''} ${e.path || ''} — ${e.desc || ''}`)); } } if ((team.db_models || []).length) { L.push('\n## 数据模型 / 实体'); team.db_models.forEach((m) => L.push(`- ${m}`)); L.push('(结构变更需附迁移 / 版本管理)'); } return L.join('\n') + '\n'; } // 验收报告(内联本项目的 DoD) function buildAcceptance(team) { const L = ['# 验收报告', '', '- 任务:', '- 负责角色:', '', '## DoD 自检(全部满足才算完成)']; // DoD 行直接跟随 TEAM_CONVENTIONS / team.definition_of_done(开放写权限模式:含「跨职责修改已说明并获 owner 复核」) mergeUniq(team.definition_of_done || []).forEach((d) => L.push(`- [ ] ${d}`)); L.push('', '## 结构化产出'); [ '变更摘要', '修改文件', '测试结果', '风险与影响', '需要配合(哪个角色接手 / 确认)', '跨职责说明(为什么改非本职责文件 / 影响哪个角色 / 需要谁复核 / 是否改动契约、数据库结构、安全策略、部署配置;无则写"无")', ].forEach((x) => L.push(`- ${x}:`)); L.push('- 结论(通过 / 返工):'); return L.join('\n') + '\n'; } // .env.example(从 env_vars 生成) function buildEnvExample(team) { const L = ['# 复制为 .env 并填写实际值', '']; const vars = team.env_vars || []; if (vars.length) { vars.forEach((v) => { if (v && v.desc) L.push(`# ${v.desc}`); L.push(`${(v && v.name) || 'KEY'}=`); L.push(''); }); } else { L.push('# 由 devops-engineer 按本项目实际需要补全(纯库 / 无状态工具可能一个都不需要)。'); L.push('# 常见类别(有才填):数据源连接、密钥 / 令牌、外部服务凭证、存储、运行时参数、日志等级。'); } return L.join('\n') + '\n'; } // 协作流程 + 任务状态目录 function buildCoordination(team) { const L = [ '# 协作流程与任务状态', '', '多 Agent 协作的任务状态在这里流转,别只靠聊天记录(会丢上下文)。', '', '## 建议目录', '- `requirements.md` — 需求拆分', '- `contract-freeze.md` — 契约冻结版(contract owner 定稿后其他人不得擅改,改动走裁决)', '- `task-ledger.md` — 任务状态账本(防跑飞:assigned→…→accepted,打回必须引用条款)', '- `handoffs/` — 交接单(套 templates/handoff.md)', '- `acceptance/` — 验收报告(套 templates/acceptance-report.md)', '- `decisions/` — 裁决记录(套 templates/decision-record.md)', '', '## 调度流转(总控按此推进)', ]; (team.orchestrator && team.orchestrator.workflow ? team.orchestrator.workflow : []).forEach((w, i) => L.push(`${i + 1}. ${w}`)); return L.join('\n') + '\n'; } const TPL_DECISION = `# 裁决记录(冲突交总控裁决时填写) - 编号 / 日期: - 冲突描述: - 涉及角色: - 各方主张: - 裁决结论: - 影响范围 / 需同步的契约或文件: - 版本 / 生效说明: `; // 任务状态账本:防跑飞的第二道闸。每个任务一行状态机,打回必须留可追溯的理由。 const TASK_LEDGER = `# 任务状态账本(task-ledger) > 任务的分派、返工、验收状态在这里流转,别只靠聊天记录(会丢上下文、无法追溯打回理由)。 > 每个任务先由总控产出《任务契约》(templates/task-charter.md),再在此登记。 ## 状态机 assigned(已分派)→ in-progress(进行中)→ review(待验收)→ rework(打回返工)→ accepted(验收通过,任务关闭) ## 返工规则(防止无限拉锯) - 每次打回必须在「上次打回原因」里引用《任务契约》或 DoD 的**具体条款**,不凭感觉打回。 - 同一任务打回 ≥ 2 次:不再简单退回,改由总控重新澄清《任务契约》(多半是任务边界本身没定义清楚)。 - 只有状态为 accepted 的任务才算关闭。 ## 账本 | task-id | 负责角色 | 契约链接 | 状态 | 打回次数 | 上次打回原因(引用条款) | 更新时间 | |---------|----------|----------|------|----------|--------------------------|----------| | | | | | 0 | | | `; const COMMIT_MSG_HOOK = `#!/usr/bin/env bash # 提交信息格式校验:type(scope): 摘要 msg=$(head -1 "$1") # 放行 git 自动生成的 merge / revert 提交 if echo "$msg" | grep -qE '^(Merge |Revert )'; then exit 0; fi if ! echo "$msg" | grep -qE '^(feat|fix|docs|refactor|test|chore|perf|ci|build)(\\([a-z0-9-]+\\))?: .+'; then echo "✗ 提交信息不符合规范:type(scope): 摘要(如 feat(模块): 一句话摘要)" exit 1 fi `; const PRECOMMIT_HOOK = `#!/usr/bin/env bash # 角色检查(开放写权限模式):提交前对执行角色只做「跨职责提醒」——改动落在其他角色 # owner_paths 内时打印警告提示报告 + 复核,但不阻断提交;orchestrator 例外:只读角色, # 写任何文件都会被阻断(exit 1)。 # 把 AGENT_ROLE 设成当前角色,或改下面的默认值。 ROLE="\${AGENT_ROLE:-}" [ -z "$ROLE" ] && { echo "⚠ 未设置 AGENT_ROLE,跳过角色检查(export AGENT_ROLE=backend-engineer 后生效)"; exit 0; } bash scripts/check-allowed-paths.sh "$ROLE" `; const SETUP_SH = `#!/usr/bin/env bash # 一键 bootstrap:git 仓库 + 权限 + 钩子 + 目录 + 清理旧生成残留 set -e [ -d .git ] || git init chmod +x scripts/*.sh scripts/hooks/* 2>/dev/null || true git config core.hooksPath scripts/hooks mkdir -p docs/coordination/handoffs docs/coordination/acceptance docs/coordination/decisions # 清理孤儿 agent 文件(把新版 zip 解压覆盖到旧目录时,上一代角色文件会残留) PF=agent-paths.json; [ -f "$PF" ] || PF=.agent-paths.json _roles() { if command -v node >/dev/null 2>&1; then node -e "process.stdout.write(Object.keys(require('./'+process.argv[1]).roles).join(' '))" "$PF" elif command -v python3 >/dev/null 2>&1; then python3 -c "import json,sys;print(' '.join(json.load(open(sys.argv[1]))['roles'].keys()),end='')" "$PF"; fi } if [ -f "$PF" ]; then KNOWN=" $(_roles) orchestrator " for d in agents .claude/agents; do [ -d "$d" ] || continue for f in "$d"/*.toml "$d"/*.md; do [ -e "$f" ] || continue b=$(basename "$f"); b="\${b%.toml}"; b="\${b%.md}"; b="\${b#_}" case "$KNOWN" in *" $b "*) ;; *) echo " 清理孤儿: $f"; rm -f "$f";; esac done done # 清理旧隐藏版元数据(已由可见版取代,留着会误导审查) [ "$PF" = "agent-paths.json" ] && rm -f .agent-paths.json .agent-manifest.json 2>/dev/null || true fi # 关键隐藏文件在场检查(缺失=解压/拷贝丢了隐藏文件) for need in .github/CODEOWNERS .github/workflows/ci.yml .gitignore .env.example; do [ -f "$need" ] || echo "⚠ 缺少 \${need} —— 解压时丢了隐藏文件!请重新 unzip -o 包.zip -d 全新目录" done echo "✓ 完成:git 仓库就绪、钩子启用、旧残留已清理。" echo " 角色检查:export AGENT_ROLE=<你的角色> 后,commit 时自动运行(执行角色跨职责只提醒;orchestrator 只读阻断)。" echo " 下一步:由 devops-engineer 初始化项目骨架,CI 才会真正跑。" # 一致性自检(发现旧版本残留会提示;只提示不阻断 bootstrap) bash scripts/verify-consistency.sh || true `; // .gitignore 按技术栈派生(避免只覆盖 Node) function buildGitignore(team) { const s = ((team.stack || []).join(' ') + ' ' + (team.package_manager || '')).toLowerCase(); const has = (re) => re.test(s); const L = ['# 通用', '.env', '.env.*', '!.env.example', '*.log', '.DS_Store', 'Thumbs.db', '.idea/', '.vscode/', '*.tmp', '']; if (has(/node|npm|pnpm|yarn|next|vite|react|vue|nuxt|svelte|remix|astro|typescript|javascript/)) L.push('# Node', 'node_modules/', 'dist/', 'build/', '.next/', '.nuxt/', '.svelte-kit/', 'coverage/', ''); if (has(/python|pip|poetry|django|flask|fastapi|airflow|pandas/)) L.push('# Python', '__pycache__/', '*.py[cod]', '.venv/', 'venv/', '.pytest_cache/', '.mypy_cache/', ''); if (has(/rust|cargo/)) L.push('# Rust', 'target/', ''); if (has(/\bgo\b|golang/)) L.push('# Go', 'bin/', ''); if (has(/java|kotlin|gradle|maven|spring/)) L.push('# JVM', '*.class', 'target/', '.gradle/', ''); if (has(/flutter|dart/)) L.push('# Flutter', '.dart_tool/', 'build/', ''); if (has(/swift|ios|xcode/)) L.push('# Swift', '.build/', 'DerivedData/', ''); return L.join('\n') + '# 其余构建产物 / 依赖目录按本项目补充\n'; } // 兜底:确保协同规范字段齐全(正常来自 /api/employees 已规范化) function packDefaults(result) { const fill = (v, d) => (asArr(v).length ? v : d); result.output_format = fill(result.output_format, TEAM_CONVENTIONS.output_format); result.definition_of_done = fill(result.definition_of_done, TEAM_CONVENTIONS.definition_of_done); result.git_rules = fill(result.git_rules, TEAM_CONVENTIONS.git_rules); result.handoff_protocol = fill(result.handoff_protocol, TEAM_CONVENTIONS.handoff_protocol); result.security_checklist = asArr(result.security_checklist); result.employees = asArr(result.employees); result.api_endpoints = asArr(result.api_endpoints); result.ci_steps = asArr(result.ci_steps); result.env_vars = asArr(result.env_vars); result.db_models = asArr(result.db_models); result.interface_kind = result.interface_kind || (result.api_endpoints.length ? 'rest' : 'none'); result.auth_scheme = result.auth_scheme || ''; result.package_manager = result.package_manager || ''; if (result.orchestrator) result.orchestrator.workflow = fill(result.orchestrator.workflow, ORCHESTRATOR_WORKFLOW); // pack 复用完整结构性归一(id 清洗/路径互斥/引用收敛/总控只读,含 orchestrator.id 对齐;不覆写模型)+ 口径强制 reconcileTeam(result); enforceConsistency(result); return result; } // ============================================================================ // 以下为本应用适配层:LLM 走 router 分组,打包走零依赖 zip // ============================================================================ // 把分组 baseUrl 归一成「+ /chat/completions 即正确」的基址(对齐 memory.js 的端点拼接) function chatBase(baseUrl) { const base = String(baseUrl || "").replace(/\/+$/, ""); if (/\/chat\/completions$/.test(base)) return base.replace(/\/chat\/completions$/, ""); if (/\/v\d+$/.test(base)) return base; return base + "/v1"; } // 取分组:优先按 id,否则用默认 LLM 分组。返回 {baseUrl,key,model} function resolveGroup(groupId) { let g = null; if (groupId != null && groupId !== "" && typeof router.groupById === "function") { g = router.groupById(groupId); } if (!g && typeof router.llmGroup === "function") g = router.llmGroup(); if (!g) throw new Error("没有可用的中转分组,请先在「路由」里配置好带 baseUrl 和密钥的分组"); if (!g.baseUrl || !g.key) throw new Error(`分组「${g.name || g.id}」缺少 baseUrl 或密钥`); return g; } // 生成员工团队:需求 → 结构化团队 async function generate({ requirement, tool, groupId, model, agentModel } = {}) { if (!requirement || !String(requirement).trim()) throw new Error("请先输入项目需求"); const g = resolveGroup(groupId); const useModel = (model && String(model).trim()) || g.model; if (!useModel) throw new Error("未选择模型"); const gwBase = chatBase(g.baseUrl); const content = await callChat(gwBase, g.key, useModel, [ { role: "system", content: EMPLOYEE_SYSTEM_PROMPT }, { role: "user", content: buildEmployeeInstruction(requirement, tool) }, ], 5000); // 员工运行模型:用户可指定,否则用分析模型 const runModel = (agentModel && String(agentModel).trim()) || useModel; const result = normalizeTeam(extractJson(content), runModel, requirement); return result; } // 预览单个 agent 文件(与打包用同一套序列化) function preview({ result, tool, id } = {}) { if (!result || !Array.isArray(result.employees)) throw new Error("缺少员工数据"); packDefaults(result); const isClaude = tool === "claude"; const ext = isClaude ? "md" : "toml"; const isLead = id === "orchestrator"; const agent = isLead ? result.orchestrator : result.employees.find((e) => e.id === id); if (!agent) throw new Error("未找到该员工"); const proto = sharedProtocol(result, isLead); const content = isClaude ? toMarkdown(agent, isLead ? "orchestrator" : "worker", proto) : toToml(agent, isLead ? "orchestrator" : "worker", proto); const filename = isClaude ? `.claude/agents/${agent.id}.md` : `agents/${isLead ? "_orchestrator" : agent.id}.toml`; return { filename, content, lang: ext }; } // 组装完整文件树(返回 [{path, content, mode?}]),供打包 zip function packFiles(result, tool) { if (!result || !Array.isArray(result.employees)) throw new Error("缺少员工数据"); packDefaults(result); const isClaude = tool === "claude"; const dir = isClaude ? ".claude/agents" : "agents"; const protoWorker = sharedProtocol(result, false); const protoLead = sharedProtocol(result, true); const files = []; const add = (path, content, mode) => files.push(mode ? { path, content, mode } : { path, content }); if (result.orchestrator) { const o = result.orchestrator; if (isClaude) add(`${dir}/${o.id}.md`, toMarkdown(o, "orchestrator", protoLead)); else add(`${dir}/_orchestrator.toml`, toToml(o, "orchestrator", protoLead)); } result.employees.forEach((e) => { if (isClaude) add(`${dir}/${e.id}.md`, toMarkdown(e, "worker", protoWorker)); else add(`${dir}/${e.id}.toml`, toToml(e, "worker", protoWorker)); }); add("CONVENTIONS.md", buildConventions(result)); if (!isClaude) add("AGENTS.md", buildConventions(result)); if (isClaude) add("CLAUDE.md", buildClaudeMd(result)); add("README.md", buildReadme(result, tool)); add(".github/CODEOWNERS", buildCodeowners(result)); add(".github/workflows/ci.yml", buildCI(result)); add("agent-paths.json", buildAgentPaths(result)); add(".env.example", buildEnvExample(result)); add(".gitignore", buildGitignore(result)); add("scripts/setup.sh", SETUP_SH, 0o755); add("scripts/check-allowed-paths.sh", CHECK_SCRIPT, 0o755); add("scripts/verify-consistency.sh", VERIFY_SCRIPT, 0o755); add("scripts/hooks/commit-msg", COMMIT_MSG_HOOK, 0o755); add("scripts/hooks/pre-commit", PRECOMMIT_HOOK, 0o755); add("templates/handoff.md", TPL_HANDOFF); add("templates/api-contract.md", buildContract(result)); add("templates/acceptance-report.md", buildAcceptance(result)); add("templates/decision-record.md", TPL_DECISION); add("templates/task-charter.md", TASK_CHARTER); add("docs/coordination/README.md", buildCoordination(result)); add("docs/coordination/task-ledger.md", TASK_LEDGER); add("docs/coordination/requirements.md", "# 需求拆分\n\n(总控拆解后填写:功能列表、优先级、负责角色、依赖关系)\n"); add("docs/coordination/contract-freeze.md", "# 契约冻结版\n\n(contract owner 定稿后填写;冻结后任何改动走 templates/decision-record.md)\n"); add("docs/coordination/handoffs/.gitkeep", ""); add("docs/coordination/acceptance/.gitkeep", ""); add("docs/coordination/decisions/.gitkeep", ""); const manifest = { generated_at: new Date().toISOString(), mode: "open-write", tool: isClaude ? "claude" : "codex", project_name: result.project_name || "", package_manager: result.package_manager || "", auth_scheme: result.auth_scheme || "", interface_kind: result.interface_kind || "", required_env: asArr(result.env_vars).map((v) => v && v.name).filter(Boolean), roles: result.employees.map((e) => e.id).concat(["orchestrator"]), files: [], }; manifest.files = files.map((f) => f.path).concat(["agent-manifest.json"]).sort(); add("agent-manifest.json", JSON.stringify(manifest, null, 2) + "\n"); return files; } // 打包成 zip buffer + 建议文件名 function pack({ result, tool } = {}) { const files = packFiles(result, tool); const safe = cleanId(result.project_name, "project"); const buf = createZip(files); return { buffer: buf, filename: `agents-${safe}.zip` }; } module.exports = { generate, preview, packFiles, pack };