Commit e9e389822e4234fdabba580c5b08c192fac8cf4e

Authored by yanghl
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,6 +1986,65 @@ function checkTagExistsPromptM(tagName) {
1986 ].join('\n') 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 function createTagPromptM(phaseId, fe) { 2048 function createTagPromptM(phaseId, fe) {
1990 return [ 2049 return [
1991 `# 打 annotated tag \`milestone/${phaseId}\``, 2050 `# 打 annotated tag \`milestone/${phaseId}\``,
@@ -2171,7 +2230,7 @@ async function runBranchSetup(module, c) { @@ -2171,7 +2230,7 @@ async function runBranchSetup(module, c) {
2171 const branch = fe ? 'frontend-phase' : `module-${id}` 2230 const branch = fe ? 'frontend-phase' : `module-${id}`
2172 const lbl = (k) => `branch:${k}:${id}` 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 if (c.lane != null) { 2235 if (c.lane != null) {
2177 // lane 模式:先确保 lane worktree 存在且在目标分支(幂等三分支见 prompt),树才存在、脏树检查才有意义。 2236 // lane 模式:先确保 lane worktree 存在且在目标分支(幂等三分支见 prompt),树才存在、脏树检查才有意义。
@@ -2244,8 +2303,8 @@ async function runMilestone(module) { @@ -2244,8 +2303,8 @@ async function runMilestone(module) {
2244 log(`milestone: ${phaseId} 自动提交脏工作树残留(${rec.detail || ''})`) 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 // step 3: merge (idempotent — skip if already an ancestor) 2309 // step 3: merge (idempotent — skip if already an ancestor)
2251 // merge 冲突保持**硬 halt**:自动 abort/stash/改文件均不安全,把树留给人工(设计原则不变)。 2310 // merge 冲突保持**硬 halt**:自动 abort/stash/改文件均不安全,把树留给人工(设计原则不变)。
@@ -2298,9 +2357,12 @@ async function runMilestone(module) { @@ -2298,9 +2357,12 @@ async function runMilestone(module) {
2298 // else: 已是 targetTag → 静默跳过(resume 幂等) 2357 // else: 已是 targetTag → 静默跳过(resume 幂等)
2299 2358
2300 // step 6: annotated tag (idempotent — tag exists 时静默跳过) 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 log(`milestone: ${phaseId} → ${targetTag}`) 2368 log(`milestone: ${phaseId} → ${targetTag}`)
@@ -2313,7 +2375,7 @@ async function runCrossModule(module, c) { @@ -2313,7 +2375,7 @@ async function runCrossModule(module, c) {
2313 const id = module?.id ?? '<module>' 2375 const id = module?.id ?? '<module>'
2314 const lbl = (k) => `xmod:${k}:${id}` 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 const changed = await agentR(collectCrossModuleChangedPromptM(def.branch, c), {label: lbl('diff'), phase: 'Milestone', schema: CHANGED_FILES_SCHEMA}) 2380 const changed = await agentR(collectCrossModuleChangedPromptM(def.branch, c), {label: lbl('diff'), phase: 'Milestone', schema: CHANGED_FILES_SCHEMA})
2319 if (!changed.files.length) { 2381 if (!changed.files.length) {
@@ -2424,9 +2486,14 @@ async function featureLoop(items, phase, c, feStage = &#39;all&#39;) { @@ -2424,9 +2486,14 @@ async function featureLoop(items, phase, c, feStage = &#39;all&#39;) {
2424 if (useSpecPrefetch && items.length >= 2) { 2486 if (useSpecPrefetch && items.length >= 2) {
2425 // 直用 agent() 而非 agentR:thunk 内 throw 会被 parallel() 折成 null 丢失诊断;null 槽位留给 2487 // 直用 agent() 而非 agentR:thunk 内 throw 会被 parallel() 折成 null 丢失诊断;null 槽位留给
2426 // 主链 agentR 重查,那里的 halt 语义与现行串行路径逐字一致。 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 // 只对"确认未完成"的项预生成;tag check 失败(缺席)的项不预生成——其完成态未知,预生成可能 2497 // 只对"确认未完成"的项预生成;tag check 失败(缺席)的项不预生成——其完成态未知,预生成可能
2431 // 白烧一次 spec 子代理,且主链重查后若已完成会整段 skip。 2498 // 白烧一次 spec 子代理,且主链重查后若已完成会整段 skip。
2432 const todo = items.filter(id => doneById.get(id) === false) 2499 const todo = items.filter(id => doneById.get(id) === false)
@@ -2462,6 +2529,8 @@ async function featureLoop(items, phase, c, feStage = &#39;all&#39;) { @@ -2462,6 +2529,8 @@ async function featureLoop(items, phase, c, feStage = &#39;all&#39;) {
2462 const isDone = async (id) => { 2529 const isDone = async (id) => {
2463 const cached = doneById.get(id) 2530 const cached = doneById.get(id)
2464 if (cached !== undefined) return cached 2531 if (cached !== undefined) return cached
  2532 + const snap = tagKnown(`req-done/${id}`) // 1a:快照可用则零子代理
  2533 + if (snap !== null) return snap
2465 const done = await agentR(checkReqDoneTagPromptM(id, c), {label:`donecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA}) 2534 const done = await agentR(checkReqDoneTagPromptM(id, c), {label:`donecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA})
2466 return done.exists 2535 return done.exists
2467 } 2536 }
@@ -2517,15 +2586,27 @@ async function featureLoop(items, phase, c, feStage = &#39;all&#39;) { @@ -2517,15 +2586,27 @@ async function featureLoop(items, phase, c, feStage = &#39;all&#39;) {
2517 } 2586 }
2518 // approve 后落地 dedup 真值:req-done/<id> tag(失败经仲裁重试,确不可恢复才 halt)。 2587 // approve 后落地 dedup 真值:req-done/<id> tag(失败经仲裁重试,确不可恢复才 halt)。
2519 // tag 全仓共享,恒以模块 ctx c 打(Phase F 在合并回模块分支**之后**才打,见 featureBatchRun 批后收口)。 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 // overlap phase1 中间完成态 tag:fe-code-done/<id>(组件代码 + jsdom 绿)。 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 // phase1 入口 dedup:fe-code-done/<id> 已存在 → 本 FE phase1 已完成,跳过(phase2 用 req-done dedup,走 isDone)。 2606 // phase1 入口 dedup:fe-code-done/<id> 已存在 → 本 FE phase1 已完成,跳过(phase2 用 req-done dedup,走 isDone)。
2528 const isPhase1Done = async (id) => { 2607 const isPhase1Done = async (id) => {
  2608 + const snap = tagKnown(`fe-code-done/${id}`) // 1a:快照可用则零子代理
  2609 + if (snap !== null) return snap
2529 const r = await agentR(checkFeCodeDoneTagPromptM(id, c), {label:`fecodecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA}) 2610 const r = await agentR(checkFeCodeDoneTagPromptM(id, c), {label:`fecodecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA})
2530 return r.exists 2611 return r.exists
2531 } 2612 }
@@ -3150,6 +3231,9 @@ async function runBehaviorGate(feItems, c) { @@ -3150,6 +3231,9 @@ async function runBehaviorGate(feItems, c) {
3150 } 3231 }
3151 3232
3152 phase('Router') 3233 phase('Router')
  3234 +// 完成态 tag 全量预取(1a):放在 Router 之前——其后 featureLoop 的每 REQ / 每 FE dedup、
  3235 +// runMilestone 的 tag 存在性查询全部走内存快照。失败静默回退逐项查(见 primeTagSnapshot)。
  3236 +await primeTagSnapshot()
3153 // Router 语义断言(feItems/reqs 互斥)+ id 形状硬约束(防 shell 注入:id 直接拼入 `git ... ${id}`)。 3237 // Router 语义断言(feItems/reqs 互斥)+ id 形状硬约束(防 shell 注入:id 直接拼入 `git ... ${id}`)。
3154 // id 形状(assertSafeId)是**安全护栏**——失败立即硬 halt,绝不重试绕过。 3238 // id 形状(assertSafeId)是**安全护栏**——失败立即硬 halt,绝不重试绕过。
3155 // reqs/feItems 互斥违例可由仲裁带 guidance 重跑 router 纠正(绝对上限 ADJUDICATE_MAX)。 3239 // reqs/feItems 互斥违例可由仲裁带 guidance 重跑 router 纠正(绝对上限 ADJUDICATE_MAX)。
@@ -3552,7 +3636,7 @@ async function prepareParallelWave(wave) { @@ -3552,7 +3636,7 @@ async function prepareParallelWave(wave) {
3552 return null 3636 return null
3553 } 3637 }
3554 try { 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 const mc = makeCtx() 3640 const mc = makeCtx()
3557 // (1) 主根复位(D2 + 补 3c):module-* 分支必须未被主根持有(worktree add 才不 fatal),且兄弟 3641 // (1) 主根复位(D2 + 补 3c):module-* 分支必须未被主根持有(worktree add 才不 fatal),且兄弟
3558 // milestone 的 merge 要落在默认分支主根上。脏树恢复的目标分支 = **当前 HEAD 分支**(传默认分支 3642 // milestone 的 merge 要落在默认分支主根上。脏树恢复的目标分支 = **当前 HEAD 分支**(传默认分支
@@ -3717,7 +3801,7 @@ if (overlapOn) { @@ -3717,7 +3801,7 @@ if (overlapOn) {
3717 // 后端全部 done + phase1 done → phase2:reconcile(合 default→frontend-phase + 移除 phase1 worktree)→ 3801 // 后端全部 done + phase1 done → phase2:reconcile(合 default→frontend-phase + 移除 phase1 worktree)→
3718 // 标准 runModule(feStage='e2e')(主根全栈,skeleton 幂等跳过,behaviorGate/testGate/milestone 原样)。 3802 // 标准 runModule(feStage='e2e')(主根全栈,skeleton 幂等跳过,behaviorGate/testGate/milestone 原样)。
3719 phase('Frontend') 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 await bestEffortAction(removeLaneWorktreePromptM(laneRoot('frontend-phase')), 3805 await bestEffortAction(removeLaneWorktreePromptM(laneRoot('frontend-phase')),
3722 { label:'fe-overlap:rm-worktree', phase:'Milestone', okMsg:'fe-overlap: 已移除 phase1 worktree', failTag:'fe-overlap phase1 worktree 移除' }) 3806 { label:'fe-overlap:rm-worktree', phase:'Milestone', okMsg:'fe-overlap: 已移除 phase1 worktree', failTag:'fe-overlap phase1 worktree 移除' })
3723 const rec = await agentR(mergeDefaultIntoFrontendPromptM('frontend-phase', def.branch), 3807 const rec = await agentR(mergeDefaultIntoFrontendPromptM('frontend-phase', def.branch),