Files
codex-market-app/electron/lib/team.js
T

1644 lines
87 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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");
// ============================================================================
// 以下 <CORE> 区块自 提示词提取/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, modelOrOptions, requirement) {
const options = modelOrOptions && typeof modelOrOptions === 'object'
? modelOrOptions
: { analysisModel: modelOrOptions, orchestratorModel: modelOrOptions, agentModel: modelOrOptions };
const analysisModel = String(options.analysisModel || '').trim();
const orchestratorModel = String(options.orchestratorModel || analysisModel).trim();
const agentModel = String(options.agentModel || analysisModel).trim();
team.stack = asArr(team.stack);
team.employees = asArr(team.employees);
team.employees.forEach((e) => {
e.model = agentModel; // 执行角色统一使用成本模型;总控单独使用 orchestratorModel
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 = orchestratorModel;
team.orchestrator.constraints = ensureConstraints(team.orchestrator);
team.orchestrator.responsibilities = asArr(team.orchestrator.responsibilities);
team.orchestrator.workflow = ORCHESTRATOR_WORKFLOW; // 注入调度状态机
}
team.model_routing = {
generation_model: analysisModel,
orchestrator_model: orchestratorModel,
worker_model: agentModel,
analysis_model: analysisModel,
capability_policy: '需要识图、截图对比、视频理解、音频语义分析、复杂架构或最终审计的任务由总控处理;执行角色只接收结构化证据和明确验收标准',
};
// 结构性归一(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### 五、模型与能力路由');
TEAM_CONVENTIONS.model_routing.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('');
const routeHint = agent.model
? `\n\n[TD_MODEL_ROUTE model="${agent.model}" role="${role}" agent="${agent.id || role}"; runtime hint only, ignore in task output.]`
: '';
lines.push(`prompt = ${multiline((agent.prompt || '') + protocol + routeHint)}`);
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}`));
L.push('\n## 模型与能力路由');
L.push(`- 团队生成模型(仅生成配置):${(team.model_routing && (team.model_routing.generation_model || team.model_routing.analysis_model)) || '未指定'}`);
L.push(`- 总控模型:${(team.orchestrator && team.orchestrator.model) || '未指定'}`);
const workerModels = Array.from(new Set(team.employees.map((e) => e.model).filter(Boolean)));
L.push(`- 执行模型:${workerModels.join('、') || '未指定'}`);
TEAM_CONVENTIONS.model_routing.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 {
const generationModel = (team.model_routing && (team.model_routing.generation_model || team.model_routing.analysis_model)) || '';
const leadModel = team.orchestrator && team.orchestrator.model ? team.orchestrator.model : '';
const workerModel = team.employees[0] && team.employees[0].model ? team.employees[0].model : '';
L.push(`本次团队配置由 \`${generationModel || '未指定'}\` 生成;生成后总控文件使用 \`model = "${leadModel}"\`,执行角色使用 \`model = "${workerModel}"\`。请确保运行模型已在本地路由分组中配置;本包会按模型名自动选择对应分组。`);
L.push('涉及识图、截图对比、视频理解、音频语义分析或最终视觉验收时,必须由总控处理,再把结构化证据交给执行角色。');
}
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": { "<id>": { "allowed": ["**"], "owner": [...] } } }
// allowed = 技术可编辑范围(执行角色恒 ["**"]);owner = 职责/审核归属(保留原始 glob,脚本按 glob 匹配)。
// 消费方脚本向后兼容旧结构(roles.<id> 是数组时视为 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 <role-id>
set -euo pipefail
ROLE="\${1:-}"
[ -z "$ROLE" ] && { echo "用法: $0 <role-id>"; 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.<id>={allowed:[...],owner:[...]};旧 roles.<id>=[...](视为 owner 且 allowed 同值)。
# 用法:_read roles | _read allowed <id> | _read owner <id> | _read others <id>(其他角色 owner,行格式 <id>\\t<path>)
_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.<id>={allowed:[...],owner:[...]}(取 owner=职责/审核归属);旧 roles.<id>=[...](即 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() { # 输出全部「角色<TAB>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 <<EOF_OWN
$(_own_all)
EOF_OWN
n=\${#OPATHS[@]}
a=0
while [ "$a" -lt "$n" ]; do
b=$((a+1))
while [ "$b" -lt "$n" ]; do
if [ "\${OROLES[a]}" != "\${OROLES[b]}" ]; then
b1="\${OPATHS[a]%/}"; b1="\${b1%/\\*\\*}"; b1="\${b1%/\\*}"
b2="\${OPATHS[b]%/}"; b2="\${b2%/\\*\\*}"; b2="\${b2%/\\*}"
if [ "$b1" = "$b2" ]; then
echo "✗ owner_paths 重叠: \${OPATHS[a]}(@\${OROLES[a]})与 \${OPATHS[b]}(@\${OROLES[b]})指向同一路径 —— 职责归属必须互斥"; bad=1
elif [[ "$b2" == "$b1"/* ]]; then
echo "✗ owner_paths 父子重叠: \${OPATHS[a]}(@\${OROLES[a]})包含 \${OPATHS[b]}(@\${OROLES[b]})—— 请收窄父路径"; bad=1
elif [[ "$b1" == "$b2"/* ]]; then
echo "✗ owner_paths 父子重叠: \${OPATHS[b]}(@\${OROLES[b]})包含 \${OPATHS[a]}(@\${OROLES[a]})—— 请收窄父路径"; bad=1
fi
fi
b=$((b+1))
done
a=$((a+1))
done
if [ "$bad" -eq 0 ]; then
echo "✓ 一致性自检通过(本包自洽,可放心审查/开工)"
exit 0
fi
echo ""
echo "✗ 自检未通过 —— 上述文件是旧版本残留或解压不完整。"
echo " 修复:bash scripts/setup.sh 清理孤儿文件;仍不过 → 重新 unzip -o 到全新空目录。"
exit 1
`;
// CI 按本项目技术栈生成(用模型给的 ci_steps;GitHub runner 预装 node/python/go/rust 等)
function buildCI(team) {
const steps = asArr(team.ci_steps).filter(Boolean);
const run = steps.length
? steps.map((s) => ` ${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('### <METHOD> <path>');
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);
const workerModel = result.employees.find((e) => e && e.model) || {};
result.model_routing = {
...(result.model_routing || {}),
generation_model: (result.model_routing && (result.model_routing.generation_model || result.model_routing.analysis_model)) || '',
orchestrator_model: (result.orchestrator && result.orchestrator.model) || '',
worker_model: workerModel.model || '',
};
// pack 复用完整结构性归一(id 清洗/路径互斥/引用收敛/总控只读,含 orchestrator.id 对齐;不覆写模型)+ 口径强制
reconcileTeam(result);
enforceConsistency(result);
return result;
}
// ============================================================================
// </CORE> 以下为本应用适配层: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, orchestratorGroupId, agentGroupId, model, agentModel, orchestratorModel } = {}) {
if (!requirement || !String(requirement).trim()) throw new Error("请先输入项目需求");
if (tool !== "claude" && (!String(orchestratorModel || "").trim() || !String(agentModel || "").trim())) {
throw new Error("Codex 团队必须分别选择总控执行模型和子 Agent 执行模型;团队生成模型只用于生成配置");
}
const analysisGroup = resolveGroup(groupId);
const orchestratorGroup = resolveGroup(orchestratorGroupId || groupId);
const workerGroup = resolveGroup(agentGroupId || groupId);
const useModel = (model && String(model).trim()) || analysisGroup.model;
if (!useModel) throw new Error("未选择模型");
const gwBase = chatBase(analysisGroup.baseUrl);
const content = await callChat(gwBase, analysisGroup.key, useModel, [
{ role: "system", content: EMPLOYEE_SYSTEM_PROMPT },
{ role: "user", content: buildEmployeeInstruction(requirement, tool) },
], 5000);
// 生成模型只负责本次生成;总控/执行模型是生成后的运行时配置,三者彼此独立。
const runModel = (agentModel && String(agentModel).trim()) || workerGroup.model || useModel;
const leadModel = (orchestratorModel && String(orchestratorModel).trim()) || orchestratorGroup.model || useModel;
if (!runModel) throw new Error("未选择子 Agent 模型");
if (typeof router.registerModelRoute === "function") {
router.registerModelRoute(orchestratorGroupId || groupId || orchestratorGroup.id, leadModel);
router.registerModelRoute(agentGroupId || groupId || workerGroup.id, runModel);
}
const result = normalizeTeam(extractJson(content), {
analysisModel: useModel,
orchestratorModel: leadModel,
agentModel: 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 || "",
model_routing: {
generation: (result.model_routing && (result.model_routing.generation_model || result.model_routing.analysis_model)) || "",
orchestrator: (result.model_routing && result.model_routing.orchestrator_model) || (result.orchestrator && result.orchestrator.model) || "",
workers: Array.from(new Set(result.employees.map((e) => e && e.model).filter(Boolean))),
capability_policy: (result.model_routing && result.model_routing.capability_policy) || "",
},
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 };