Skip to content

Commit 01394b5

Browse files
committed
fix: exit 127 恢复为 CLI 失败(按场景分歧取代误统一)
CI 实证:test_mcp_server 的失败日志/安静模式用例依赖 check 面在工具缺失 (exit 127)时报失败这一环境无关性(runner 无 ruff,靠 127=失败才进得了 失败路径)——v0.8.1 首版把 127 并入 unverified 导致 PR#28 CI 红。 - run_per_language:仅 exit 2 归 unverified;127 恢复为失败(含设计注释) - 交互钩子路径对 127 仍 skipped:按场景的有意分歧(健康面红、工作面不挡人), UnverifiedParityTests 反转锁住该分歧(127 失败 / 2 unverified) - README 双语 exit-127 句改为按场景分流表述 - OpenSpec 2026-09-22-restore-cli-127-strictness(skip_specs)归档, 记录对 push-face-delta 归档文案的取代 验证:unittest 85、run_all 141/0、test_mcp_server 5/5、openspec 12/12、lint 全绿
1 parent 94a9d81 commit 01394b5

7 files changed

Lines changed: 81 additions & 22 deletions

File tree

‎README.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,9 +250,12 @@ Gate scanning is scoped to **what you are about to change**, decided by git stat
250250
`exclude` customize language detection. Full scans always skip vendor/build
251251
snapshots (immutable supply-chain content).
252252

253-
Verdict honesty: a linter crashing with exit 2 (usage/dependency failure) or
254-
exit 127 (command missing) is reported as **unverified**, never as a lint failure
255-
— "cannot verify" is not "verified bad". A tool crash never triggers auto-fix.
253+
Verdict honesty: a linter crashing with exit 2 (usage/dependency failure) is
254+
reported as **unverified**, never as a lint failure — "cannot verify" is not
255+
"verified bad", and a tool crash never triggers auto-fix. Exit 127 (command
256+
missing) is split by context on purpose: interactive hooks skip it (never block
257+
a person for missing tooling), while `check`/CI treats it as **failure** — a
258+
health surface must go red when tooling is absent.
256259

257260
Escape hatch: `git config codeguard.skipGate true` bypasses both the soft and
258261
hard gate for one repository; every bypass is counted and surfaced by the Stop

‎README.zh-CN.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,9 +233,10 @@ codeguard:
233233
`extensions` 与 `exclude` 自定义语言识别。全仓扫描始终剔除 vendor/build
234234
快照(供应链不可变内容)。
235235

236-
判定诚实性:linter 以 exit 2(用法/依赖崩溃)或 exit 127(命令不存在)退出时
237-
按**未验证**上报,绝不计为 lint 失败——"无法验证"不等于"验证失败";工具崩溃
238-
也不会触发自动修复。
236+
判定诚实性:linter 以 exit 2(用法/依赖崩溃)退出时按**未验证**上报,绝不计
237+
为 lint 失败——"无法验证"不等于"验证失败",工具崩溃也不会触发自动修复。
238+
exit 127(命令不存在)**按场景有意分流**:交互钩子跳过它(不因工具缺失挡人);
239+
`check`/CI 面按**失败**处理——健康面在工具缺失时就该变红。
239240

240241
逃生门:`git config codeguard.skipGate true` 对单仓同时豁免软门与硬门;每次
241242
绕过都会计数并由 Stop 汇总展示,标志遗留未清时也会提醒。钩子共享状态位于
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
schema: spec-driven
2+
created: 2026-09-22
3+
skip_specs: true
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
## Why
2+
3+
`2026-09-22-push-face-delta` 的 proposal/design 文案把 exit 127 与 exit 2 一并写成"同归 unverified"。落地后 CI 立刻红了:`test_mcp_server` 的失败日志与安静模式用例依赖 **CLI 在工具缺失(127)时报失败**这一环境无关性(CI runner 无 ruff,靠 127=失败才能可靠进入失败路径)。结论:127 的 CLI/hook 分叉是**按场景的有意分歧**,不是口径 bug——交互钩子跳过(不挡人),健康面必须红。该行为已恢复,此处仅记录对归档文案的取代;spec 层无变更(主 spec 的 unverified 要求只写了 exit 2,未涉及 127)。
4+
5+
## What Changes
6+
7+
- 恢复 `run_per_language` 的 exit 127 = 失败(rc 2 的 unverified 保持,0.8.0 起 CI 已验证)。
8+
- 回归测试反转为锁住该分歧(`UnverifiedParityTests`:CLI 127 失败 / CLI 2 unverified)。
9+
- README 双语的 exit-127 句同步改为按场景分流的表述。
10+
- 本文档取代 `2026-09-22-push-face-delta` 归档 proposal/design 中关于 127 的那段陈述。
11+
12+
## Capabilities
13+
14+
### New Capabilities
15+
16+
None. Infrastructure-only correction; `skip_specs: true`.
17+
18+
### Modified Capabilities
19+
20+
None.
21+
22+
## Impact
23+
24+
`scripts/run_per_language.py`、`tests/test_hardening_fixes.py`、README 双语;不影响退出码总契约(PostToolUse 恒 0、PreToolUse 0/2 未动)与 JSON schema。
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- [x] 1.1 恢复 run_per_language rc127=失败(rc2 unverified 保留)
2+
- [x] 1.2 UnverifiedParityTests 反转锁住有意分歧
3+
- [x] 1.3 README 双语 127 句按场景分流
4+
- [x] 1.4 `openspec validate --strict` 通过并归档

