2026-07-20 08:48:56 +08:00
// 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 ;
}
2026-08-16 12:45:32 +08:00
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 ();
2026-07-20 08:48:56 +08:00
team . stack = asArr ( team . stack );
team . employees = asArr ( team . employees );
team . employees . forEach (( e ) => {
2026-08-16 12:45:32 +08:00
e . model = agentModel ; // 执行角色统一使用成本模型;总控单独使用 orchestratorModel
2026-07-20 08:48:56 +08:00
e . constraints = ensureConstraints ( e );
e . responsibilities = asArr ( e . responsibilities );
e . handoff_in = e . handoff_in || '' ;
e . handoff_out = e . handoff_out || '' ;
});
if ( team . orchestrator ) {
2026-08-16 12:45:32 +08:00
team . orchestrator . model = orchestratorModel ;
2026-07-20 08:48:56 +08:00
team . orchestrator . constraints = ensureConstraints ( team . orchestrator );
team . orchestrator . responsibilities = asArr ( team . orchestrator . responsibilities );
team . orchestrator . workflow = ORCHESTRATOR_WORKFLOW ; // 注入调度状态机
}
2026-08-16 12:45:32 +08:00
team . model_routing = {
generation_model : analysisModel ,
orchestrator_model : orchestratorModel ,
worker_model : agentModel ,
analysis_model : analysisModel ,
capability_policy : '需要识图、截图对比、视频理解、音频语义分析、复杂架构或最终审计的任务由总控处理;执行角色只接收结构化证据和明确验收标准' ,
};
2026-07-20 08:48:56 +08:00
// 结构性归一(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 } ` ));
2026-08-16 12:45:32 +08:00
L . push ( '\n### 五、模型与能力路由' );
TEAM_CONVENTIONS . model_routing . forEach (( x ) => L . push ( `- ${ x } ` ));
L . push ( `\n### 六、接口契约` );
2026-07-20 08:48:56 +08:00
L . push ( `- 契约 owner: ${ ownerName ( team ) } ,只有 owner 能定稿 / 变更契约。` );
L . push ( `- 契约约定: ${ team . contract_spec } ` );
L . push ( `- 其他角色需遵循契约,有异议先与 owner 或总控确认,不擅自改动。` );
if ( team . security_checklist . length ) {
2026-08-16 12:45:32 +08:00
L . push ( '\n### 七、安全 / 权限审核清单(相关改动必须自检)' );
2026-07-20 08:48:56 +08:00
team . security_checklist . forEach (( x ) => L . push ( `- ${ x } ` ));
}
if ( isLead ) {
2026-08-16 12:45:32 +08:00
L . push ( '\n### 八、调度流程(总控按此状态机推进)' );
2026-07-20 08:48:56 +08:00
(( team . orchestrator && team . orchestrator . workflow ) || ORCHESTRATOR_WORKFLOW ). forEach (( x , i ) => L . push ( ` ${ i + 1 } . ${ x } ` ));
L . push ( '\n遇到跨角色冲突、职责归属不清、契约争议,由你(总控)裁决并驱动返工闭环。你是只读角色,不写任何文件。' );
2026-08-16 12:45:32 +08:00
L . push ( '\n### 九、范围纪律(防跑飞——总控是第一道闸)' );
2026-07-20 08:48:56 +08:00
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 复核)。' );
2026-08-16 12:45:32 +08:00
L . push ( '\n### 八、范围纪律(防跑飞——动工前必读)' );
2026-07-20 08:48:56 +08:00
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 ( '' );
2026-08-16 12:45:32 +08:00
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 ) } ` );
2026-07-20 08:48:56 +08:00
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 } ` ));
2026-08-16 12:45:32 +08:00
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 } ` ));
2026-07-20 08:48:56 +08:00
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 {
2026-08-16 12:45:32 +08:00
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 ( '涉及识图、截图对比、视频理解、音频语义分析或最终视觉验收时,必须由总控处理,再把结构化证据交给执行角色。' );
2026-07-20 08:48:56 +08:00
}
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 );
2026-08-16 12:45:32 +08:00
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 || '' ,
};
2026-07-20 08:48:56 +08:00
// 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 ;
}
// 生成员工团队:需求 → 结构化团队
2026-08-16 12:45:32 +08:00
async function generate ({ requirement , tool , groupId , orchestratorGroupId , agentGroupId , model , agentModel , orchestratorModel } = {}) {
2026-07-20 08:48:56 +08:00
if ( ! requirement || ! String ( requirement ). trim ()) throw new Error ( "请先输入项目需求" );
2026-08-16 12:45:32 +08:00
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 ;
2026-07-20 08:48:56 +08:00
if ( ! useModel ) throw new Error ( "未选择模型" );
2026-08-16 12:45:32 +08:00
const gwBase = chatBase ( analysisGroup . baseUrl );
2026-07-20 08:48:56 +08:00
2026-08-16 12:45:32 +08:00
const content = await callChat ( gwBase , analysisGroup . key , useModel , [
2026-07-20 08:48:56 +08:00
{ role : "system" , content : EMPLOYEE_SYSTEM_PROMPT },
{ role : "user" , content : buildEmployeeInstruction ( requirement , tool ) },
], 5000 );
2026-08-16 12:45:32 +08:00
// 生成模型只负责本次生成;总控/执行模型是生成后的运行时配置,三者彼此独立。
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 );
2026-07-20 08:48:56 +08:00
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 || "" ,
2026-08-16 12:45:32 +08:00
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 ) || "" ,
},
2026-07-20 08:48:56 +08:00
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 };