1644 lines
87 KiB
JavaScript
1644 lines
87 KiB
JavaScript
// 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 };
|