‎scripts/run_per_language.py‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,19 @@ def run_check(languages: list[str], project_root: Path,
5757
# 否则规则集随机器漂移、供应链快照给出不可修的永久红。
5858
lint_cmd = scope_cmd(lint_cmd, project_root, full_excludes=True)
5959
rc, out, err = _run(lint_cmd, cwd=project_root, timeout=timeout)
60-
if rc in (2, 127):
61-
# exit 2 = 用法/依赖/配置崩溃;127 = 命令不存在——都与仓库内容无关,
62-
# 与钩子路径同归「无法验证 ≠ 验证失败」(此前 CLI 把 127 报成失败、
63-
# 与 hook skipped 口径分叉)。不触发 fix、不写失败日志。
64-
reason = (
65-
"exit 2(工具链异常,非 lint 结论)" if rc == 2
66-
else "命令不存在(exit 127)"
67-
)
60+
if rc == 2:
61+
# exit 2 = 用法/依赖/配置崩溃,与仓库内容无关 → unverified
62+
# (不计失败、不触发 fix、不写失败日志)。
63+
# exit 127(命令不存在)**故意保持失败**:check CLI 是 CI/健康面,
64+
# "工具没装"在那里就该红——test_mcp_server 的失败日志与安静模式用例
65+
# 依赖这条环境无关性(CI 无 ruff,靠 127=失败才进得了失败路径)。
66+
# 交互钩子路径对 127 归 skipped(不挡人工作):按场景的有意分歧,
67+
# 不是口径 bug(v0.8.1 曾误统一过一次,本提交修正)。
6868
results.append({
6969
"language": lang,
7070
"passed": True,
71-
"exit_code": rc,
72-
"unverified": reason,
71+
"exit_code": 2,
72+
"unverified": "exit 2(工具链异常,非 lint 结论)",
7373
"stderr_tail": err[-2000:] if err else "",
7474
"stdout_tail": out[-1000:] if out else "",
7575
})

‎tests/test_hardening_fixes.py‎

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -435,24 +435,48 @@ def test_ups_uses_push_face_for_push_intent(self) -> None:
435435

436436

437437
class UnverifiedParityTests(unittest.TestCase):
438-
"""CLI exit 127 与钩子 skipped 口径对齐(此前 CLI 报失败)。"""
438+
"""rc2/127 按场景分流:rc2 = unverified(两处一致);rc127 = 故意分歧。
439439
440-
def test_rc_127_is_unverified_in_run_per_language(self) -> None:
440+
check CLI 是 CI/健康面——工具缺失必须红(环境无关性由 test_mcp_server
441+
的失败日志/安静模式用例依赖);交互钩子对 127 归 skipped 不挡工作。
442+
这是有意设计,本测试锁住它,防止再被"统一"。
443+
"""
444+
445+
@staticmethod
446+
def _fake_run(returncode: int):
441447
import run_per_language as rpl
442448

443449
class _Fake:
444-
returncode = 127
445-
stdout = ""
446-
stderr = "command not found"
450+
pass
447451

452+
_Fake.returncode = returncode
453+
_Fake.stdout = ""
454+
_Fake.stderr = "boom" if returncode != 0 else ""
448455
original = rpl.subprocess.run
449456
rpl.subprocess.run = lambda *a, **k: _Fake()
457+
return original
458+
459+
def test_rc_127_is_failure_in_cli(self) -> None:
460+
import run_per_language as rpl
461+
462+
original = self._fake_run(127)
450463
try:
451464
results = rpl.run_check(["python"], Path(tempfile.mkdtemp(prefix="cg-127-")))
452465
finally:
453466
rpl.subprocess.run = original
467+
self.assertFalse(results[0]["passed"], "CLI 面工具缺失必须报失败")
468+
self.assertNotIn("unverified", results[0])
469+
470+
def test_rc_2_is_unverified_in_cli(self) -> None:
471+
import run_per_language as rpl
472+
473+
original = self._fake_run(2)
474+
try:
475+
results = rpl.run_check(["python"], Path(tempfile.mkdtemp(prefix="cg-2-")))
476+
finally:
477+
rpl.subprocess.run = original
454478
self.assertTrue(results[0]["passed"])
455-
self.assertIn("exit 127", results[0].get("unverified", ""))
479+
self.assertIn("exit 2", results[0].get("unverified", ""))
456480

457481

458482
class FourHookDedupTests(unittest.TestCase):

0 commit comments

Comments
 (0)