Commit e9e389822e4234fdabba580c5b08c192fac8cf4e
1 parent
2664a0f8
perf(coding): tag 全量预取 + 默认分支 memo,砍掉纯查询类子代理会话
Workflow 脚本自身无 fs / 无 shell(沙箱限制),git 只能经子代理跑——省不掉 "手",但可以省"次数"。本次只动两类**纯查询**调用点,不碰任何验收语义、 不改错误处理粒度。 1a tag 全量预取:开跑时(Router 之前)一次 `git tag -l "req-done/*" "fe-code-done/*" "milestone/*"` 拉回全表建 Set,之后 - featureLoop 入口 dedup(每 REQ / 每 FE 一次) - overlap phase1 dedup(每 FE 一次) - runMilestone tag 存在性(每模块一次) 全部变成内存查表。tag 是仓库级 ref、worktree 间共享,一份全局快照对主根 与所有 lane 同时成立。运行中新打的 tag 由 noteTag() 同步记账(脚本是唯一 写入方,单线程 await 串行,不会漏;显式判 success,防日后加 allowContinue 时把失败放行误记为已打)。 1b 默认分支 memo:refs/heads/main|master 是整程不变量且全仓共享,原本 runBranchSetup / runMilestone / runCrossModule / prepareParallelWave / fe-overlap 五个调用点各起一次子代理探测,每模块至少查 3 次。改为首次探测 后整程复用;探测失败不缓存,下次重试,保持原有 agentR 的 halt 语义。 以 5 模块 x 4 REQ + 8 FE 估算,两项合计省约 45 次子代理会话。 fail-safe:预取失败(子代理 null / 结果不合形)→ tagSnapshot 保持 null, tagKnown() 返回 null,全部调用点自动回退到"每次一个子代理查"的现行路径, 语义一字不差,不新增任何 halt 点。 验证:lib/check-workflow-syntax.mjs 语法门通过;调用点配平经 grep 自检; 多 pattern `git tag -l` 语法在本机 git 上实测有效。coding.mjs 无行为测试, 真实验证需下一轮实跑。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Showing
1 changed file
with
102 additions
and
18 deletions
workflows/coding.mjs
| ... | ... | @@ -1986,6 +1986,65 @@ function checkTagExistsPromptM(tagName) { |
| 1986 | 1986 | ].join('\n') |
| 1987 | 1987 | } |
| 1988 | 1988 | |
| 1989 | +// ── 完成态 tag 全量预取(省会话优化 1a)─────────────────────────────────────── | |
| 1990 | +// 动机:req-done/<id> / fe-code-done/<FE> / milestone/<id> 的存在性查询原本**每次一个子代理** | |
| 1991 | +// (featureLoop 入口 dedup 每 REQ 一次、phase1 dedup 每 FE 一次、runMilestone 每模块一次)。 | |
| 1992 | +// Workflow 脚本自身无 fs / 无 shell(沙箱限制),git 只能经子代理跑——省不掉"手",但可以省"次数": | |
| 1993 | +// 开跑时**一次**拉回全表,之后全部变成内存查表。tag 是仓库级 ref(worktree 间共享),故一份全局 | |
| 1994 | +// 快照对主根与所有 lane 同时成立。 | |
| 1995 | +// | |
| 1996 | +// 一致性:运行中新打的 tag 由 JS 在创建成功后 noteTag() 同步进快照——脚本是唯一写入方, | |
| 1997 | +// 单线程 await 串行记账,不会漏。预取失败(子代理 null / 返回不合形)→ tagSnapshot 保持 null, | |
| 1998 | +// 全部调用点自动回退到"每次一个子代理查"的现行路径,语义一字不差、不新增 halt 点。 | |
| 1999 | +const TAG_LIST_SCHEMA = { type:'object', additionalProperties:false, | |
| 2000 | + required:['tags'], properties:{ tags:{ type:'array', items:{ type:'string' } } } } | |
| 2001 | + | |
| 2002 | +function listAllTagsPromptM() { | |
| 2003 | + return [ | |
| 2004 | + '# 列出全部完成态 tag(一次性预取,供上层内存查表)', | |
| 2005 | + microStepContract(ROOT), | |
| 2006 | + '', | |
| 2007 | + `跑 \`git -C ${ROOT} tag -l "req-done/*" "fe-code-done/*" "milestone/*"\`(\`git tag -l\` 接受多个 pattern 作位置参数,一次列全三类)。`, | |
| 2008 | + '把 stdout 按行切分,去掉空行与行首尾空白,原样返回**完整 tag 名**(含 `req-done/` 等前缀),不要截断前缀、不要排序、不要去重加工。', | |
| 2009 | + '一个 tag 都没有(仓库尚无完成态)→ 返回空数组 `{ "tags": [] }`,这是合法结果,不是失败。', | |
| 2010 | + '## 输出(TAG_LIST_SCHEMA)', | |
| 2011 | + '- `{ "tags": ["req-done/REQ-001", "milestone/order_mgmt", ...] }`', | |
| 2012 | + ].join('\n') | |
| 2013 | +} | |
| 2014 | + | |
| 2015 | +let tagSnapshot = null // Set<string> | null(null = 未预取 / 预取失败 → 回退逐项查) | |
| 2016 | + | |
| 2017 | +// 预取一次。失败只 log 不抛——回退路径完全等价于本优化之前的行为。 | |
| 2018 | +async function primeTagSnapshot() { | |
| 2019 | + if (tagSnapshot) return | |
| 2020 | + const r = await agent(listAllTagsPromptM(), {label:'tags:prefetch', phase:'Router', schema: TAG_LIST_SCHEMA}) | |
| 2021 | + if (!r || !Array.isArray(r.tags)) { | |
| 2022 | + log('tag 预取失败(子代理无返回 / 结果不合形)——回退逐项 tag 查询,行为同优化前') | |
| 2023 | + return | |
| 2024 | + } | |
| 2025 | + tagSnapshot = new Set(r.tags.map(t => String(t).trim()).filter(Boolean)) | |
| 2026 | + log(`tag 预取: ${tagSnapshot.size} 个完成态 tag 进入内存快照(后续 dedup 查询零子代理开销)`) | |
| 2027 | +} | |
| 2028 | + | |
| 2029 | +// 查表。返回 null = 快照不可用,调用方须回退到子代理查询。 | |
| 2030 | +function tagKnown(name) { return tagSnapshot ? tagSnapshot.has(name) : null } | |
| 2031 | + | |
| 2032 | +// 记账:tag 创建成功后调用,保持快照与仓库一致(快照不可用时是 no-op)。 | |
| 2033 | +function noteTag(name) { if (tagSnapshot) tagSnapshot.add(name) } | |
| 2034 | + | |
| 2035 | +// ── 默认分支 memo(省会话优化 1b)───────────────────────────────────────────── | |
| 2036 | +// `refs/heads/main|master` 是整程不变量且全仓共享,但原本有 5 个调用点各起一次子代理探测 | |
| 2037 | +// (runBranchSetup / runMilestone / runCrossModule / prepareParallelWave / fe-overlap), | |
| 2038 | +// 每个模块至少查 3 次。首次探测结果整程复用;探测失败**不缓存**,下次调用重试, | |
| 2039 | +// 保持原有 agentR 的 halt 语义不变。 | |
| 2040 | +let defaultBranchMemo = null | |
| 2041 | +async function getDefaultBranch(label) { | |
| 2042 | + if (defaultBranchMemo) return defaultBranchMemo | |
| 2043 | + const def = await agentR(detectDefaultBranchPromptM(), {label, phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) | |
| 2044 | + if (def?.branch) defaultBranchMemo = def | |
| 2045 | + return def | |
| 2046 | +} | |
| 2047 | + | |
| 1989 | 2048 | function createTagPromptM(phaseId, fe) { |
| 1990 | 2049 | return [ |
| 1991 | 2050 | `# 打 annotated tag \`milestone/${phaseId}\``, |
| ... | ... | @@ -2171,7 +2230,7 @@ async function runBranchSetup(module, c) { |
| 2171 | 2230 | const branch = fe ? 'frontend-phase' : `module-${id}` |
| 2172 | 2231 | const lbl = (k) => `branch:${k}:${id}` |
| 2173 | 2232 | |
| 2174 | - const def = await agentR(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) | |
| 2233 | + const def = await getDefaultBranch(lbl('default')) | |
| 2175 | 2234 | |
| 2176 | 2235 | if (c.lane != null) { |
| 2177 | 2236 | // lane 模式:先确保 lane worktree 存在且在目标分支(幂等三分支见 prompt),树才存在、脏树检查才有意义。 |
| ... | ... | @@ -2244,8 +2303,8 @@ async function runMilestone(module) { |
| 2244 | 2303 | log(`milestone: ${phaseId} 自动提交脏工作树残留(${rec.detail || ''})`) |
| 2245 | 2304 | } |
| 2246 | 2305 | |
| 2247 | - // step 2: detect default branch | |
| 2248 | - const def = await agentR(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) | |
| 2306 | + // step 2: detect default branch(memo,整程只探测一次——见 getDefaultBranch) | |
| 2307 | + const def = await getDefaultBranch(lbl('default')) | |
| 2249 | 2308 | |
| 2250 | 2309 | // step 3: merge (idempotent — skip if already an ancestor) |
| 2251 | 2310 | // merge 冲突保持**硬 halt**:自动 abort/stash/改文件均不安全,把树留给人工(设计原则不变)。 |
| ... | ... | @@ -2298,9 +2357,12 @@ async function runMilestone(module) { |
| 2298 | 2357 | // else: 已是 targetTag → 静默跳过(resume 幂等) |
| 2299 | 2358 | |
| 2300 | 2359 | // step 6: annotated tag (idempotent — tag exists 时静默跳过) |
| 2301 | - const tag = await agentR(checkTagExistsPromptM(targetTag), {label: lbl('tag?'), phase: 'Milestone', schema: EXISTS_SCHEMA}) | |
| 2302 | - if (!tag.exists) { | |
| 2303 | - await runAction(g => createTagPromptM(phaseId, fe) + g, {site:`milestone-tag:${phaseId}`, grp:'Milestone', label: lbl('tag')}) | |
| 2360 | + const snapTag = tagKnown(targetTag) // 1a:快照可用则零子代理 | |
| 2361 | + const tagExists = snapTag !== null ? snapTag | |
| 2362 | + : (await agentR(checkTagExistsPromptM(targetTag), {label: lbl('tag?'), phase: 'Milestone', schema: EXISTS_SCHEMA})).exists | |
| 2363 | + if (!tagExists) { | |
| 2364 | + const tr = await runAction(g => createTagPromptM(phaseId, fe) + g, {site:`milestone-tag:${phaseId}`, grp:'Milestone', label: lbl('tag')}) | |
| 2365 | + if (tr?.success !== false) noteTag(targetTag) // 1a 记账(同上,显式判 success) | |
| 2304 | 2366 | } |
| 2305 | 2367 | |
| 2306 | 2368 | log(`milestone: ${phaseId} → ${targetTag}`) |
| ... | ... | @@ -2313,7 +2375,7 @@ async function runCrossModule(module, c) { |
| 2313 | 2375 | const id = module?.id ?? '<module>' |
| 2314 | 2376 | const lbl = (k) => `xmod:${k}:${id}` |
| 2315 | 2377 | |
| 2316 | - const def = await agentR(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) | |
| 2378 | + const def = await getDefaultBranch(lbl('default')) | |
| 2317 | 2379 | |
| 2318 | 2380 | const changed = await agentR(collectCrossModuleChangedPromptM(def.branch, c), {label: lbl('diff'), phase: 'Milestone', schema: CHANGED_FILES_SCHEMA}) |
| 2319 | 2381 | if (!changed.files.length) { |
| ... | ... | @@ -2424,9 +2486,14 @@ async function featureLoop(items, phase, c, feStage = 'all') { |
| 2424 | 2486 | if (useSpecPrefetch && items.length >= 2) { |
| 2425 | 2487 | // 直用 agent() 而非 agentR:thunk 内 throw 会被 parallel() 折成 null 丢失诊断;null 槽位留给 |
| 2426 | 2488 | // 主链 agentR 重查,那里的 halt 语义与现行串行路径逐字一致。 |
| 2427 | - const checks = await parallel(items.map(id => () => | |
| 2428 | - agent(checkReqDoneTagPromptM(id, c), {label:`donecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA}))) | |
| 2429 | - checks.forEach((r, i) => { if (r) doneById.set(items[i], r.exists) }) | |
| 2489 | + // 1a:快照可用 → 全批 dedup 零子代理(原本每项一次,items 多时这里就是一整排会话)。 | |
| 2490 | + if (tagSnapshot) { | |
| 2491 | + items.forEach(id => doneById.set(id, tagKnown(`req-done/${id}`))) | |
| 2492 | + } else { | |
| 2493 | + const checks = await parallel(items.map(id => () => | |
| 2494 | + agent(checkReqDoneTagPromptM(id, c), {label:`donecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA}))) | |
| 2495 | + checks.forEach((r, i) => { if (r) doneById.set(items[i], r.exists) }) | |
| 2496 | + } | |
| 2430 | 2497 | // 只对"确认未完成"的项预生成;tag check 失败(缺席)的项不预生成——其完成态未知,预生成可能 |
| 2431 | 2498 | // 白烧一次 spec 子代理,且主链重查后若已完成会整段 skip。 |
| 2432 | 2499 | const todo = items.filter(id => doneById.get(id) === false) |
| ... | ... | @@ -2462,6 +2529,8 @@ async function featureLoop(items, phase, c, feStage = 'all') { |
| 2462 | 2529 | const isDone = async (id) => { |
| 2463 | 2530 | const cached = doneById.get(id) |
| 2464 | 2531 | if (cached !== undefined) return cached |
| 2532 | + const snap = tagKnown(`req-done/${id}`) // 1a:快照可用则零子代理 | |
| 2533 | + if (snap !== null) return snap | |
| 2465 | 2534 | const done = await agentR(checkReqDoneTagPromptM(id, c), {label:`donecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA}) |
| 2466 | 2535 | return done.exists |
| 2467 | 2536 | } |
| ... | ... | @@ -2517,15 +2586,27 @@ async function featureLoop(items, phase, c, feStage = 'all') { |
| 2517 | 2586 | } |
| 2518 | 2587 | // approve 后落地 dedup 真值:req-done/<id> tag(失败经仲裁重试,确不可恢复才 halt)。 |
| 2519 | 2588 | // tag 全仓共享,恒以模块 ctx c 打(Phase F 在合并回模块分支**之后**才打,见 featureBatchRun 批后收口)。 |
| 2520 | - const tagDone = async (id) => runAction(g => createReqDoneTagPromptM(id, phase, c) + g, { | |
| 2521 | - site:`req-done-tag:${phase}:${id}`, grp, label:`reqdone:${phase}:${id}`, dec: c.decisions, root: c.root, | |
| 2522 | - }) | |
| 2589 | + const tagDone = async (id) => { | |
| 2590 | + const r = await runAction(g => createReqDoneTagPromptM(id, phase, c) + g, { | |
| 2591 | + site:`req-done-tag:${phase}:${id}`, grp, label:`reqdone:${phase}:${id}`, dec: c.decisions, root: c.root, | |
| 2592 | + }) | |
| 2593 | + // 1a 记账:保持内存快照与仓库一致。显式判 success——当前 runAction 未开 allowContinue, | |
| 2594 | + // 走到这里恒为真;若日后给本调用点加了 allowContinue,失败放行时**绝不能**误记为已打 tag。 | |
| 2595 | + if (r?.success !== false) noteTag(`req-done/${id}`) | |
| 2596 | + return r | |
| 2597 | + } | |
| 2523 | 2598 | // overlap phase1 中间完成态 tag:fe-code-done/<id>(组件代码 + jsdom 绿)。 |
| 2524 | - const tagFeCodeDone = async (id) => runAction(g => createFeCodeDoneTagPromptM(id, c) + g, { | |
| 2525 | - site:`fe-code-done-tag:${id}`, grp, label:`fecodedone:${id}`, dec: c.decisions, root: c.root, | |
| 2526 | - }) | |
| 2599 | + const tagFeCodeDone = async (id) => { | |
| 2600 | + const r = await runAction(g => createFeCodeDoneTagPromptM(id, c) + g, { | |
| 2601 | + site:`fe-code-done-tag:${id}`, grp, label:`fecodedone:${id}`, dec: c.decisions, root: c.root, | |
| 2602 | + }) | |
| 2603 | + if (r?.success !== false) noteTag(`fe-code-done/${id}`) // 1a 记账(同上,显式判 success) | |
| 2604 | + return r | |
| 2605 | + } | |
| 2527 | 2606 | // phase1 入口 dedup:fe-code-done/<id> 已存在 → 本 FE phase1 已完成,跳过(phase2 用 req-done dedup,走 isDone)。 |
| 2528 | 2607 | const isPhase1Done = async (id) => { |
| 2608 | + const snap = tagKnown(`fe-code-done/${id}`) // 1a:快照可用则零子代理 | |
| 2609 | + if (snap !== null) return snap | |
| 2529 | 2610 | const r = await agentR(checkFeCodeDoneTagPromptM(id, c), {label:`fecodecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA}) |
| 2530 | 2611 | return r.exists |
| 2531 | 2612 | } |
| ... | ... | @@ -3150,6 +3231,9 @@ async function runBehaviorGate(feItems, c) { |
| 3150 | 3231 | } |
| 3151 | 3232 | |
| 3152 | 3233 | phase('Router') |
| 3234 | +// 完成态 tag 全量预取(1a):放在 Router 之前——其后 featureLoop 的每 REQ / 每 FE dedup、 | |
| 3235 | +// runMilestone 的 tag 存在性查询全部走内存快照。失败静默回退逐项查(见 primeTagSnapshot)。 | |
| 3236 | +await primeTagSnapshot() | |
| 3153 | 3237 | // Router 语义断言(feItems/reqs 互斥)+ id 形状硬约束(防 shell 注入:id 直接拼入 `git ... ${id}`)。 |
| 3154 | 3238 | // id 形状(assertSafeId)是**安全护栏**——失败立即硬 halt,绝不重试绕过。 |
| 3155 | 3239 | // reqs/feItems 互斥违例可由仲裁带 guidance 重跑 router 纠正(绝对上限 ADJUDICATE_MAX)。 |
| ... | ... | @@ -3552,7 +3636,7 @@ async function prepareParallelWave(wave) { |
| 3552 | 3636 | return null |
| 3553 | 3637 | } |
| 3554 | 3638 | try { |
| 3555 | - const def = await agentR(detectDefaultBranchPromptM(), { label: 'wave:default', phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA }) | |
| 3639 | + const def = await getDefaultBranch('wave:default') | |
| 3556 | 3640 | const mc = makeCtx() |
| 3557 | 3641 | // (1) 主根复位(D2 + 补 3c):module-* 分支必须未被主根持有(worktree add 才不 fatal),且兄弟 |
| 3558 | 3642 | // milestone 的 merge 要落在默认分支主根上。脏树恢复的目标分支 = **当前 HEAD 分支**(传默认分支 |
| ... | ... | @@ -3717,7 +3801,7 @@ if (overlapOn) { |
| 3717 | 3801 | // 后端全部 done + phase1 done → phase2:reconcile(合 default→frontend-phase + 移除 phase1 worktree)→ |
| 3718 | 3802 | // 标准 runModule(feStage='e2e')(主根全栈,skeleton 幂等跳过,behaviorGate/testGate/milestone 原样)。 |
| 3719 | 3803 | phase('Frontend') |
| 3720 | - const def = await agentR(detectDefaultBranchPromptM(), { label:'fe-overlap:default', phase:'Milestone', schema: DEFAULT_BRANCH_SCHEMA }) | |
| 3804 | + const def = await getDefaultBranch('fe-overlap:default') | |
| 3721 | 3805 | await bestEffortAction(removeLaneWorktreePromptM(laneRoot('frontend-phase')), |
| 3722 | 3806 | { label:'fe-overlap:rm-worktree', phase:'Milestone', okMsg:'fe-overlap: 已移除 phase1 worktree', failTag:'fe-overlap phase1 worktree 移除' }) |
| 3723 | 3807 | const rec = await agentR(mergeDefaultIntoFrontendPromptM('frontend-phase', def.branch), | ... | ... |