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 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 = &#39;all&#39;) {
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 = &#39;all&#39;) {
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 = &#39;all&#39;) {
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),
... ...