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:
parent
f99b554e9e
commit
38efb3eb17
@ -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 命令 rc≠0)→ 返回 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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user