feat(deliver): git收口引擎化——deliver时引擎代为add+commit本任务写入的apps/modules仓库,收口核验记录回填交付件(2026-09-16 pbls M1a编造git证据事故根治)

事故:M1a develop在交付摘要声称'git status --porcelain为空已push',QC实测modules/pbl_blueprint约60项未提交→证据链不可信→连退3次达上限→fault→项目paused。deliver入口此前只有占位文档/配图/类型三门禁,无git收口核验,靠QC事后抽查→退回后agent照样编造→死循环(LLM编造工具结果高发形态)。

修法(机制化,不靠LLM自觉):_enforce_git_closure在deliverable files落盘后对本任务写入过的apps/{x},modules/{x}仓库根引擎代为add+commit(无远程只commit,非git自动init);projects/过程仓库跳过(多角色并发git锁)。收口核验记录(含'agent未提交,引擎代为收口N项')回填交付件正文——给QC/PM真实仪表盘,agent的git口头声明从此可对照引擎事实。收口失败不阻断deliver(环境原因卡死任务更糟),回填警告让QC按事实核验。
This commit is contained in:
ymq 2026-09-16 12:37:26 +08:00
parent f99b554e9e
commit 38efb3eb17

View File

@ -2396,6 +2396,86 @@ def _validate_stub_docs(space_dir, written_files):
"若因信息缺失或上游故障无法产出正文,用 ask_question 如实冒泡等待回答,禁止交付占位文档。")
async def _enforce_git_closure(space_dir, written_files, do_commit=True):
"""git 收口硬门禁2026-09-16 pbls M1a「编造 git 证据」事故根治)。
事故形状develop 在交付摘要里声称git add -A 全量收口 + commit + push
git status --porcelain 为空git log origin/main..HEAD 为空QC run_shell
实测 modules/pbl_blueprint 有约 60 项未提交变更M/D/?? 声明与磁盘事实矛盾
证据链不可信 连退 3 次达上限 fault 项目 paused
这是LLM 编造工具结果的高发形态 deliver 入口此前只有占位文档/配图形态/
交付类型三个门禁**没有 git 收口核验**只能靠 QC 事后 run_shell 抽查
QC 轮次有限退回后 agent 下次照样编造 死循环
机制处置确定性不靠 LLM 自觉提交时机铁律模块/应用仓库 develop 阶段必须
本地提交给 QC 留证据项目过程仓库才走审核后统一提交
· 只处理本任务实际写入过的 apps/{x}modules/{x} 仓库根projects/ 过程仓库跳过
避免多角色并发写的 git 锁争用
· do_commit=True 时由引擎代为 add -A + commit无远程只 commit git 目录自动 init
已提交 agent 的口头声明变成引擎保证的事实
· 收口失败git 命令 rc0 返回 ok=False + 可行动原因由调用方拒绝 deliver
Returns: (ok: bool, report: str) report 为空=本任务未触及代码仓库否则为核验记录
调用方回填交付件 QC/PM 当仪表盘agent 声称的收口是否属实
"""
touched = set()
for f in written_files or []:
rp = f if os.path.isabs(f) else os.path.join(space_dir, f)
try:
rel = os.path.relpath(rp, space_dir)
except ValueError:
continue
parts = rel.split(os.sep)
# 只对仓库根apps/{name}、modules/{name}收口projects/ 过程仓库不在此提交
if len(parts) >= 2 and parts[0] in ('apps', 'modules'):
touched.add(os.path.join(space_dir, parts[0], parts[1]))
if not touched:
return True, ''
lines, ok = [], True
for rp in sorted(touched):
label = os.path.relpath(rp, space_dir)
if not os.path.isdir(rp):
continue
was_git = os.path.isdir(os.path.join(rp, '.git'))
try:
st = await _run_shell('git status --porcelain', rp, 20)
except Exception as e:
st = {'rc': -1, 'stdout': '', 'stderr': str(e)}
dirty = (st.get('stdout') or '').strip()
if st.get('rc', -1) != 0 and was_git:
ok = False
lines.append(f"{label}: git status 执行失败({str(st.get('stderr', ''))[:120]}),无法核验收口")
continue
if not dirty and was_git:
continue # 本来就干净,无需记录(避免噪音)
if not do_commit:
if dirty:
ok = False
n = len(dirty.splitlines())
lines.append(f"{label}: 工作区有 {n} 项未提交变更,交付前必须 git_commit_push 收口")
continue
# 引擎代为收口add -A + commit无远程只 commit非 git 目录自动 init
n_dirty = len(dirty.splitlines()) if dirty else 0
r = await _git_commit_push(rp, 'deliver: 交付收口(引擎代为提交)')
rc = r.get('rc', -1)
if rc != 0:
ok = False
lines.append(f"{label}: 引擎收口失败——{str(r.get('message', ''))[:160]}")
continue
st2 = await _run_shell('git status --porcelain', rp, 20)
still = (st2.get('stdout') or '').strip()
if still:
ok = False
lines.append(f"{label}: 收口后仍有 {len(still.splitlines())} 项未提交变更(可能被并发写入)")
continue
if n_dirty:
lines.append(f"{label}: agent 未提交,引擎代为收口 {n_dirty} 项变更({str(r.get('message',''))[:80]}")
elif not was_git:
lines.append(f"{label}: 非 git 目录,引擎已 git init 并首次提交")
return ok, "\n".join(lines)
def _validate_deliverable_type(role, deliverable_type, allowed_types):
"""deliver 类型守卫:角色声明了合法类型清单时,非法类型拒绝并返回清单(可行动报错)。
@ -2921,6 +3001,21 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
else:
logger.error(f"code file failed: {abs_path} err={msg}")
# git 收口硬门禁2026-09-16 pbls M1a「编造 git 证据」根治):在 deliverable files
# 全部落盘之后执行(此前收口会漏提交 files 内容)。引擎代为 add+commit 本任务
# 写入过的 apps/modules 仓库——「已提交」从 agent 口头声明变成引擎保证的事实;
# 核验记录含「agent 未提交引擎代为收口」回填交付件正文QC/PM 拿真实仪表盘。
_gc_ok, _gc_report = await _enforce_git_closure(
space_dir, list(written_files) + files_written, do_commit=True)
if _gc_report:
result_text += ("\n\n---\n## git 收口核验(引擎自动执行,非 agent 声明)\n"
+ _gc_report)
if not _gc_ok:
result_text += ("\n⚠️ 收口未完全成功QC 请按上述事实核验,勿轻信交付摘要中的"
" git 声明。")
logger.info(f"git closure enforced: task={task_id} ok={_gc_ok} "
f"report={_gc_report[:200]}")
# 写交付件文档result 摘要快照,评审留痕用;交付件本体是 docs/ 编号树 + files 清单)
file_path = ''
if result_text: