Skip to content

Fix/parser backed prewrite validation - #458

Open
NanluQingshi wants to merge 14 commits into
ceilf6:developfrom
NanluQingshi:fix/parser-backed-prewrite-validation
Open

Fix/parser backed prewrite validation#458
NanluQingshi wants to merge 14 commits into
ceilf6:developfrom
NanluQingshi:fix/parser-backed-prewrite-validation

Conversation

@NanluQingshi

Copy link
Copy Markdown
Contributor

Linked Issue Or Context

Summary

  • Replace quote/bracket heuristics with TypeScript parser diagnostics for TypeScript, JavaScript, JSX/TSX, and JSON writes, eliminating false blocks for apostrophes, multiline templates, JSX text, and regex literals.
  • Add a pure shared line-range patch projector so executor preflight and apply_patch validate the same complete projected content.
  • Validate create_file and arbitrary apply_patch results before disk mutation, bind patch execution to the validated original-content hash, and serialize same-target writes so the validated projection cannot race a concurrent write.
  • Preserve guard enabledChecks, validation-failure events, phase accounting, and desktop reducer behavior with regression coverage.

Impact Scope

  • @frontagent/hallucination-guard: parser-backed synchronous syntax validation and syntax/import facades.
  • @frontagent/shared: reusable deterministic patch projection.
  • @frontagent/mcp-file: create/patch pre-write validation and stale-original hash rejection.
  • @frontagent/core: executor preflight, same-target write ordering, internal argument propagation, and validation-failure events.
  • Desktop state: consumes validation failures without leaving phases active.

GitNexus Impact Summary

  • Risk level: HIGH.
  • Critical skeleton changes: Executor.executeStep, PhaseRunner, and the create_file / apply_patch MCP boundary.
  • GitNexus impact: detect_changes({ scope: "compare", base_ref: "develop" }) reported 43 changed symbols across 25 files, 15 affected execution flows, and HIGH aggregate risk. Executor has 18 direct upstream dependents and affects agent execute, planOnly, executeSteps, and constructor flows. HallucinationGuard is MEDIUM (6 direct dependents, 2 agent flows); PhaseRunner, applyPatch, createFile, checkSyntaxValidity, validateSourceSyntax, and applyFilePatches were LOW individually.
  • Verification: context(Executor.executeStep) confirmed pre-write validation sits before callTool and preserves trace/failure paths. Focused executor, phase, guard, file-tool, shared, and desktop tests passed, followed by the full local gate.

Verification

  • pnpm agent:bootstrap — passed.
  • pnpm quality:predev — passed.
  • Focused tests — 10 files / 171 tests passed.
  • pnpm quality:precommit — passed (lint, full typecheck, full tests, 38 workflow tests).
  • pnpm quality:local — passed (Contract Guard, all precommit checks, and 14/14 package builds).

Checklist

  • I have linked an issue or explained why this PR stands alone.
  • I have kept the diff focused on the stated change.
  • I have run pnpm quality:precommit, or explained why it could not run.
  • I have run pnpm quality:local for critical skeleton changes, or explained why it could not run.
  • I have updated docs or tests when behavior, public APIs, or Harness contracts changed.
  • For critical skeleton changes, I have filled the GitNexus impact summary with concrete results.

NanluQingshi and others added 3 commits August 6, 2026 00:27
Move line-range patch projection into a reusable pure helper so executor preflight and the file tool evaluate identical projected content.

Co-Authored-By: Claude <noreply@anthropic.com>
Replace quote and bracket heuristics with TypeScript parser diagnostics for TypeScript, JavaScript, JSX/TSX, and JSON. Expose syntax-only validation for deterministic pre-write callers while preserving enabled-check behavior.

Fixes ceilf6#413

Co-Authored-By: Claude <noreply@anthropic.com>
Preflight create_file and arbitrary apply_patch content with the parser-backed guard, bind patch execution to the validated original hash, and serialize same-target writes so validated projections cannot race.

Complete the general syntax-validation path left intentionally narrow in ceilf6#402 and surface validation failures consistently to desktop consumers.

Co-Authored-By: Claude <noreply@anthropic.com>
@NanluQingshi
NanluQingshi requested a review from ceilf6 as a code owner August 5, 2026 17:50

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ ceilf6/repo-guard

代码评审报告: Fix/parser-backed prewrite validation (#413, #387)

风险等级:
处理建议: 请求修改
决策摘要: 解析器替换启发式这条主线是对的、覆盖也扎实,但 create_filewx 失败路径会 unlinkSync 掉并发写入者刚创建的文件(数据丢失),且 PR 声称的「file-tool 边界写盘前校验」与「同目标写序列化」在代码里并不存在——合并前需要先修 race 回滚路径并把描述/CHANGELOG 与实现对齐。

级联分析

  • 变更符号: Executor.executeStep / 新增 validateWriteBeforeExecutionPhaseRunner.executePhaseSequential|ParallelHallucinationGuard.validateSyntax|validateImports|validateCodecheckSyntaxValidity/新增 validateSourceSyntaxapplyPatchcreateFile、新增 applyFilePatchesdetectLanguageAgentEvent.validation_failed.stage
  • 受影响流程: agent execute/planOnly/executeSteps 的写盘链路;phase 顺序/并行执行与 recovery;apply_patch/create_file MCP 边界;desktop consoleReducer 日志;benchmark 遥测聚合。
  • 变更集外调用方: packages/core/src/executor/progress-enforcement.ts:42-47(已有同款 needsRollback 中止语义,本 PR 使 PhaseRunner 与之对齐);benchmarks/eval/report.mjs:89-90(只渲染 pre_execution/post_write 两个 stage,未加 pre_write);benchmarks/eval/run-eval.mjs:287-289(按 type:stage 通用聚合,无需改动);PatchResult.validation 在全仓无消费方(仅 apply-patch.test.ts:370)。
  • 置信度: medium —(graph 证据来自 PR 内 GitNexus 摘要,未由我复核)我的结论基于仓库只读检查与文本搜索:needsRollbacksyntaxValidvalidateCode/Syntax/ImportscollectedContext.files.set 均已全仓 grep 确认。

问题发现

  1. [高] create_filewx 失败路径会删除并发写入者刚创建的文件

    • 证据: create-file.ts:86flag: 'wx' 写入;EEXIST 落入 create-file.ts:95-98 的 catch,调用 snapshotManager.rollback(snapshotId);该快照在 create-file.ts:69-72 因「检查时文件不存在」被建为 'create' 类型,而 SnapshotManager.rollback'create' 快照执行 unlinkSyncsnapshot.ts:113-123)。
    • 受影响调用方/流程: 并行 phase 中两个 create_file 步骤命中同一路径(phase-runner.ts:198-205 同一波内并发);任何在 existsSync 检查与写入之间创建该文件的外部进程。
    • 最小可行修复: 在 catch 中区分 EEXIST——直接返回既有的 File already exists: ... 错误并跳过 rollback(本次调用没有创建该文件,无权删除它);其余错误保持现有回滚。
  2. [高] file-tool 边界并未做「写盘前校验」,PR 描述与实现不符

    • 证据: apply-patch.ts:68 计算 validation 后,apply-patch.ts:71-76 无条件写盘并在 78-83 返回 success: truecreate-file.tswx 外没有任何语法校验。全仓 grep syntaxValid 显示 PatchResult.validation 无消费方(仅 apply-patch.test.ts:370 断言),新加的 apply-patch.test.ts:346-375 用例本身就把「syntaxValid=false 而 success=true」固化为期望。
    • 受影响调用方/流程: 任何不经 Executor.executeStep 的写入(@frontagent/mcp-file 作为独立 MCP server 被外部 client 调用、hook 路径、未来调用方)仍会把无效内容留在磁盘——正是 #387「no bad state ever exists」要消除的状态。附带成本:mcp-file/package.json:26 为此新增 @frontagent/hallucination-guard 运行时依赖(连带 typescript),每次 patch 额外全量解析一遍,收益为零。
    • 最小可行修复: 二选一——(a) 在 applyPatch/createFile 里真正拦截(syntaxValid === false 时不写盘、返回 success: false,并给非代码内容留显式 opt-out);(b) 撤掉 mcp-file 对 guard 的依赖,把 PR 描述与 CHANGELOG 改为「仅 executor preflight 拦截」。
  3. [中] JSON 走严格 JSON.parse 且已升级为写盘否决权,JSONC 文件(tsconfig.json、.vscode/*.json)会被误拦并连带中止整个 phase

    • 证据: executor.ts:577-578 只跳过 yamljson 进入否决路径;guard.ts:69syntaxValidity 默认 truesyntax-validity.ts:178-181JSON.parse。代码生成 prompt 明确要求产出 tsconfig.jsonllm/code-generation.ts:100),而主流 TS 模板的 tsconfig 带注释/尾逗号。失败后 executor.ts:198needsRollback: true,进而触发 phase-runner.ts:295-300 的整段 phase 跳过。
    • 受影响调用方/流程: 所有生成配置文件的 create_file 步骤;这与 #413 要消除的 false block 属同一类,只是换了语言。
    • 最小可行修复: 对 tsconfig*.json / jsconfig*.json / .vscode/*.json 用容忍注释的解析(如 ts.parseConfigFileTextToJson),或把 JSON 的 block 严格限定在纯 JSON 路径、其余降级为 warn。
  4. [中] 「serialize same-target writes」未实现,描述与代码不一致

    • 证据: packages/core/src/executor 下无任何 lock/queue/串行化实现(已 grep mutex|lock|queue|serialize|inFlight|sameTarget);phase-runner.ts:198-205 仍把同一波 ready 步骤全部并发提交。真正起作用的是 apply-patch.ts:49-56 的 hash 校验,它把竞争变成「后手失败」而非「顺序执行」。
    • 受影响调用方/流程: 并行模式下同文件多补丁步骤——其中一个必然以 changed since executor preflight 失败,且 executor-skills.ts:292-294 明确将其定为不可跳过。
    • 最小可行修复: 修正描述为「hash 绑定拒绝陈旧基线」,或若确实要两个写都成功,则补一个按目标路径的顺序化调度。
  5. [中] PhaseRunner 的 phase 级中止范围超出既定目标,且未在描述/CHANGELOG 说明

    • 证据: phase-runner.ts:220-222226-235(并行)与 295-300(顺序)对任意 needsRollback 失败中止整段 phase。needsRollback 的既有来源包括 executeStep 内任何抛错(executor.ts:246-260)与 required validation 失败(executor.ts:244);例如 executor-skills.ts:242 抛出的 Cannot apply patch: file not found in context 过去只失败该步,现在会把 phase 内无依赖关系的步骤一并标为 skipped(recovery 成功时才可能被重跑,phase-runner.ts:399-423)。方向上与 progress-enforcement.ts:42-47 一致,可以接受,但属于未声明的行为变更,两个新测试只覆盖了刻意构造的写失败。
    • 最小可行修复: 把跳过范围收敛为失败步骤的(传递)依赖方,或在 CHANGELOG 写明 phase-abort 语义并补一个「异常路径同样中止 phase」的测试。
  6. [低] 遥测下游未同步:pre_write 阶段在消融报告里不可见

    • 证据: benchmarks/eval/report.mjs:89-90 只渲染 pre_executionpost_write 两行;语法拦截已整体迁移到 pre_writeexecutor.ts:187),该表将对新的主要拦截来源恒显 0,与 #388「可计数的拦截量」目标冲突(run-eval.mjs:287-289 已通用记录该键,无需改)。
    • 最小可行修复: 在 report.mjs 的分阶段表格加一行 pre_write

行级发现

  • [packages/mcp-file/src/tools/create-file.ts:86] wx 的 EEXIST 分支落到 catch 里的 snapshotManager.rollback(),而该快照是 'create' 类型 → unlinkSync 掉并发写入者的文件;请在 catch 中识别 EEXIST,返回 already-exists 错误且不回滚。
  • [packages/mcp-file/src/tools/apply-patch.ts:74] 上一行算出的 validation 从未参与决策,syntaxValid === false 时仍然写盘并返回 success: true;要么在此之前拦截,要么去掉 mcp-file 对 guard 的运行时依赖并修正 PR 声明。
  • [packages/core/src/executor/executor.ts:578] 只排除 yaml,使 .json 获得写盘否决权,而校验用的是严格 JSON.parse;tsconfig/.vscode 这类 JSONC 会被误拦,请对这些路径改用容忍注释的解析。
  • [packages/core/src/executor/phase-runner.ts:295] needsRollback 已被既有异常路径广泛置真,这里的 break 会跳过 phase 内互不依赖的步骤;建议把跳过限定在失败步骤的依赖闭包,或在 CHANGELOG 明确该语义。
  • [packages/core/src/executor/executor.ts:707] 写后校验只剩 import 检查;当 preflight 返回 undefined(如 create_filecontent 非字符串)时,语法校验将完全缺失,而此前 validateCode 会覆盖该情形。

Karpathy 评审

  • 假设: 隐含假设「executor 是 file tool 的唯一调用方」——mcp-file 是独立 MCP server,该假设未被任何机制保证(发现 2)。另一处隐含假设是 collectedContext 中的内容与磁盘一致,现由 hash 校验兜底,方向正确。
  • 简洁性: applyFilePatches 抽到 @frontagent/shared 是本 PR 最好的部分——删掉了 apply-patch 内约 130 行重复的边界/重叠校验,并让 preflight 与实际写入共用同一投影,属于真正减少复杂度的重组。反面是 mcp-file 里那条「算了但不用」的校验链路(发现 2),是纯增复杂度。
  • 结构质量: guard 的 validateSyntax/validateImports 拆分让 validateCode 变成组合调用,enabledChecks 语义保持一致,是干净的边界改进。syntax-validity.ts 由 178 行手写扫描降为解析器驱动,理解成本明显下降。
  • 变更范围: 超出既定目标的部分有两处:PhaseRunner 的 phase 级中止(发现 5)、detectLanguage 新增 mts/cts(无害且被 preflight 需要)。phase-runner.test.ts:335rresolve 重命名属无关噪声。
  • 验证: 单测覆盖到位(TSX/JSX/正则/撇号/模板、TS-in-JS、stale hash、pre_write 事件、reducer 三阶段)。但没有一条测试覆盖本 PR 新引入的失败路径:EEXIST 回滚、JSONC 拦截、非 dryRun 下 syntaxValid=false 仍写盘。

缺失覆盖

  • createFile 在 existsSync 之后、写入之前被并发创建:断言 EEXIST 后目标文件仍然存在(当前会被删除)。
  • applyPatch 非 dryRun、投影结果语法无效:断言磁盘内容与返回值符合期望的契约(现状是写入 + success: true,与 PR 描述矛盾)。
  • create_filetsconfig.json(含注释):断言不被 pre-write 拦截,或明确记录为预期拦截。
  • executeStep 内部抛错(如 apply_patch 缺上下文)时,phase 内无依赖步骤是否应被 skipped——当前无测试固化该新语义。
  • #387 的验收证据来自冻结的 30 任务 benchmark(benchmarks/results/2026-07-12-sdd-ablation.md 的 TS1127 落盘)。PR 的验证只列了单测与本地 gate;full 臂(guard 开启,run-eval.mjs:94)重跑一次并给出围栏不再落盘的结果,会显著提高对该 issue 已修复的信心。

if (overwrite) {
writeFileSync(safePath.fullPath, content, 'utf-8');
} else {
writeFileSync(safePath.fullPath, content, { encoding: 'utf-8', flag: 'wx' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wx 的 EEXIST 分支落到 catch 里的 snapshotManager.rollback(),而该快照是 'create' 类型 → unlinkSync 掉并发写入者的文件;请在 catch 中识别 EEXIST,返回 already-exists 错误且不回滚。

writeFileSync(safePath.fullPath, newContent, 'utf-8');
snapshotManager.updateSnapshotContent(snapshotId, newContent);
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
writeFileSync(safePath.fullPath, projected.content, 'utf-8');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

上一行算出的 validation 从未参与决策,syntaxValid === false 时仍然写盘并返回 success: true;要么在此之前拦截,要么去掉 mcp-file 对 guard 的运行时依赖并修正 PR 声明。

Comment thread packages/core/src/executor/executor.ts Outdated
const path = typeof toolParams.path === 'string' ? toolParams.path : undefined;
if (!path) return undefined;
const language = detectLanguage(path);
if (!language || language === 'yaml') return undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

只排除 yaml,使 .json 获得写盘否决权,而校验用的是严格 JSON.parse;tsconfig/.vscode 这类 JSONC 会被误拦,请对这些路径改用容忍注释的解析。

onStepComplete(step, output);
}

if (!output.stepResult.success && output.needsRollback) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needsRollback 已被既有异常路径广泛置真,这里的 break 会跳过 phase 内互不依赖的步骤;建议把跳过限定在失败步骤的依赖闭包,或在 CHANGELOG 明确该语义。

);
return codeValidation;
if (language === 'typescript' || language === 'javascript') {
return this.config.hallucinationGuard.validateImports(content, path);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

写后校验只剩 import 检查;当 preflight 返回 undefined(如 create_filecontent 非字符串)时,语法校验将完全缺失,而此前 validateCode 会覆盖该情形。

NanluQingshi and others added 4 commits August 7, 2026 16:39
Use TypeScript JSON parsing for tsconfig, jsconfig, and VS Code settings while keeping package and application JSON strict.

Co-Authored-By: Claude <noreply@anthropic.com>
Enforce parser validation at create and patch tool boundaries, preserve dry-run diagnostics, and discard speculative snapshots when exclusive creation loses an EEXIST race.

Co-Authored-By: Claude <noreply@anthropic.com>
Defer same-target writes into later parallel waves, keep distinct targets concurrent, and let dependency failures skip only dependents instead of aborting unrelated phase work.

Co-Authored-By: Claude <noreply@anthropic.com>
Render the new pre_write telemetry stage and execute the report against distinct arm counts to keep validation metrics complete.

Co-Authored-By: Claude <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ ceilf6/repo-guard

代码评审报告: Fix/parser-backed prewrite validation (#413, #387)

风险等级:
处理建议: 需要人工判断
决策摘要: 工程实现正确、测试扎实,两个 issue 的验收点基本达成;但 #413 明确留给维护者的打包决策在本 PR 中被顺带做掉了——typescript 成为 guard 的运行时依赖后会被整体打进已发布的 CLI bundle(build.mjs 的 external 未同步),且该决策没有任何体积测量或记录,需要维护者签字后再合。

级联分析

  • 变更符号: validateSourceSyntax / checkSyntaxValidityHallucinationGuard.validateSyntax|validateImports|validateCodeapplyFilePatches(新)、applyPatchcreateFileSnapshotManager.discardSnapshotExecutor.executeStep|validateWriteBeforeExecution|validateAfterExecution|getWriteTargetPhaseRunnerDepsAgentEvent.validation_failed.stage
  • 受影响流程: executor 写盘前拦截 → callTool → 写后 import 校验;phase 并行调度的同目标串行化;desktop 事件日志;benchmark 报表;mcp-file server dispatch;CLI/VSCode 打包图。
  • 变更集外调用方(我实际查证,非图谱):
    • packages/runtime-node/src/mcp-clients.ts:38-48 进程内直传 args,__frontagentExpectedOriginalHash 能到达 applyPatch
    • packages/mcp-file/src/server.ts:90 stripInternalArgs 会剥掉 __frontagent*,走 stdio 的外部 MCP 路径上 stale-base 保护静默失效(非降级失败,是"检查不存在")⚠️
    • PhaseRunner 全仓仅 executor.ts:60 一处构造,新增必填 getWriteTarget 无遗漏调用方 ✅
    • executor-skills.ts:292 的新短路先于 'Cannot apply patch' 命中,stale 错误确实是致命而非静默跳过 ✅(新错误串同时含两个子串,顺序是对的)
    • validateBeforeExecution:406-419 会为 apply_patch 自动读文件入 context,因此 preflight 里 "original content unavailable" 分支基本是兜底 ✅
    • HallucinationGuard 新增两个 public 方法会结构性收紧 ExecutorConfig.hallucinationGuard(本 PR 的 executor 测试 fake 全部要补两个方法即为证据),对 @frontagent/core 外部使用者是轻微契约扩宽。
  • 置信度: medium(无代码图谱输出可复核,结论来自 diff + 仓库文本查证;PR 自述的 GitNexus 数据未独立验证)

问题发现

  1. [高] typescript 变成运行时依赖后被整体打进已发布的 CLI 产物

    • 证据: packages/hallucination-guard/package.json:24typescript 移到 dependenciespackages/hallucination-guard/src/checks/syntax-validity.ts:3 是全仓唯一 import ts from 'typescript'build.mjs:15external 仍只有 ['playwright','ts-morph']dist/index.mjspackage.json:files 中发布的唯一 CLI 产物。ts-morph 此前是 external,所以 TS 编译器此前不在 CLI bundle 里。
    • 受影响调用方/流程: pnpm build:verify → npm 发布的 fa CLI;apps/vscode/build.mjs:11 同样不 external,vsix 会多带一份 TS。
    • 影响: 已发布产物体积量级变化(TS 完整编译器约 8–10MB 未压缩)+ sourcemap,且这正是 #413 写明"是维护者的决定"的那一项,PR 描述里没有任何体积数据或取舍说明。
    • 最小可行修复: 二选一并写进 PR 说明——(a) build.mjs/apps/vscode/build.mjstypescript 加入 external,并在根 dependencies 声明;(b) 明确接受内联,附上 dist/index.mjs 变更前后的体积数字。
  2. [中] enabledChecks.syntaxValidity: false 不再能端到端关闭语法否决

    • 证据: packages/mcp-file/src/tools/create-file.ts:65-68apply-patch.ts:58-68 无条件调用 validateFileSyntax;guard 侧的开关(guard.ts:187)只影响 executor preflight。
    • 受影响调用方/流程: 任何以 enabledChecks.syntaxValidity=false 运行的部署,写盘仍会被文件工具拦截;PR 描述"Preserve guard enabledChecks"与实际端到端行为不符。
    • 影响: 解析器一旦对某个合法文件误报,使用者没有任何逃生口(#413 正是因为"检查不可靠时不该有 veto"才被立项)。
    • 最小可行修复: 要么把文件工具的校验做成可关闭参数(默认开),要么在 CHANGELOG/README 明确"文件工具层的语法校验不受 guard 配置控制"。
  3. [中] JSONC 白名单过窄,.json 写入的新误报会直接否决写盘

    • 证据: syntax-validity.ts:212-219 仅放行 tsconfig*/jsconfig*.vscode/*.json,其余 .json 走严格 JSON.parse
    • 受影响调用方/流程: agent 写 .eslintrc.json.devcontainer/devcontainer.jsonturbo.json 等带注释/尾逗号的配置时,现在是 block 而不是"写下去后报失败";空内容 .json 同样被拒。
    • 影响: 拦截误报的代价从"误导性失败"升级为"写不进去",属于 #413 关心的方向。
    • 最小可行修复: 把白名单扩到这几类公认 JSONC 文件并补测试,或对非 manifest 类 .json 的解析失败降级为 warn。
  4. [中] preflight 的管线失败被伪装成 syntax_validity block,污染 pre_write 拦截指标

    • 证据: executor.ts:613-628 把 "patches must be an array" / "original content unavailable" 包成 type: 'syntax_validity', severity: 'block',随后由 emitValidationFailed('pre_write', ...)executor.ts:194)发射;benchmarks/eval/report.mjs:90 新增行会把它们计进"写盘前内容拦截"。
    • 受影响调用方/流程: #388 建立的"拦截量可计数"语义——emitValidationFailed 上方的注释正是说工具/管线失败不能算拦截。
    • 最小可行修复: 给这类失败一个独立 type(如 preflight_unavailable),或在这条路径上不发射 validation_failed
  5. [中] stale-hash 由缓存 context 单点决定,且没有自愈路径

    • 证据: executor.ts:649-654collectedContext.files 里的内容算哈希;validateBeforeExecution:406 只在缺失时才自动读盘;executor-skills.ts:292 把 stale 错误标为致命不可跳过。
    • 受影响调用方/流程: 若同一 run 中 run_command 或外部进程改了该文件,apply_patch 会硬失败,而重试仍复用同一份陈旧缓存 → 重复失败直到 recovery 次数耗尽。
    • 影响: 相比旧行为(静默按行号打到错内容上)更安全,但把可恢复情形变成了不可恢复。
    • 最小可行修复: 收到 stale 错误时清除 collectedContext.files 中该条目(或 apply_patch preflight 前强制重读),让重试有机会成功。
  6. [低] 新增包边界上的松类型与重复映射

    • 证据: packages/mcp-file/src/syntax-validation.ts:11-19 通过 'errors' in result.details + as Array<{...}> 把 guard 结果强转,而 guard 已导出 SyntaxErrorDetail;同文件 43-49 行又复制了一份扩展名→语言映射,与 phase-ordering.ts:144-164 并行存在(本 PR 为了 .mts/.cts 同时改两处,正是漂移成本的实证)。
    • 最小可行修复: guard 侧导出返回 SyntaxErrorDetail[] 的类型化入口;把 detectLanguage 提到 @frontagent/shared 供两侧复用。

行级发现

  • [packages/hallucination-guard/package.json:24] typescript 进 dependencies 后会被 build.mjs 内联进发布的 dist/index.mjs(external 只有 playwright/ts-morph);要么把 typescript 加入 external 并在根 dependencies 声明,要么在 PR 中记录产物体积变化。
  • [packages/mcp-file/src/tools/create-file.ts:65] 这里的语法校验对所有调用方无条件生效,enabledChecks.syntaxValidity: false 无法关闭;补一个可选开关或明确文档化该层不可配置。
  • [packages/hallucination-guard/src/checks/syntax-validity.ts:179] JSONC 放行仅限 tsconfig/jsconfig/.vscode,.eslintrc.jsondevcontainer.jsonturbo.json 等带注释配置会被直接否决写盘;扩展白名单并补测试。
  • [packages/core/src/executor/executor.ts:624] "original content unavailable" 属于管线失败而非语法拦截,却用 syntax_validity/block 上报,会被计进 pre_write 拦截指标;换独立 type 或此路径不发射事件。
  • [packages/core/src/executor/executor.ts:651] 哈希取自缓存 context,命中 stale 后错误被标为致命且缓存不刷新,重试必然复现;失败时清除该文件的 context 条目。
  • [packages/mcp-file/src/syntax-validation.ts:13] result.details.errors as Array<{...}> 在新包边界上绕过类型;改为 guard 侧导出返回 SyntaxErrorDetail[] 的入口。
  • [packages/mcp-file/src/syntax-validation.ts:43] 与 phase-ordering.ts:detectLanguage 重复的扩展名映射,本 PR 已被迫同步两处;下沉到 @frontagent/shared 单点维护。

Karpathy 评审

  • 假设: 隐含假设"executor 与文件工具永远同进程"——runtime-node 成立,但 stdio server 路径上 stripInternalArgs 会让 stale-base 保护静默消失,这一点没有在代码或文档中言明。另一隐含假设是 collectedContext 始终等于磁盘现状(见问题 5)。
  • 简洁性: 主体是净简化——applyPatch 从 275 行降到约 110 行,投影逻辑收敛到 @frontagent/shared 单一实现,executor 与工具共用同一投影;启发式括号/引号计数被解析器取代,删除的是真复杂度而非搬家。保留的 checkOuterMarkdownFence 有正当理由(```ts…``` 会被 TS 当作合法 tagged template 解析通过),是必要例外而非残留。
  • 结构质量: 新增 syntax-validation.ts 属于薄适配层,可接受,但带一处强转和一份重复的扩展名映射(见问题 6)。validateCode 现已无生产调用方,仅测试引用——作为公共 API 保留合理,但值得在文档里标注新入口。
  • 变更范围: 基本贴合目标。phase-runner 的同目标写串行化与 create_filewx 独占写严格说超出 #413 的范围,但它们是"写盘前投影"成立的前提条件,属于必要配套;benchmarks/eval/report.mjs + 新 workflow 测试是可观测性配套,可接受。
  • 验证: 覆盖到位——#413 表格里四个用例(it's、多行模板、JSX 撇号、正则)都有断言,.ts 中 JSX 与 .js 中类型注解的反向用例也有;file tool 层验证了"拒写不留快照不落盘";applyFilePatches 有独立单测。缺口见下。

缺失覆盖

  • 没有端到端用例证明 enabledChecks.syntaxValidity: false 时写盘仍被文件工具拦截(当前测试只覆盖 guard facade 本身),而 PR 描述声称保留了该契约。
  • 没有对发布产物体积的断言或记录,typescript 内联进 dist/index.mjs 属于不可见回归。
  • stdio MCP server 路径(stripInternalArgs 剥掉哈希)下的 stale-base 行为无测试,保护缺失是静默的。
  • .vue / .svelte / .html / .css 写入仍完全不校验(detectLanguage 返回 null),#387 描述的"围栏内容落盘"在这些扩展名上依旧可复现;建议加一条固化该有意缺口的测试或在 CHANGELOG 标注范围。
  • .json 空内容与常见 JSONC 配置(.eslintrc.jsondevcontainer.json)被拒写的行为没有测试固化,属于问题 3 的验证盲区。

"@frontagent/shared": "workspace:*",
"@frontagent/sdd": "workspace:*"
"@frontagent/sdd": "workspace:*",
"typescript": "^6.0.3"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typescript 进 dependencies 后会被 build.mjs 内联进发布的 dist/index.mjs(external 只有 playwright/ts-morph);要么把 typescript 加入 external 并在根 dependencies 声明,要么在 PR 中记录产物体积变化。

return { success: false, error: 'File content must be a string' };
}

const syntaxValidation = validateFileSyntax(content, filePath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里的语法校验对所有调用方无条件生效,enabledChecks.syntaxValidity: false 无法关闭;补一个可选开关或明确文档化该层不可配置。

*/
function checkJsonSyntax(code: string): SyntaxError[] {
function checkJsonSyntax(code: string, filePath?: string): SyntaxErrorDetail[] {
if (filePath && isKnownJsoncConfigPath(filePath)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSONC 放行仅限 tsconfig/jsconfig/.vscode,.eslintrc.jsondevcontainer.jsonturbo.json 等带注释配置会被直接否决写盘;扩展白名单并补测试。

return {
path,
content: '',
validation: this.buildWriteValidationFailure(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"original content unavailable" 属于管线失败而非语法拦截,却用 syntax_validity/block 上报,会被计进 pre_write 拦截指标;换独立 type 或此路径不发射事件。

),
toolParams: {
...toolParams,
__frontagentExpectedOriginalHash: createHash('sha256')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

哈希取自缓存 context,命中 stale 后错误被标为致命且缓存不刷新,重试必然复现;失败时清除该文件的 context 条目。

const result = validateSourceSyntax({ code: content, language, filePath });
const errors =
result.details && typeof result.details === 'object' && 'errors' in result.details
? (result.details.errors as Array<{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

result.details.errors as Array<{...}> 在新包边界上绕过类型;改为 guard 侧导出返回 SyntaxErrorDetail[] 的入口。

return `Syntax validation failed for ${filePath}:${firstError.line}:${firstError.column}: ${firstError.message}`;
}

function detectSyntaxLanguage(filePath: string): 'typescript' | 'javascript' | 'json' | undefined {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

phase-ordering.ts:detectLanguage 重复的扩展名映射,本 PR 已被迫同步两处;下沉到 @frontagent/shared 单点维护。

NanluQingshi and others added 5 commits August 8, 2026 21:58
Keep the parser runtime installed beside the published npm CLI instead of embedding the full compiler, while documenting and testing the dependency-free VSIX's intentional inline bundle.

Co-Authored-By: Claude <noreply@anthropic.com>
Centralize syntax language detection, expose typed parser diagnostics, honor the mutable guard switch only through trusted in-process metadata, and keep external MCP validation default-on.

Co-Authored-By: Claude <noreply@anthropic.com>
Refresh patch context before preparation, reject explicit patches collected against changed content, return machine-readable stale-hash errors, and refresh or invalidate cached content before recovery.

Co-Authored-By: Claude <noreply@anthropic.com>
Count failed checks by stage and type so parser interceptions are reported independently from patch projection, stale context, and malformed preflight inputs.

Co-Authored-By: Claude <noreply@anthropic.com>
Verify the trusted executor marker observes runtime setCheckEnabled changes rather than only constructor defaults.

Co-Authored-By: Claude <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ ceilf6/repo-guard

代码评审报告: Fix/parser-backed prewrite validation

风险等级:
处理建议: 评论
决策摘要: 核心方向正确且实现扎实(解析器取代引号计数、写盘前投影校验、hash 绑定、内部参数在 stdio 边界被剥离),可以合并;但写盘前 veto 对「原文件本就语法非法」的修复场景一刀切拒绝,JSONC 白名单会误伤目标工程中的合法配置文件,建议合入前或紧随其后补这两处。

级联分析

  • 变更符号: validateSourceSyntax / checkSyntaxValidity(返回类型收窄为 SyntaxValidityCheckResultdetailsunknown 收窄为结构化对象——HallucinationCheckResult.details: unknown,赋值兼容,已确认)、HallucinationGuard.validateSyntax/validateImports/isCheckEnabled(新增)、applyPatch/createFile(新增内部参数与写盘前 veto)、applyFilePatches(shared 新增)、Executor.executeStepPhaseRunner.selectConflictFreeStepsdetectLanguage(改为 detectSyntaxLanguage 别名,新增 .mts/.cts/.jsonc)。
  • 受影响流程: executor 写盘前后校验、apply_patch 上下文刷新、并行 phase 同目标写入串行化、mcp-file stdio server 与两个 in-process adapter(runtime-node/src/mcp-clients.ts:38-48apps/cli/src/mcp-client.ts:43-56)、desktop validation_failed 日志、benchmark report 阶段统计。
  • 变更集外调用方: applyPatch/createFile 的三个调用点(server.ts / runtime-node / cli mcp-client)全部只做参数透传,语法 veto 默认开启对它们生效——这是有意的(stripInternalArgs__frontagent 前缀剥离,外部客户端无法关掉,internal-args.ts:10-27 已确认,注释也同步更新)。guard.validateCode 在生产代码中已无调用方(仅剩自身定义与测试 mock),成为仅供外部使用的公开 API。
  • 置信度: medium(我用 Read/Grep 逐一核对了符号、调用方、打包与安全边界,未运行测试;PR 提供的 GitNexus 结论与 diff 一致)。

问题发现

  1. [中] 写盘前 veto 只看投影结果,不看原文件是否本来就非法,阻断增量修复路径

    • 证据: apply-patch.ts:71-82executor.ts:709-713 只对 projected.content 调用语法校验,没有把 originalContent 的合法性纳入判断。#387 自身证据(benchmarks/results/2026-07-12-sdd-ablation.md:markdown 围栏写进 .tsx 并落盘)说明仓库里确实存在已落盘的非法文件;用户「帮我修好这个报错文件」也是核心场景。
    • 受影响调用方/流程: executor preflight + recovery 循环 + 所有 in-process/stdio 的 apply_patch 调用方。文件有多处语法错误、模型分次修复时,每个中间态都会被硬拒(错误信息可读,模型通常能在下一轮一次性补全,因此是「多烧一次 recovery 预算」而不是死锁)。
    • 最小可行修复: 先对 originalContent 跑一次 validateFileSyntax;原文件已非法时不 veto(或仅在错误条数增加时 veto),保持 #387「不引入新的坏状态」的语义。executor preflight 同理(原始内容已在手上)。补一条「patch 一个已非法文件」的回归测试。
  2. [中] JSONC 判定是硬编码文件名白名单,落在会阻断写入的路径上

    • 证据: syntax-validity.ts:227-249 白名单为 *.jsonc / tsconfig|jsconfig*.json / .eslintrc.json / 5 个 .vscode/* / devcontainer。syntax-validity.test.ts 还显式把 turbo.json 钉成严格 JSON,而 Turborepo 的 turbo.json 支持注释;同类还有 biome.json.vscode/keybindings.jsonnest-cli.jsoncomponents.json
    • 受影响调用方/流程: agent 向目标工程写配置文件时被硬失败——正是 #413 要消除的「误判为 block」这一类。名单必然随生态漂移,且分散在 guard 共享路径里。
    • 最小可行修复: 反转策略——JSON.parse 失败后用 ts.parseJsonText 复解,能干净通过时只对少量必须严格的清单(package.json、锁文件、manifest)保留 block,其余降级为 warn;或至少把名单提到可配置的单一常量并补上 turbo.json/biome.json
  3. [低] 非预检语言的成功写入不刷新 collectedContext,会触发一次多余的 stale_patch_context 拦截

    • 证据: executor.ts:253-255 仅在 preflight?.content 存在(即 TS/JS/JSON)时回写缓存;.md/.css/.html/.vuevalidateWriteBeforeExecutionif (!language) return undefinedexecutor.ts:644-645)。随后对同一文件的显式 patch 步骤在 executor.ts:463-481 发现缓存≠磁盘并硬拦截。
    • 受影响调用方/流程: 前端任务里常见的「先写 css/md,再 patch 同一文件」。因为第 462 行在返回拦截前已刷新缓存,重试即可通过,代价是一个失败步骤 + 一次 recovery 配额。
    • 最小可行修复: 任何 create_file/apply_patch 成功后都 files.delete(path)(或写回),不要只覆盖预检过的语言。
  4. [低] dryRun 下语法非法仍返回 success: true

    • 证据: apply-patch.ts:74!dryRun && !validation.syntaxValid,预览态把 success=truevalidation.syntaxValid=false 同时返回,测试固化了该行为。
    • 受影响调用方/流程: 只看 success 的预览调用方会认为补丁没问题。
    • 最小可行修复: 保持不写盘的同时,在 dry-run 结果里补一个明确的 error/errorCode,或在文档里写明 dry-run 的 success 只表示「投影成功」。

行级发现

  • [packages/mcp-file/src/tools/apply-patch.ts:71] 只校验 projected.content;原文件本就语法非法时,任何部分修复补丁都会被拒。建议先校验 originalContent,原文件已非法则跳过 veto(或仅在错误增多时拒绝)。
  • [packages/core/src/executor/executor.ts:709] preflight 同样只看投影结果,与工具层是同一个盲点;此处已持有 originalContent,判断成本几乎为零。
  • [packages/hallucination-guard/src/checks/syntax-validity.ts:227] 白名单缺 turbo.json(Turborepo 允许注释)、biome.json.vscode/keybindings.json 等;建议反转为「JSONC 复解通过即放行,仅少量清单强制严格」,避免名单长期漂移导致误拒写入。
  • [packages/core/src/executor/executor.ts:253] 只有预检覆盖的语言才回写文件缓存,导致 .md/.css 写入后缓存变陈旧并在后续 patch 步骤触发一次多余拦截;成功写入后统一 delete/refresh 即可。

Karpathy 评审

  • 假设: 假设「写盘前内容合法」等价于「写盘后文件合法」——对 create_file 成立,对 apply_patch 依赖 hash 绑定,已用 __frontagentExpectedOriginalHash + wx 独占创建 + phase 内同目标串行三层兜住,这部分假设是显式化且可验证的。未显式化的假设是「被写文件此前是合法的」(见问题 1)。打包这个 #413 点名的「maintainer call」已在 CHANGELOG 记录(CLI external 7.3MB vs 内联 17.4MB,VSIX 有意内联,apps/vscode/src/package.test.ts:51-57 用测试钉住),处理得当。
  • 简洁性: applyFilePatches 抽到 shared 让 executor 与工具共用同一投影,是真正删复杂度的重组;apply-patch.ts 从 275 行手写括号扫描缩到 57 行是净减。代价是同一份内容在 executor 与 mcp-file 各解析一次(JS 路径还额外跑一次 transpileModule 全量 emit),属于跨进程边界的合理纵深防御,但值得在 perf 敏感场景留意。detectLanguage 保留为别名(phase-ordering.ts:144)是薄包装,但用于兼容既有导入,可接受。
  • 结构质量: 分层合理——shared 放纯投影与语言判定,guard 放解析,mcp-file 通过 syntax-validation.ts 做薄适配(未发现 hallucination-guard → mcp-file 反向依赖,无环)。唯一的结构隐患是问题 2 的硬编码名单。
  • 变更范围: 基本聚焦。benchmarks/eval/report.mjs + scripts/tests/benchmark-report.test.mjs 属于新阶段的遥测配套,pnpm-lock.yaml 中 electron-builder 的 peer 展开变化是提升 typescript 到 dependencies 的副产物,均可追溯。
  • 验证: 阶段事件(pre_execution/pre_write/post_write)到 desktop reducer 到 benchmark 报表整条链路都有测试,create_file 的并发竞态、快照 discard、stale hash 都有针对性用例,验证强度与「HIGH 风险骨架变更」匹配。

缺失覆盖

  • apply_patch 打到一个原本就语法非法的文件(部分修复 / 完整修复两种),断言是否应被 veto——这是问题 1 的判定用例。
  • 工具返回 errorCode: 'stale_original_hash'refreshCollectedFileContext 的端到端行为(当前只测了 hash 绑定与工具侧拒绝,没测 executor 侧的上下文刷新与后续重试)。
  • 非预检语言(.md/.css)成功写入后,对同一文件的显式 patch 步骤是否被 stale_patch_context 误拦(问题 3)。
  • turbo.json / biome.json 这类白名单外 JSONC 的期望行为——当前测试把 turbo.json 钉为严格 JSON,需要确认这是刻意决策还是疏漏。

'original',
'modified',
);
const validation = __frontagentSyntaxValidationEnabled

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

只校验 projected.content;原文件本就语法非法时,任何部分修复补丁都会被拒。建议先校验 originalContent,原文件已非法则跳过 veto(或仅在错误增多时拒绝)。

Comment thread packages/core/src/executor/executor.ts Outdated
return {
path,
content: projected.content,
validation: await this.config.hallucinationGuard.validateSyntax(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preflight 同样只看投影结果,与工具层是同一个盲点;此处已持有 originalContent,判断成本几乎为零。

}
}

function isKnownJsoncConfigPath(filePath: string): boolean {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

白名单缺 turbo.json(Turborepo 允许注释)、biome.json.vscode/keybindings.json 等;建议反转为「JSONC 复解通过即放行,仅少量清单强制严格」,避免名单长期漂移导致误拒写入。

Comment thread packages/core/src/executor/executor.ts Outdated
this.validateAfterExecution(step, toolResult, toolParams, preflight?.content),
);

if (preflight?.content && this.isSuccessfulToolResult(toolResult)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

只有预检覆盖的语言才回写文件缓存,导致 .md/.css 写入后缓存变陈旧并在后续 patch 步骤触发一次多余拦截;成功写入后统一 delete/refresh 即可。

NanluQingshi and others added 2 commits August 8, 2026 22:52
Reject contentless patches, validate JSONC grammar strictly, keep dry runs out of file context, bind non-code patches, reject deferred explicit coordinates, and restore overwrite snapshots on write failures.

Co-Authored-By: Claude <noreply@anthropic.com>
Allow incremental repairs when a source file is already invalid, reject invalid dry-run previews, and treat non-manifest JSON configs as JSONC while keeping package, lock, turbo, data, and FrontAgent settings strict.

Co-Authored-By: Claude <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ ceilf6/repo-guard

代码评审报告: Fix/parser-backed prewrite validation (#413)

风险等级:
处理建议: 请求修改
决策摘要: 解析器化 + 写盘前校验的主干设计正确且验证充分,但有两处可确认的误拦截/状态泄漏问题(并发降级标记被写进可持久化的 step.params;JSONC 白名单让合法 JSONC 配置写入被否决),修复量都很小,建议修完再合。

级联分析

  • 变更符号: checkSyntaxValidity / 新增 validateSourceSyntaxHallucinationGuard.validateSyntax|validateImports|isCheckEnabledapplyFilePatches(新, shared)、applyPatch / createFile(新增内部参数与写盘前校验)、Executor.executeStep / validateWriteBeforeExecution / validateAfterExecutionPhaseRunner.selectConflictFreeStepsAgentEvent.validation_failed.stage
  • 受影响流程: executor 单步执行与并发相位调度、apply_patch/create_file MCP 写入、guard 快速校验、桌面端 validation_failed 日志、benchmark 报告生成、CLI/VSIX 打包(typescript 变为运行时依赖)。
  • 变更集外调用方: packages/runtime-node/src/mcp-clients.ts(直接透传 args → 内部参数保留,hash 绑定有效);packages/mcp-file/src/server.tsstripInternalArgs 剥离 __frontagent*(外部 MCP host 默认开启语法校验、无 hash 绑定,属预期降级);packages/core/src/executor/phase-ordering.tsdetectLanguage 已改为 detectSyntaxLanguage 别名,未产生重复实现;executor-skills.ts:293 已显式排除 stale_original_hash,避免被旧的 Cannot apply patch 前缀吞成 skip——这一条是关键且做对了。read_file 无截断,executor 侧 sha256 与 applyPatch 侧 disk 读取一致,hash 绑定成立。
  • 置信度: medium(基于仓库只读检查与文本搜索复核;我未自行运行 GitNexus 或测试,PR 提供的图谱与测试结论未独立复现)

问题发现

  1. [中] 并发降级标记 __frontagentDeferredSameTargetWrite 被写进 step.params 且从不清除

    • 证据: phase-runner.ts:233 直接对 step.params 赋值;executor.ts:456-486 把该标记当作「内容已变」的充分条件,无视实际内容比对。AgentSessionSnapshot.planpackages/core/src/types.ts:75-86)序列化整个 plan,含 step.params
    • 受影响调用方/流程: 并发相位执行 → 被延后的 apply_patch(显式 patches)在本轮必然以 stale_patch_context 失败,即使同目标的前一步骤自身失败/被跳过、文件根本没变;标记随快照落盘后,resume 场景下该步骤永久失败。同时该 key 会随 toolParams 一路传给 in-process MCP 适配器(被工具忽略,但把调度状态泄漏进工具参数)。
    • 最小可行修复: 把延后信息保存在 PhaseRunner 本地(如 Set<stepId> / WeakSet),执行后清除;executor 侧只在「刷新后的磁盘内容 ≠ patches 生成时所依据的内容」时拦截,而不是依赖一个粘性布尔值。
  2. [中] JSONC 白名单给合法 JSONC 配置带来新的写入否决

    • 证据: syntax-validity.ts:227-249 仅放行 *.jsonc / tsconfig|jsconfig*.json / .eslintrc.json / 五个 .vscode/* / devcontainer;其余 .jsonJSON.parse 严格路径且 severity: 'block'syntax-validity.test.ts 更明确断言 turbo.json 必须失败。而 turbo.jsonbiome.jsondeno.json.vscode/keybindings.json 等在生态里都合法带注释——本仓库自身就用 turbo/biome。经 create_file/apply_patch 写入或修改这类已含注释的文件,会被写盘前整体拒绝。
    • 受影响调用方/流程: executor 写盘前 veto、createFile/applyPatch 工具层默认开启的校验(外部 MCP host 无法关闭)。方向是 fail-closed 不会损坏文件,但这正是 #413 想消除的「合法代码被判 block」类误报,且现在带写否决权。
    • 最小可行修复: 对 .json 默认走 ts.parseConfigFileTextToJson 容忍路径(仅在真正无法解析时拦截),把严格 JSON 限定为确需严格的少数路径(如 package.json);或至少把 turbo/biome/deno/.vscode/* 纳入白名单并在注释里写明白名单的维护口径。
  3. [低] dryRun 预览被写盘前 veto 吞掉

    • 证据: apply-patch.ts:74 刻意只在 !dryRun 时因语法失败返回失败(dry-run 仍返回 diff + validation 供预览);但 executor.ts:645 起的 validateWriteBeforeExecution 不区分 dryRun,语法不合格的 dry-run 在 callTool 之前就被判 step 失败,工具的预览语义在 executor 路径下不可达。
    • 受影响调用方/流程: 任何用 dryRun: true 做补丁预览的步骤。
    • 最小可行修复: preflight 中对 toolParams.dryRun === true 只记录不阻断,或显式说明 executor 不支持失败预览。
  4. [低] mcp-file 为一个纯函数引入整包 guard 依赖

    • 证据: packages/mcp-file/src/syntax-validation.ts:1@frontagent/hallucination-guard 包根导入 validateSourceSyntax,而包根 (index.ts) 会连带 guard.js@frontagent/sdd 与全部 checks 进入文件工具/独立 server 的依赖图。
    • 最小可行修复: 为 guard 增加 ./syntax 子路径导出,或把纯解析器下沉到 @frontagent/shared,让 mcp-file 只依赖它需要的部分。

行级发现

  • [packages/core/src/executor/executor.ts:456] wasDeferredSameTargetWrite 把 PhaseRunner 写入 step.params 的粘性标记当成「内容已变」的充分条件;标记不会被清除且会随会话快照持久化,导致前置写入未真正改动文件、或 resume 之后,该 apply_patch 步骤仍被永久判为 stale。建议改为 runner 本地状态并按真实内容比对拦截。
  • [packages/hallucination-guard/src/checks/syntax-validity.ts:227] JSONC 白名单漏掉了生态中常见的注释型配置(turbo.json、biome.json、deno.json、.vscode/keybindings.json),这些文件将以 severity: 'block' 被写盘前否决;建议 .json 默认走容忍解析、仅对少数路径保持严格 JSON。
  • [packages/core/src/executor/executor.ts:645] preflight 未对 dryRun 做例外,使 apply-patch.ts:74 刻意保留的 dry-run 预览语义在 executor 路径下失效;建议 dry-run 只记录校验结果而不阻断调用。

Karpathy 评审

  • 假设: 关键假设已被显式绑定——read_file 返回未截断的原文,因此 executor 的 sha256 与工具侧磁盘读取一致(已核对 read-file.ts,无 startLine/endLine 时为原文);内部参数只在 in-process 适配器保留(internal-args.ts + runtime-node/mcp-clients.ts)。未言明的假设是「同目标写入必须串行且延后即视为失效」,这一条被编码成粘性 params 标记而非显式调度状态(见问题 1)。
  • 简洁性: 主体是减复杂度的:手写括号/引号扫描被解析器替换,patch 投影收敛为 @frontagent/shared/file-patches.ts 单一实现,detectLanguage 变为共享实现别名,apply-patch.ts 从 275 行删到 57 行净减。反向的复杂度只有 JSONC 白名单这一处 ad-hoc 特例表。
  • 结构质量: 分层合理——纯投影在 shared,解析在 guard,写盘前后分别由 validateSyntax / validateImports 承担,enabledChecks 被贯通。扣分项:包根导入把 guard 全量拖进 mcp-file(问题 4);调度状态写进 step.params 破坏了 runner 与 step 数据的边界(问题 1)。
  • 变更范围: 与 #413/#387 目标一致,report.mjs 与 benchmark 文案的改动可追溯到新事件分层;无明显无关重构。
  • 验证: 覆盖度好——撇号/多行模板/JSX/正则、TSX vs TS vs JSX 判别、fence、hash 绑定与 stale 拒绝、快照不被创建、wx 独占创建竞态、dry-run 不污染上下文、并发同目标串行、桌面端三阶段标签、report 分类计数均有针对性用例。

缺失覆盖

  • 缺少「被延后的 apply_patch 在前置同目标步骤失败/文件未变」以及「携带 __frontagentDeferredSameTargetWrite 的 plan 经 resume 再执行」的用例——这两条正是问题 1 的失败路径。
  • 缺少非白名单 JSONC(turbo.json/biome.json 带注释)经 apply_patch 修改的用例;当前测试反向锁定了 turbo.json 必须失败,会把误拦截固化成契约。
  • apps/vscode/src/package.test.ts:52-53dist/extension.cjs 不存在时直接 return,而 quality:ci 的顺序是 test 先于 build:verify;本 PR 让 typescript 成为 guard 的运行时依赖并依赖「VSIX 内联打包」这一不变量,该断言在标准门禁下实际为空跑。建议改为断言 esbuild 配置的 externals,或在该测试前保证已构建。
  • 缺少 dry-run 预览路径的 executor 级用例(问题 3)。

);
{
const cachedContent = context.collectedContext.files.get(path);
const wasDeferredSameTargetWrite = step.params.__frontagentDeferredSameTargetWrite === true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wasDeferredSameTargetWrite 把 PhaseRunner 写入 step.params 的粘性标记当成「内容已变」的充分条件;标记不会被清除且会随会话快照持久化,导致前置写入未真正改动文件、或 resume 之后,该 apply_patch 步骤仍被永久判为 stale。建议改为 runner 本地状态并按真实内容比对拦截。

}
}

function isKnownJsoncConfigPath(filePath: string): boolean {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSONC 白名单漏掉了生态中常见的注释型配置(turbo.json、biome.json、deno.json、.vscode/keybindings.json),这些文件将以 severity: 'block' 被写盘前否决;建议 .json 默认走容忍解析、仅对少数路径保持严格 JSON。

}
| undefined
> {
if (step.action !== 'create_file' && step.action !== 'apply_patch') return undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preflight 未对 dryRun 做例外,使 apply-patch.ts:74 刻意保留的 dry-run 预览语义在 executor 路径下失效;建议 dry-run 只记录校验结果而不阻断调用。

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ ceilf6/repo-guard

代码评审报告: Fix/parser-backed prewrite validation (#413)

风险等级:
处理建议: 请求修改
决策摘要: 方向正确、测试扎实,但新的 JSON 分类逻辑把「除白名单外的所有 .json」交给 tsconfig 解析器,会把顶层数组/标量的合法 JSON 判为语法错误——而这个判定现在拥有写盘否决权,属于 #413 要消灭的同一类误拦;另有一处永不清除的调度标志会污染持久化的 step.params。

级联分析

  • 变更符号: checkSyntaxValidity / 新增 validateSourceSyntaxHallucinationGuard.validateSyntax / validateImports / isCheckEnabledExecutor.executeStep(新增 validateWriteBeforeExecution)、Executor.validatePreExecution / validateAfterExecutionPhaseRunner.selectConflictFreeStepsapplyPatchcreateFile、新增 applyFilePatches / detectSyntaxLanguage / SnapshotManager.discardSnapshot
  • 受影响流程: agent execute / planOnly 的每一次 create_fileapply_patch 写盘;apply_patch 现在每步额外做一次 read_file;guard validateCode 在执行器路径上已被拆分替代;desktop 执行日志 reducer;benchmark 事件统计(validation_failed:pre_write:<check> 键,报告与 run-eval 已对齐,scripts/tests/benchmark-report.test.mjs 覆盖)。
  • 变更集外调用方: 未发现未同步的调用方。detectLanguage 已改为复用 detectSyntaxLanguagephase-ordering.ts:144),无重复实现;validateCode 保留为公共 API,执行器不再调用;packages/mcp-file@frontagent/hallucination-guard 新依赖边不构成循环;typescript 的运行时依赖已在根 package.jsonbuild.mjs external 列表、VSIX 打包注释中处理一致。
  • 置信度: medium(在 checkout 中用 Read/Grep 逐一核对了被改符号的调用方与打包配置,关联 issue 上下文完整;但 PR diff 被截断 24 个文件,且本环境未安装依赖,无法执行测试或实际跑 TypeScript 解析器验证 JSON 判定)。

问题发现

  1. [高] .json 默认走 JSONC 解析器,顶层数组/标量的合法 JSON 会被写盘前否决

    • 证据: syntax-validity.ts:253 的兜底 return baseName.endsWith('.json') 让所有不在 isStrictJsonPath 硬编码清单里的 .json 进入 ts.parseConfigFileTextToJson:195)。该 API 对非对象根会写入 The root value of a 'tsconfig.json' file must be an object 诊断并返回 error,于是 [{"id":1}]"str"123 这类合法 JSON 被判为语法错误。链路上这已是硬否决:create-file.ts:67-72 直接拒绝写盘,executor.ts:199-213callTool 前终止 step。证据等级 (inferred):依据 TypeScript API 契约推断,本环境未安装 typescript,未实际执行验证。
    • 受影响调用方/流程: 任何 agent 写 mock 数据 / 列表型 JSON(items.jsonmock.json、i18n 数组文件等)的 create_fileapply_patch;外部 MCP 客户端因内部参数被 stripInternalArgs 剥离,无法绕过。这正是 #413 所警告的「不可靠的检查不应拥有写否决权」。
    • 最小可行修复: 在 checkJsonSyntax 里先尝试 JSON.parse,成功即通过;失败后再按 isKnownJsoncConfigPath 决定是否用 JSONC 宽松解析。这既保留本分支刻意放宽的 JSONC 支持,又完全消除数组/标量根误拦,同时让 package.json 等严格路径行为不变。并补一条数组根 JSON 的回归用例。
    • 附带(同一根因,无需单列): 诊断文案对 biome.json 等文件也会写成 "tsconfig.json";反方向的代价是 en.jsonmanifest.json 这类严格消费的 JSON 现在允许注释与尾逗号并静默通过,运行时才 JSON.parse 失败——修复方案同上即可收敛。
  2. [中] __frontagentDeferredSameTargetWrite 写入 step.params 后永不清除,会随会话快照持久化

    • 证据: phase-runner.ts:233 把调度状态写进 step.paramsexecutor.ts:456 读取它并在存在显式 patches 时无条件拦截。没有任何位置删除该键。agent.ts:649-654 在 resume 时保留 resume.plan.steps 的全部 params,仅把非 completed 步骤重置为 pending。
    • 受影响调用方/流程: 被同目标去重推迟并失败的 apply_patch 步骤,在 resume 后即使文件与上下文完全一致也会再次被判 stale_patch_context,即永久失败;该键还会随 {...step.params} 进入 callTool 参数(进程内适配器不剥离内部键),把调度状态泄漏到工具边界。
    • 最小可行修复: 读取即消费——在 executor.ts:456 取值后 delete step.params.__frontagentDeferredSameTargetWrite;或把推迟标记放进 PhaseRunner 内的运行期 WeakSet<ExecutionStep>,不落到 step.params
  3. [中] 写盘前校验块把 executor.ts 从约 788 行推过 1000 行(现 1017 行)

    • 证据: executor.ts:632-760 新增 validateWriteBeforeExecution / buildWriteValidationFailure / isSuccessfulToolResult,加上 :296-313refreshCollectedFileContextcrypto 哈希绑定,共约 150 行独立职责代码;本 PR 净增 +249 行。
    • 受影响调用方/流程: 该目录已有 phase-runner.tsphase-ordering.tstool-call-handler.ts 的拆分惯例,Executor 又是 18 个直接上游依赖的关键骨架,文件继续膨胀会直接抬高后续改动的评审与验证成本。
    • 最小可行修复: 把这组函数整体移到 executor/write-preflight.ts(纯函数 + 注入 guard),Executor 只保留一次调用与结果分支,符合同目录既有模式,不改变任何行为。
  4. [低] overwrite: true 但目标当时不存在时使用 wx,竞态下返回失败而非按调用方意图覆盖

    • 证据: create-file.ts:98-101 仅在 overwrite && existedBeforeWrite 时走普通写;overwrite: true 且检查时不存在则用 flag: 'wx',EEXIST 会被 :112-126 归为 "File appeared concurrently" 失败。悬空符号链接同样触发(existsSync 为 false 但 wx 报 EEXIST)。
    • 最小可行修复: lostExclusiveRace && overwrite === true 时改为普通写重试一次,仅对 overwrite: false 保留硬失败。

行级发现

  • [packages/hallucination-guard/src/checks/syntax-validity.ts:253] 兜底把所有 .json 归为 JSONC,使顶层数组/标量的合法 JSON 经 parseConfigFileTextToJson 被判语法错误并否决写盘;建议先 JSON.parse 成功即放行,失败再按路径决定是否宽松解析。
  • [packages/core/src/executor/executor.ts:456] 该推迟标志由 PhaseRunner 写入 step.params 且从不清除,会随会话快照持久化,使 resume 后的显式 patch 步骤永久被判 stale;读取后立即 delete,或改为 PhaseRunner 内的运行期 WeakSet。
  • [packages/core/src/executor/executor.ts:632] 写盘前校验整块逻辑使本文件越过 1000 行;按同目录 phase-runner.ts / tool-call-handler.ts 的既有模式抽到 executor/write-preflight.ts,行为不变。
  • [packages/mcp-file/src/tools/create-file.ts:101] overwrite: true 且检查时文件不存在也走 wx,竞态或悬空符号链接会返回 "appeared concurrently" 而非按调用方意图覆盖;EEXIST 且 overwrite === true 时应普通写重试。

Karpathy 评审

  • 假设: 哈希绑定隐含「read_file 返回逐字节原文」这一未文档化契约(read-file.ts:88-124 当前成立);若日后 read_file 加行号或截断,所有 patch 都会退化为 stale_original_hash,值得在 applyPatch 的哈希比较处留一行契约注释。另一处隐含假设是「.json 多半是配置文件」,即上面的发现 1。
  • 简洁性: 共享投影器 applyFilePatches 是本 PR 最好的部分——删除了 apply-patch 里 275 行重复的边界/重叠/括号启发式,且用 detectSyntaxLanguage 统一了语言判定。executor 与工具各自再解析一次原文与投影(每次 patch 共 4 次解析)属于对外部 MCP 调用方的合理纵深防御,不算冗余。file-patches.ts:26/31content !== undefined 分支在 bounds 校验之后已不可达,属可清理的小冗余。
  • 结构质量: 主要退化是发现 3(文件越界)与发现 2(调度状态放进数据模型 step.params)。isStrictJsonPath 的硬编码 basename 清单是共享路径上的 ad-hoc 特例,会持续漂移,发现 1 的修复方案可同时缓解它。
  • 变更范围: 基本聚焦。benchmark 报告与 report.mjs 重写由新的分阶段事件键驱动,可接受;pnpm-lock.yaml 中 electron-builder 的 peer 解析串重排与本变更无关,属重解析噪音。
  • 验证: 焦点测试质量高——写盘前拦截不调用 callTool、快照不创建、stale hash 拒绝、dry-run 不污染上下文、同目标写序列化、enabledChecks 贯通、TSX/JSX 与 .ts 的区分都有断言,方向与 #387/#413 的验收标准一致。缺口集中在下列场景。

缺失覆盖

  • 顶层为数组/字符串/数字的合法 .json 内容(create_file 与 executor preflight 两侧),当前 JSONC 用例全部是对象根。
  • .json 内容:JSONC 路径判通过、JSON.parse 判失败,两条路径行为不一致。
  • 会话 resume 场景:携带 __frontagentDeferredSameTargetWrite 的 step 重跑仍被拦截。
  • 非法 operation(如 LLM 生成 "update"):applyFilePatches 的 switch 静默落空,投影等于原文,最终以 success 返回但文件未变——「校验过的投影」在此处会给出假成功。
  • 严格 JSON 消费方(i18n/locale、manifest.json 等)写入带尾逗号内容时被放行的回归用例,用于固定放宽策略的边界。

return true;
}

return baseName.endsWith('.json');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

兜底把所有 .json 归为 JSONC,使顶层数组/标量的合法 JSON 经 parseConfigFileTextToJson 被判语法错误并否决写盘;建议先 JSON.parse 成功即放行,失败再按路径决定是否宽松解析。

);
{
const cachedContent = context.collectedContext.files.get(path);
const wasDeferredSameTargetWrite = step.params.__frontagentDeferredSameTargetWrite === true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

该推迟标志由 PhaseRunner 写入 step.params 且从不清除,会随会话快照持久化,使 resume 后的显式 patch 步骤永久被判 stale;读取后立即 delete,或改为 PhaseRunner 内的运行期 WeakSet。

};
}

private async validateWriteBeforeExecution(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

写盘前校验整块逻辑使本文件越过 1000 行;按同目录 phase-runner.ts / tool-call-handler.ts 的既有模式抽到 executor/write-preflight.ts,行为不变。

if (overwrite && existedBeforeWrite) {
writeFileSync(safePath.fullPath, content, 'utf-8');
} else {
writeFileSync(safePath.fullPath, content, { encoding: 'utf-8', flag: 'wx' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overwrite: true 且检查时文件不存在也走 wx,竞态或悬空符号链接会返回 "appeared concurrently" 而非按调用方意图覆盖;EEXIST 且 overwrite === true 时应普通写重试。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] syntax_validity flags legitimate code as block — apostrophes, template literals, JSX text

1 participant