From f69792104fcaa60a638b42d453b05e2869b87d9c Mon Sep 17 00:00:00 2001 From: Yue Ma Date: Wed, 9 Sep 2026 21:51:14 -0400 Subject: [PATCH 1/2] fix(standards): end the whole process tree when a tool times out subprocess terminates the child it started and leaves that child's own children running. Under an explicit command an operator would notice an orphaned tree; under a hook, which is where this is heading, nothing would. The recorded debt from the previous step is paid before anything runs unattended. Each tool now starts in its own session, so a timeout can end the group rather than one process. end_group sends SIGTERM, waits a bounded grace period, then SIGKILL, and treats an already-gone group as done rather than as an error. The test builds the case that actually matters: a stub spawns a background grandchild that would outlive it, records its pid, and sleeps past the timeout. After execute returns, that pid is gone. A second stub traps SIGTERM and is still ended, within the grace budget instead of hanging. A third confirms an ordinary tool is unaffected by running in its own session. Mutation check: restoring the previous behaviour of terminating only the child fails the grandchild test. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016cCs5DdUjLDkP8V3Z5x6XH --- .../structured-coding/scripts/standards.py | 48 +++++++++++++-- .../structured-coding/scripts/standards.py | 48 +++++++++++++-- dist/structured-coding-claude-code.zip | Bin 110687 -> 111061 bytes dist/structured-coding-codex.zip | Bin 111031 -> 111405 bytes scripts/test_standards.py | 55 ++++++++++++++++++ structured-coding/scripts/standards.py | 48 +++++++++++++-- 6 files changed, 181 insertions(+), 18 deletions(-) diff --git a/dist/claude-code/structured-coding/scripts/standards.py b/dist/claude-code/structured-coding/scripts/standards.py index 62128de..e13183b 100644 --- a/dist/claude-code/structured-coding/scripts/standards.py +++ b/dist/claude-code/structured-coding/scripts/standards.py @@ -10,6 +10,7 @@ import json import os import re +import signal import subprocess import time import sys @@ -470,26 +471,61 @@ def bounded_output(raw): return text[:MAX_OUTPUT].strip() + "\n[output truncated]" +GRACE_SECONDS = 5 + + +def end_group(process): + """A tool's children must not outlive the event that started it. + + subprocess terminates only the child it started. Under an explicit command an + operator would notice an orphaned tree; under a hook nothing would.""" + for number in (signal.SIGTERM, signal.SIGKILL): + try: + os.killpg(process.pid, number) + except (OSError, ProcessLookupError): + return + try: + process.wait(timeout=GRACE_SECONDS) + if number is signal.SIGKILL: + return + except subprocess.TimeoutExpired: + continue + else: + # The leader is gone; make sure nothing it spawned is still holding on. + try: + os.killpg(process.pid, 0) + except (OSError, ProcessLookupError): + return + + def execute(root, tool, command, seconds): """Run one tool and classify from what happened, not from the exit code alone.""" if seconds <= 0: return "INCONCLUSIVE", "the total time budget was already spent", "" started = time.monotonic() try: - result = subprocess.run( + # Its own session, so a timeout can end the whole tree and not just the leader. + process = subprocess.Popen( command, cwd=str(root), stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, timeout=seconds, + stderr=subprocess.STDOUT, start_new_session=True, ) except FileNotFoundError: # Never a pass: an absent tool established nothing. return "INCONCLUSIVE", f"{command[0]} is not installed or not on PATH", "" - except subprocess.TimeoutExpired: - return "INCONCLUSIVE", f"no result within {seconds:.0f}s", "" except OSError as error: return "INCONCLUSIVE", f"could not start ({type(error).__name__})", "" + try: + captured, _ = process.communicate(timeout=seconds) + except subprocess.TimeoutExpired: + end_group(process) + try: + process.communicate(timeout=GRACE_SECONDS) + except (subprocess.TimeoutExpired, OSError, ValueError): + pass + return "INCONCLUSIVE", f"no result within {seconds:.0f}s", "" elapsed = time.monotonic() - started - output = bounded_output(result.stdout or b"") - code = result.returncode + output = bounded_output(captured or b"") + code = process.returncode if code == 0: return "PASS", f"clean in {elapsed:.1f}s", output if code < 0: diff --git a/dist/codex/structured-coding/scripts/standards.py b/dist/codex/structured-coding/scripts/standards.py index 62128de..e13183b 100644 --- a/dist/codex/structured-coding/scripts/standards.py +++ b/dist/codex/structured-coding/scripts/standards.py @@ -10,6 +10,7 @@ import json import os import re +import signal import subprocess import time import sys @@ -470,26 +471,61 @@ def bounded_output(raw): return text[:MAX_OUTPUT].strip() + "\n[output truncated]" +GRACE_SECONDS = 5 + + +def end_group(process): + """A tool's children must not outlive the event that started it. + + subprocess terminates only the child it started. Under an explicit command an + operator would notice an orphaned tree; under a hook nothing would.""" + for number in (signal.SIGTERM, signal.SIGKILL): + try: + os.killpg(process.pid, number) + except (OSError, ProcessLookupError): + return + try: + process.wait(timeout=GRACE_SECONDS) + if number is signal.SIGKILL: + return + except subprocess.TimeoutExpired: + continue + else: + # The leader is gone; make sure nothing it spawned is still holding on. + try: + os.killpg(process.pid, 0) + except (OSError, ProcessLookupError): + return + + def execute(root, tool, command, seconds): """Run one tool and classify from what happened, not from the exit code alone.""" if seconds <= 0: return "INCONCLUSIVE", "the total time budget was already spent", "" started = time.monotonic() try: - result = subprocess.run( + # Its own session, so a timeout can end the whole tree and not just the leader. + process = subprocess.Popen( command, cwd=str(root), stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, timeout=seconds, + stderr=subprocess.STDOUT, start_new_session=True, ) except FileNotFoundError: # Never a pass: an absent tool established nothing. return "INCONCLUSIVE", f"{command[0]} is not installed or not on PATH", "" - except subprocess.TimeoutExpired: - return "INCONCLUSIVE", f"no result within {seconds:.0f}s", "" except OSError as error: return "INCONCLUSIVE", f"could not start ({type(error).__name__})", "" + try: + captured, _ = process.communicate(timeout=seconds) + except subprocess.TimeoutExpired: + end_group(process) + try: + process.communicate(timeout=GRACE_SECONDS) + except (subprocess.TimeoutExpired, OSError, ValueError): + pass + return "INCONCLUSIVE", f"no result within {seconds:.0f}s", "" elapsed = time.monotonic() - started - output = bounded_output(result.stdout or b"") - code = result.returncode + output = bounded_output(captured or b"") + code = process.returncode if code == 0: return "PASS", f"clean in {elapsed:.1f}s", output if code < 0: diff --git a/dist/structured-coding-claude-code.zip b/dist/structured-coding-claude-code.zip index a808e2b422cb043cb1bb315f5cd5079d950e8973..849a2819be5f8681c665e211dca7328afabd6786 100644 GIT binary patch delta 8320 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 K27amm00020>JXa% delta 7953 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 H0RR91%Ai9L diff --git a/dist/structured-coding-codex.zip b/dist/structured-coding-codex.zip index 5a3769653b75a11c8b28eb94a554cd7600aaa5f1..4c1ac12daaafe84fc03a53916335d899d04bbb19 100644 GIT binary patch delta 8324 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 OjIjYF2862t00014gAnro delta 7947 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 BLC*jH diff --git a/scripts/test_standards.py b/scripts/test_standards.py index eca51b3..ed0a710 100644 --- a/scripts/test_standards.py +++ b/scripts/test_standards.py @@ -11,6 +11,7 @@ import subprocess import sys import tempfile +import time import unittest from pathlib import Path from unittest.mock import patch @@ -887,5 +888,59 @@ def test_without_a_base_anywhere_run_reports_the_ordinary_reason(self): self.assertIn("--base is required", result.stderr) +class ProcessGroupTests(WorktreeTest): + """A timed-out tool must not leave a tree running behind it.""" + + def alive(self, pid, seconds=5): + end = time.monotonic() + seconds + while time.monotonic() < end: + try: + os.kill(pid, 0) + except (OSError, ProcessLookupError): + return False + time.sleep(0.1) + return True + + def test_a_timeout_ends_the_whole_tree_not_only_the_child(self): + pidfile = self.root / "grandchild.pid" + stub = self.root / "spawner" + stub.write_text( + "#!/bin/sh\n" + "sh -c 'sleep 300' &\n" + f"echo $! > '{pidfile}'\n" + "sleep 300\n" + ) + stub.chmod(0o755) + outcome, reason, _ = standards.execute( + self.project, {"name": "spawner"}, [str(stub)], 2 + ) + self.assertEqual(outcome, "INCONCLUSIVE") + self.assertIn("no result within", reason) + pid = int(pidfile.read_text().strip()) + self.assertFalse(self.alive(pid), "a grandchild outlived the timed-out tool") + + def test_a_tool_that_ignores_termination_is_still_ended(self): + stub = self.root / "stubborn" + stub.write_text("#!/bin/sh\ntrap '' TERM\nsleep 300\n") + stub.chmod(0o755) + started = time.monotonic() + outcome, _, _ = standards.execute( + self.project, {"name": "stubborn"}, [str(stub)], 2 + ) + self.assertEqual(outcome, "INCONCLUSIVE") + # Terminated, then killed after the grace period, rather than hanging. + self.assertLess(time.monotonic() - started, 2 + standards.GRACE_SECONDS * 2 + 5) + + def test_an_ordinary_tool_is_unaffected_by_the_new_session(self): + stub = self.root / "quick" + stub.write_text("#!/bin/sh\necho hello\nexit 1\n") + stub.chmod(0o755) + outcome, _, output = standards.execute( + self.project, {"name": "quick"}, [str(stub)], 30 + ) + self.assertEqual(outcome, "FAIL") + self.assertIn("hello", output) + + if __name__ == "__main__": unittest.main() diff --git a/structured-coding/scripts/standards.py b/structured-coding/scripts/standards.py index 62128de..e13183b 100644 --- a/structured-coding/scripts/standards.py +++ b/structured-coding/scripts/standards.py @@ -10,6 +10,7 @@ import json import os import re +import signal import subprocess import time import sys @@ -470,26 +471,61 @@ def bounded_output(raw): return text[:MAX_OUTPUT].strip() + "\n[output truncated]" +GRACE_SECONDS = 5 + + +def end_group(process): + """A tool's children must not outlive the event that started it. + + subprocess terminates only the child it started. Under an explicit command an + operator would notice an orphaned tree; under a hook nothing would.""" + for number in (signal.SIGTERM, signal.SIGKILL): + try: + os.killpg(process.pid, number) + except (OSError, ProcessLookupError): + return + try: + process.wait(timeout=GRACE_SECONDS) + if number is signal.SIGKILL: + return + except subprocess.TimeoutExpired: + continue + else: + # The leader is gone; make sure nothing it spawned is still holding on. + try: + os.killpg(process.pid, 0) + except (OSError, ProcessLookupError): + return + + def execute(root, tool, command, seconds): """Run one tool and classify from what happened, not from the exit code alone.""" if seconds <= 0: return "INCONCLUSIVE", "the total time budget was already spent", "" started = time.monotonic() try: - result = subprocess.run( + # Its own session, so a timeout can end the whole tree and not just the leader. + process = subprocess.Popen( command, cwd=str(root), stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, timeout=seconds, + stderr=subprocess.STDOUT, start_new_session=True, ) except FileNotFoundError: # Never a pass: an absent tool established nothing. return "INCONCLUSIVE", f"{command[0]} is not installed or not on PATH", "" - except subprocess.TimeoutExpired: - return "INCONCLUSIVE", f"no result within {seconds:.0f}s", "" except OSError as error: return "INCONCLUSIVE", f"could not start ({type(error).__name__})", "" + try: + captured, _ = process.communicate(timeout=seconds) + except subprocess.TimeoutExpired: + end_group(process) + try: + process.communicate(timeout=GRACE_SECONDS) + except (subprocess.TimeoutExpired, OSError, ValueError): + pass + return "INCONCLUSIVE", f"no result within {seconds:.0f}s", "" elapsed = time.monotonic() - started - output = bounded_output(result.stdout or b"") - code = result.returncode + output = bounded_output(captured or b"") + code = process.returncode if code == 0: return "PASS", f"clean in {elapsed:.1f}s", output if code < 0: From 0398977b1cd911fa88b2aebee6236621b77bd0f5 Mon Sep 17 00:00:00 2001 From: Yue Ma Date: Wed, 9 Sep 2026 22:02:42 -0400 Subject: [PATCH 2/2] feat(standards): register one hook that reports after a commit The declared checks now run without anyone remembering to ask, at the one moment a host really offers: PostToolUse after a direct git commit. The commit already exists when the report arrives, which is the simpler behaviour the operator asked for over a blocking check the agent would have to retry around. Scope was cut after the freeze, on the operator's judgement that the plan was over-engineered for what this project is. The Stop handler, the pr hook trigger and the unchanged-inputs skip are gone. That skip existed only to suppress noise from an event firing more often than the trigger it stood in for; it was a patch on a patch, and the root cause was mapping pr onto an event that does not mean pr. trigger: "pr" now has no hook, and the documentation says why rather than leaving a reader to infer it. The registered budget is 60 seconds instead of 12, because a check is slower than a notice. Both hosts were measured to honour a larger value, and Codex reports timeoutSec=60 for this hook while the others stay at 12. The host is blocked while it runs, so the documentation says to keep the commit trigger to fast checks. The handler acts only on a recognized direct git commit at the exact repository root, reuses the recognition and cwd discipline the other presets already apply, and fails open: any error returns an empty result and a diagnostic on stderr. A hook that advises must never block the host it advises. The existing coexistence test assumed exactly two presets, in its pairing, its group count and its two-way event assertion. It is generalised rather than extended, so a fourth preset would not need it rewritten again. Not observed: a PostToolUse actually firing under an authenticated turn. Registration, parsing and the budget are measured; delivery of this specific event is recorded as unverified rather than claimed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016cCs5DdUjLDkP8V3Z5x6XH --- .../structured-coding/references/platforms.md | 2 +- .../references/platforms.zh-CN.md | 2 +- .../structured-coding/references/standards.md | 28 +++- .../structured-coding/scripts/standards.py | 59 ++++++++- .../structured-coding/references/platforms.md | 2 +- .../references/platforms.zh-CN.md | 2 +- .../structured-coding/references/standards.md | 28 +++- .../structured-coding/scripts/standards.py | 59 ++++++++- dist/structured-coding-claude-code.zip | Bin 111061 -> 112452 bytes dist/structured-coding-codex.zip | Bin 111405 -> 112796 bytes scripts/hook_install.py | 18 ++- scripts/install | 4 +- scripts/test_hook_presets.py | 124 ++++++------------ scripts/test_standards.py | 73 +++++++++++ structured-coding/references/platforms.md | 2 +- .../references/platforms.zh-CN.md | 2 +- structured-coding/references/standards.md | 28 +++- structured-coding/scripts/standards.py | 59 ++++++++- translations.json | 4 +- 19 files changed, 385 insertions(+), 111 deletions(-) diff --git a/dist/claude-code/structured-coding/references/platforms.md b/dist/claude-code/structured-coding/references/platforms.md index c43e913..a14f482 100644 --- a/dist/claude-code/structured-coding/references/platforms.md +++ b/dist/claude-code/structured-coding/references/platforms.md @@ -35,7 +35,7 @@ Use `claude-code` for Claude Code. Python 3.9+, Git and macOS/Linux/WSL are requ Codex requires trust at **two** levels, and a registration is inert until both are given. First the project: until it is trusted, Codex loads no project-local hooks at all and says so on stderr, naming the file it wants the entry in. That entry must live in the `config.toml` under `CODEX_HOME`; a `-c projects."".trust_level="trusted"` override on the command line does **not** satisfy it, which is the failure that looks like hooks being delivered but ignored. Second, each hook definition is trusted by its own content hash, so changing a command asks again. Both are Codex's own mechanism; neither is granted by this installer. Claude Code has no equivalent second level. -Installation is additive. Checkpoints alone registers shared SessionStart, PreToolUse and Stop, without compact handlers. Continuity alone registers shared SessionStart and manual/automatic PreCompact. Both use five groups, with SessionStart exactly once. Removing one keeps the other usable. Bare `--remove-hooks` removes all owned presets and preserves the skill and session data. Repeating a selection is a byte-preserving no-op; duplicate/unknown names are rejected. +Installation is additive. Standards alone registers shared SessionStart and PostToolUse, and is the only preset that runs anything; its PostToolUse hook carries a 60-second budget rather than 12, because a check is slower than a notice. Checkpoints alone registers shared SessionStart, PreToolUse and Stop, without compact handlers. Continuity alone registers shared SessionStart and manual/automatic PreCompact. Both use five groups, with SessionStart exactly once. Removing one keeps the other usable. Bare `--remove-hooks` removes all owned presets and preserves the skill and session data. Repeating a selection is a byte-preserving no-op; duplicate/unknown names are rejected. One aggregate receipt owns the original settings backup. Legacy continuity receipts remain readable and migrate only on an explicit state change; check, preview and unchanged reinstall do not rewrite them. Existing customized runtime dependencies are never overwritten: compare/back up the installed skill and explicitly update it first. Missing, changed or duplicate owned groups, unknown receipt schemas, and redirected/unverifiable paths cause refusal. New installations register commands that contain no machine-specific path, so a registration committed to a shared settings file works on a teammate's machine; each machine still reviews and trusts the hooks itself. An installation made by an older release keeps its absolute commands, stays inspectable and removable, and is rewritten only by `--upgrade-registration`, which changes every command and therefore requires trusting the hooks again. `--check-hooks` says whether a registration is portable, and names the recorded interpreter only for an absolute one. diff --git a/dist/claude-code/structured-coding/references/platforms.zh-CN.md b/dist/claude-code/structured-coding/references/platforms.zh-CN.md index c06c518..8b66fc9 100644 --- a/dist/claude-code/structured-coding/references/platforms.zh-CN.md +++ b/dist/claude-code/structured-coding/references/platforms.zh-CN.md @@ -35,7 +35,7 @@ Claude Code 用户把 `codex` 换成 `claude-code`。需要 Python 3.9+、Git Codex 的信任分**两层**,两层都给到之前注册都是空转的。第一层是项目:在项目被信任之前,Codex 根本不加载任何 project-local hook,并会在 stderr 上说明,同时指出它要求写入的文件。那条记录必须写在 `CODEX_HOME` 下的 `config.toml` 里;命令行上的 `-c projects."".trust_level="trusted"` 覆盖**不满足**这一条,而这种失败看起来像是 hook 被送达却被忽略了。第二层是每个 hook 定义按自身内容 hash 单独信任,所以改动命令会重新询问。两层都是 Codex 自己的机制,installer 都不会替你授予。Claude Code 没有对应的第二层。 -安装是追加操作。单装 checkpoints 会注册共享 SessionStart、PreToolUse 和 Stop,不注册 compact handler。单装 continuity 会注册共享 SessionStart 和手动/自动 PreCompact。组合安装共五个 group,SessionStart 只出现一次。移除一个后另一个仍可用。裸 `--remove-hooks` 移除全部自有 preset,保留 skill 和 session 数据。重复安装同一选择不会改变文件字节;重复或未知名称会被拒绝。 +安装是追加操作。单装 standards 会注册共享 SessionStart 和 PostToolUse,它是唯一会真正运行东西的 preset;它的 PostToolUse hook 预算是 60 秒而不是 12 秒,因为跑一次检查比发一条提示慢。单装 checkpoints 会注册共享 SessionStart、PreToolUse 和 Stop,不注册 compact handler。单装 continuity 会注册共享 SessionStart 和手动/自动 PreCompact。组合安装共五个 group,SessionStart 只出现一次。移除一个后另一个仍可用。裸 `--remove-hooks` 移除全部自有 preset,保留 skill 和 session 数据。重复安装同一选择不会改变文件字节;重复或未知名称会被拒绝。 一份 aggregate receipt 保存原始设置备份。旧 continuity receipt 仍可读取,仅在明确改变安装状态时迁移;检查、预览和未改变选择的重装都不改写它。installer 不覆盖定制过的 runtime 依赖:先比较、备份已有 skill,再明确升级。自有 group 缺失、被修改或重复,receipt schema 未知,以及路径被重定向或无法核验,都会导致拒绝。新安装注册的命令不含任何机器相关路径,因此提交到共享设置文件里的注册在同事的机器上同样有效;每台机器仍需自己审阅并信任这些 hook。旧版本装出来的注册保留其绝对路径命令,仍可检查、仍可移除,只有 `--upgrade-registration` 会重写它;重写会改变每一条命令,因此需要重新信任这些 hook。`--check-hooks` 会说明注册是否可移植,并且只对绝对路径的注册报告记录的 interpreter。 diff --git a/dist/claude-code/structured-coding/references/standards.md b/dist/claude-code/structured-coding/references/standards.md index 1fb1620..0f2a258 100644 --- a/dist/claude-code/structured-coding/references/standards.md +++ b/dist/claude-code/structured-coding/references/standards.md @@ -1,9 +1,9 @@ # Project standards configuration -Status: **RUNS ONLY WHEN INVOKED.** This release reads the configuration and, +Status: **REPORTS; NEVER BLOCKS.** This release reads the configuration and, on the `run` command, executes the declared checks and classifies each result. -It registers no hook, so nothing happens automatically, and it blocks no commit -and no merge. Automatic triggering is later work; the target behaviour for a hook +With the optional `standards` preset installed, it also reports after a direct +`git commit`. It blocks nothing: no commit, no merge, no tool call. Automatic triggering is later work; the target behaviour for a hook remains in the [hook contract](hook-contract.md). A project uses this to state, once, what is true of its whole codebase, so the @@ -118,6 +118,28 @@ is reported as `FAIL` and the report says its exit codes are unmapped. Runs are bounded: a per-tool timeout, a total budget, and captured output truncated with the truncation marked. +## The optional preset + +```sh +./scripts/install codex --project /path/to/project --hooks standards +``` + +It registers one hook: `PostToolUse` on a direct `git commit`. After the commit +exists, the checks whose trigger is `commit` run and the result is reported to +the agent as context. Nothing is blocked, and the commit has already happened. + +The registered budget is 60 seconds, larger than the 12 the other presets use, +because a check is slower than a notice. The host is blocked while it runs, so +keep the commit trigger to fast checks; a tool that outruns the budget is +`INCONCLUSIVE`, and its process group is ended rather than left running. + +**`trigger: "pr"` has no hook, deliberately.** Neither host has a PR-completed +event. `Stop` is the nearest moment, and it fires at the end of every agent turn, +which is not what `pr` means. Rather than map onto an event that means something +else and then suppress the noise, that granularity stays an explicit +`standards.py run` at review time, alongside the review discipline the workflow +already carries. + ## Commands this skill does not ship `ruff` and `pyright` run with argv this skill supplies, and a configuration diff --git a/dist/claude-code/structured-coding/scripts/standards.py b/dist/claude-code/structured-coding/scripts/standards.py index e13183b..464c724 100644 --- a/dist/claude-code/structured-coding/scripts/standards.py +++ b/dist/claude-code/structured-coding/scripts/standards.py @@ -25,8 +25,12 @@ from continuity import ( # noqa: E402 HOSTS, Repository, active_record, atomic_json, read_json, safe_path, ) +# recognized() is the careful direct-git-commit test; it is generic rather than +# checkpoints-specific, and both files always ship together. +from checkpoints import bounded, recognized # noqa: E402 MAX_INPUT = 256 * 1024 +ERRORS = (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError) # 1 is what the shipped defaults declare, because they need nothing newer. # 2 adds a project-supplied command, for a tool we ship no argv for. SCHEMA = 1 @@ -653,15 +657,68 @@ def approve(project): return 0 +COMMIT_TIMEOUT = 60 + + +def event(project, host, mode, payload): + """Report after a commit. Never blocks, never raises to the host.""" + if mode != "post-commit" or payload.get("hook_event_name") != "PostToolUse": + raise Invalid(project, "event", "unexpected lifecycle payload") + tool_input = payload.get("tool_input") + if payload.get("tool_name") != "Bash" or not isinstance(tool_input, dict): + return {} + if any(key in tool_input for key in ("cwd", "workdir")): + return {} + if not recognized(tool_input.get("command")): + return {} + cwd = payload.get("cwd") + if not isinstance(cwd, str) or not cwd or not Path(cwd).is_absolute(): + raise Invalid(project, "cwd", "missing or invalid lifecycle cwd") + repository = Repository(project) + # Exact root excludes nested repositories and shell cwd overrides. + if Path(cwd).resolve(strict=True) != repository.root: + return {} + effective, _ = resolved(project) + if effective["checks"]["trigger"] != "commit": + return {} + base = bound_base(project, host, payload.get("session_id")) + results = outcomes(project, effective, base) + lines = [f"{r['name']}: {r['outcome']} ({r['reason']})" for r in results] + return { + "hookSpecificOutput": { + "hookEventName": "PostToolUse", + "additionalContext": bounded( + "Structured Coding standards, reported after this commit. " + "These results are advisory; nothing was blocked, and an " + "INCONCLUSIVE or NOT RUN check is not a pass. " + + "; ".join(lines) + ), + } + } + + def main(argv=None): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("action", choices=("inspect", "run", "approve")) + parser.add_argument("action", choices=("inspect", "run", "approve", "event")) + parser.add_argument("--event", choices=("post-commit",)) parser.add_argument("--project", required=True, type=Path) parser.add_argument("--base", help="revision changed-file scope compares against") parser.add_argument("--host", choices=HOSTS, help="host of a session binding to read the base from") parser.add_argument("--session", help="session id of that binding") args = parser.parse_args(argv) try: + if args.action == "event": + raw = sys.stdin.buffer.read(MAX_INPUT + 1) + if len(raw) > MAX_INPUT: + raise Invalid(args.project, "event", "oversized lifecycle payload") + try: + print(json.dumps(event(args.project, args.host, args.event, + json.loads(raw)), ensure_ascii=True)) + except ERRORS: + # Fail open: a hook must never block the host it advises. + print("Standards hook unavailable; reconcile locally.", file=sys.stderr) + print("{}") + return 0 if args.action == "approve": return approve(args.project) value = report(args.project) diff --git a/dist/codex/structured-coding/references/platforms.md b/dist/codex/structured-coding/references/platforms.md index c43e913..a14f482 100644 --- a/dist/codex/structured-coding/references/platforms.md +++ b/dist/codex/structured-coding/references/platforms.md @@ -35,7 +35,7 @@ Use `claude-code` for Claude Code. Python 3.9+, Git and macOS/Linux/WSL are requ Codex requires trust at **two** levels, and a registration is inert until both are given. First the project: until it is trusted, Codex loads no project-local hooks at all and says so on stderr, naming the file it wants the entry in. That entry must live in the `config.toml` under `CODEX_HOME`; a `-c projects."".trust_level="trusted"` override on the command line does **not** satisfy it, which is the failure that looks like hooks being delivered but ignored. Second, each hook definition is trusted by its own content hash, so changing a command asks again. Both are Codex's own mechanism; neither is granted by this installer. Claude Code has no equivalent second level. -Installation is additive. Checkpoints alone registers shared SessionStart, PreToolUse and Stop, without compact handlers. Continuity alone registers shared SessionStart and manual/automatic PreCompact. Both use five groups, with SessionStart exactly once. Removing one keeps the other usable. Bare `--remove-hooks` removes all owned presets and preserves the skill and session data. Repeating a selection is a byte-preserving no-op; duplicate/unknown names are rejected. +Installation is additive. Standards alone registers shared SessionStart and PostToolUse, and is the only preset that runs anything; its PostToolUse hook carries a 60-second budget rather than 12, because a check is slower than a notice. Checkpoints alone registers shared SessionStart, PreToolUse and Stop, without compact handlers. Continuity alone registers shared SessionStart and manual/automatic PreCompact. Both use five groups, with SessionStart exactly once. Removing one keeps the other usable. Bare `--remove-hooks` removes all owned presets and preserves the skill and session data. Repeating a selection is a byte-preserving no-op; duplicate/unknown names are rejected. One aggregate receipt owns the original settings backup. Legacy continuity receipts remain readable and migrate only on an explicit state change; check, preview and unchanged reinstall do not rewrite them. Existing customized runtime dependencies are never overwritten: compare/back up the installed skill and explicitly update it first. Missing, changed or duplicate owned groups, unknown receipt schemas, and redirected/unverifiable paths cause refusal. New installations register commands that contain no machine-specific path, so a registration committed to a shared settings file works on a teammate's machine; each machine still reviews and trusts the hooks itself. An installation made by an older release keeps its absolute commands, stays inspectable and removable, and is rewritten only by `--upgrade-registration`, which changes every command and therefore requires trusting the hooks again. `--check-hooks` says whether a registration is portable, and names the recorded interpreter only for an absolute one. diff --git a/dist/codex/structured-coding/references/platforms.zh-CN.md b/dist/codex/structured-coding/references/platforms.zh-CN.md index c06c518..8b66fc9 100644 --- a/dist/codex/structured-coding/references/platforms.zh-CN.md +++ b/dist/codex/structured-coding/references/platforms.zh-CN.md @@ -35,7 +35,7 @@ Claude Code 用户把 `codex` 换成 `claude-code`。需要 Python 3.9+、Git Codex 的信任分**两层**,两层都给到之前注册都是空转的。第一层是项目:在项目被信任之前,Codex 根本不加载任何 project-local hook,并会在 stderr 上说明,同时指出它要求写入的文件。那条记录必须写在 `CODEX_HOME` 下的 `config.toml` 里;命令行上的 `-c projects."".trust_level="trusted"` 覆盖**不满足**这一条,而这种失败看起来像是 hook 被送达却被忽略了。第二层是每个 hook 定义按自身内容 hash 单独信任,所以改动命令会重新询问。两层都是 Codex 自己的机制,installer 都不会替你授予。Claude Code 没有对应的第二层。 -安装是追加操作。单装 checkpoints 会注册共享 SessionStart、PreToolUse 和 Stop,不注册 compact handler。单装 continuity 会注册共享 SessionStart 和手动/自动 PreCompact。组合安装共五个 group,SessionStart 只出现一次。移除一个后另一个仍可用。裸 `--remove-hooks` 移除全部自有 preset,保留 skill 和 session 数据。重复安装同一选择不会改变文件字节;重复或未知名称会被拒绝。 +安装是追加操作。单装 standards 会注册共享 SessionStart 和 PostToolUse,它是唯一会真正运行东西的 preset;它的 PostToolUse hook 预算是 60 秒而不是 12 秒,因为跑一次检查比发一条提示慢。单装 checkpoints 会注册共享 SessionStart、PreToolUse 和 Stop,不注册 compact handler。单装 continuity 会注册共享 SessionStart 和手动/自动 PreCompact。组合安装共五个 group,SessionStart 只出现一次。移除一个后另一个仍可用。裸 `--remove-hooks` 移除全部自有 preset,保留 skill 和 session 数据。重复安装同一选择不会改变文件字节;重复或未知名称会被拒绝。 一份 aggregate receipt 保存原始设置备份。旧 continuity receipt 仍可读取,仅在明确改变安装状态时迁移;检查、预览和未改变选择的重装都不改写它。installer 不覆盖定制过的 runtime 依赖:先比较、备份已有 skill,再明确升级。自有 group 缺失、被修改或重复,receipt schema 未知,以及路径被重定向或无法核验,都会导致拒绝。新安装注册的命令不含任何机器相关路径,因此提交到共享设置文件里的注册在同事的机器上同样有效;每台机器仍需自己审阅并信任这些 hook。旧版本装出来的注册保留其绝对路径命令,仍可检查、仍可移除,只有 `--upgrade-registration` 会重写它;重写会改变每一条命令,因此需要重新信任这些 hook。`--check-hooks` 会说明注册是否可移植,并且只对绝对路径的注册报告记录的 interpreter。 diff --git a/dist/codex/structured-coding/references/standards.md b/dist/codex/structured-coding/references/standards.md index 1fb1620..0f2a258 100644 --- a/dist/codex/structured-coding/references/standards.md +++ b/dist/codex/structured-coding/references/standards.md @@ -1,9 +1,9 @@ # Project standards configuration -Status: **RUNS ONLY WHEN INVOKED.** This release reads the configuration and, +Status: **REPORTS; NEVER BLOCKS.** This release reads the configuration and, on the `run` command, executes the declared checks and classifies each result. -It registers no hook, so nothing happens automatically, and it blocks no commit -and no merge. Automatic triggering is later work; the target behaviour for a hook +With the optional `standards` preset installed, it also reports after a direct +`git commit`. It blocks nothing: no commit, no merge, no tool call. Automatic triggering is later work; the target behaviour for a hook remains in the [hook contract](hook-contract.md). A project uses this to state, once, what is true of its whole codebase, so the @@ -118,6 +118,28 @@ is reported as `FAIL` and the report says its exit codes are unmapped. Runs are bounded: a per-tool timeout, a total budget, and captured output truncated with the truncation marked. +## The optional preset + +```sh +./scripts/install codex --project /path/to/project --hooks standards +``` + +It registers one hook: `PostToolUse` on a direct `git commit`. After the commit +exists, the checks whose trigger is `commit` run and the result is reported to +the agent as context. Nothing is blocked, and the commit has already happened. + +The registered budget is 60 seconds, larger than the 12 the other presets use, +because a check is slower than a notice. The host is blocked while it runs, so +keep the commit trigger to fast checks; a tool that outruns the budget is +`INCONCLUSIVE`, and its process group is ended rather than left running. + +**`trigger: "pr"` has no hook, deliberately.** Neither host has a PR-completed +event. `Stop` is the nearest moment, and it fires at the end of every agent turn, +which is not what `pr` means. Rather than map onto an event that means something +else and then suppress the noise, that granularity stays an explicit +`standards.py run` at review time, alongside the review discipline the workflow +already carries. + ## Commands this skill does not ship `ruff` and `pyright` run with argv this skill supplies, and a configuration diff --git a/dist/codex/structured-coding/scripts/standards.py b/dist/codex/structured-coding/scripts/standards.py index e13183b..464c724 100644 --- a/dist/codex/structured-coding/scripts/standards.py +++ b/dist/codex/structured-coding/scripts/standards.py @@ -25,8 +25,12 @@ from continuity import ( # noqa: E402 HOSTS, Repository, active_record, atomic_json, read_json, safe_path, ) +# recognized() is the careful direct-git-commit test; it is generic rather than +# checkpoints-specific, and both files always ship together. +from checkpoints import bounded, recognized # noqa: E402 MAX_INPUT = 256 * 1024 +ERRORS = (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError) # 1 is what the shipped defaults declare, because they need nothing newer. # 2 adds a project-supplied command, for a tool we ship no argv for. SCHEMA = 1 @@ -653,15 +657,68 @@ def approve(project): return 0 +COMMIT_TIMEOUT = 60 + + +def event(project, host, mode, payload): + """Report after a commit. Never blocks, never raises to the host.""" + if mode != "post-commit" or payload.get("hook_event_name") != "PostToolUse": + raise Invalid(project, "event", "unexpected lifecycle payload") + tool_input = payload.get("tool_input") + if payload.get("tool_name") != "Bash" or not isinstance(tool_input, dict): + return {} + if any(key in tool_input for key in ("cwd", "workdir")): + return {} + if not recognized(tool_input.get("command")): + return {} + cwd = payload.get("cwd") + if not isinstance(cwd, str) or not cwd or not Path(cwd).is_absolute(): + raise Invalid(project, "cwd", "missing or invalid lifecycle cwd") + repository = Repository(project) + # Exact root excludes nested repositories and shell cwd overrides. + if Path(cwd).resolve(strict=True) != repository.root: + return {} + effective, _ = resolved(project) + if effective["checks"]["trigger"] != "commit": + return {} + base = bound_base(project, host, payload.get("session_id")) + results = outcomes(project, effective, base) + lines = [f"{r['name']}: {r['outcome']} ({r['reason']})" for r in results] + return { + "hookSpecificOutput": { + "hookEventName": "PostToolUse", + "additionalContext": bounded( + "Structured Coding standards, reported after this commit. " + "These results are advisory; nothing was blocked, and an " + "INCONCLUSIVE or NOT RUN check is not a pass. " + + "; ".join(lines) + ), + } + } + + def main(argv=None): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("action", choices=("inspect", "run", "approve")) + parser.add_argument("action", choices=("inspect", "run", "approve", "event")) + parser.add_argument("--event", choices=("post-commit",)) parser.add_argument("--project", required=True, type=Path) parser.add_argument("--base", help="revision changed-file scope compares against") parser.add_argument("--host", choices=HOSTS, help="host of a session binding to read the base from") parser.add_argument("--session", help="session id of that binding") args = parser.parse_args(argv) try: + if args.action == "event": + raw = sys.stdin.buffer.read(MAX_INPUT + 1) + if len(raw) > MAX_INPUT: + raise Invalid(args.project, "event", "oversized lifecycle payload") + try: + print(json.dumps(event(args.project, args.host, args.event, + json.loads(raw)), ensure_ascii=True)) + except ERRORS: + # Fail open: a hook must never block the host it advises. + print("Standards hook unavailable; reconcile locally.", file=sys.stderr) + print("{}") + return 0 if args.action == "approve": return approve(args.project) value = report(args.project) diff --git a/dist/structured-coding-claude-code.zip b/dist/structured-coding-claude-code.zip index 849a2819be5f8681c665e211dca7328afabd6786..d50d252e3443c6675e4ef24a152365ce8efcfb55 100644 GIT binary patch delta 20514 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 YwfgUrUC}=k3;ta-fR}o`67X01fBx5VasU7T delta 19072 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 diff --git a/dist/structured-coding-codex.zip b/dist/structured-coding-codex.zip index 4c1ac12daaafe84fc03a53916335d899d04bbb19..426fc1e2fb03ebdc8d2a42803deccc56058916a2 100644 GIT binary patch delta 20548 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%0AL)K;Y|TC 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 RAQ5tE3y7a9`3dNk{J-ZL`49jA diff --git a/scripts/hook_install.py b/scripts/hook_install.py index d85c6dc..faca660 100644 --- a/scripts/hook_install.py +++ b/scripts/hook_install.py @@ -17,7 +17,7 @@ ROOT = Path(__file__).resolve().parents[1] FLOORS = {"codex": (0, 153, 4), "claude-code": (2, 1, 261)} -PRESETS = ("continuity", "checkpoints") +PRESETS = ("continuity", "checkpoints", "standards") MAX_FILE = 1024 * 1024 MAX_JOURNAL = 8 * MAX_FILE @@ -152,7 +152,7 @@ def selection(presets): values = tuple(presets) if not values or len(set(values)) != len(values) or set(values) - set(PRESETS): raise ValueError( - "Select each supported preset at most once: continuity checkpoints" + "Select each supported preset at most once: " + " ".join(PRESETS) ) return tuple(p for p in PRESETS if p in values) @@ -219,10 +219,17 @@ def groups(host, project, skill, presets=("continuity",), shape=ABSOLUTE): ("Stop", None, "checkpoints", "stop"), ] ) + if "standards" in presets: + # One discrete event the host really has. There is no PR-completed event, + # so that granularity stays an explicit command rather than a stand-in. + capabilities.append(("PostToolUse", "^Bash$", "standards", "post-commit")) result = {} for event, matcher, script, mode in capabilities: command = registered_command(shape, host, project, skill, script, mode) - group = {"hooks": [{"type": "command", "command": command, "timeout": 12}]} + # A commit-time check needs longer than an advisory notice, and both + # hosts were measured to honour a larger value. + seconds = 60 if script == "standards" else 12 + group = {"hooks": [{"type": "command", "command": command, "timeout": seconds}]} if matcher is not None: group = {"matcher": matcher, **group} result.setdefault(event, []).append(group) @@ -485,8 +492,11 @@ def replace(path, expected, replacement): def verify_skill(plan): source = ROOT / "structured-coding" dependencies = {"scripts/continuity.py", "references/continuity.md"} - if "checkpoints" in plan.get("presets", ("continuity",)): + selected = plan.get("presets", ("continuity",)) + if "checkpoints" in selected or "standards" in selected: dependencies.update({"scripts/checkpoints.py", "references/checkpoints.md"}) + if "standards" in selected: + dependencies.update({"scripts/standards.py", "references/standards.md"}) for relative in sorted(dependencies): path = safe(plan["skill"], relative) if read(path) != (source / relative).read_bytes(): diff --git a/scripts/install b/scripts/install index 181d3f7..c196687 100755 --- a/scripts/install +++ b/scripts/install @@ -68,9 +68,9 @@ def main(): parser.add_argument("--project", required=True, type=Path, help="Existing target project directory (use . for the current directory)") modes = parser.add_mutually_exclusive_group() - modes.add_argument("--hooks", nargs="+", action="append", choices=("continuity", "checkpoints"), + modes.add_argument("--hooks", nargs="+", action="append", choices=("continuity", "checkpoints", "standards"), help="Add the selected project-local hook presets") - modes.add_argument("--remove-hooks", nargs="*", action="append", choices=("continuity", "checkpoints"), + modes.add_argument("--remove-hooks", nargs="*", action="append", choices=("continuity", "checkpoints", "standards"), help="Remove selected presets, or all owned presets when no names are given") modes.add_argument("--check-hooks", action="store_true", help="Check registration, runtime files, and host version") modes.add_argument("--upgrade-registration", action="store_true", diff --git a/scripts/test_hook_presets.py b/scripts/test_hook_presets.py index 7cb7b19..b26bd03 100644 --- a/scripts/test_hook_presets.py +++ b/scripts/test_hook_presets.py @@ -61,95 +61,49 @@ def legacy(self, host): receipt.write_bytes(hooks.serialize(value["receipt"])) return config, skill, receipt - def test_both_hosts_install_and_remove_orders_preserve_exact_original_and_state( - self, - ): + # What each preset registers. SessionStart is shared by all of them. + EVENTS = { + "continuity": {"SessionStart", "PreCompact"}, + "checkpoints": {"SessionStart", "PreToolUse", "Stop"}, + "standards": {"SessionStart", "PostToolUse"}, + } + TOTAL_GROUPS = 6 + + def test_every_preset_coexists_and_any_one_can_be_removed(self): original = b'{ "description": "keep", "hooks": {"SessionStart": []}}\n' for host in hooks.PATHS: - for first in hooks.PRESETS: - for removed in hooks.PRESETS: - with self.subTest(host=host, first=first, removed=removed): - _, config, _, receipt = hooks.locations(host, self.project) - config.parent.mkdir(exist_ok=True) - config.write_bytes(original) - self.install(host, [first]) - second = next(p for p in hooks.PRESETS if p != first) - plan = self.install(host, [second]) - state = json.loads(config.read_bytes()) - self.assertEqual(sum(map(len, state["hooks"].values())), 5) - self.assertEqual(len(state["hooks"]["SessionStart"]), 1) - self.assertEqual( - json.loads(receipt.read_bytes())["presets"], - list(hooks.PRESETS), - ) - before = config.read_bytes(), receipt.read_bytes() - self.install(host, [first]) - self.assertEqual( - before, (config.read_bytes(), receipt.read_bytes()) - ) - hooks.remove(host, self.project, presets=[removed]) - survivor = next(p for p in hooks.PRESETS if p != removed) - events = set(json.loads(config.read_bytes())["hooks"]) - self.assertEqual( - events, - {"SessionStart", "PreCompact"} - if survivor == "continuity" - else {"SessionStart", "PreToolUse", "Stop"}, - ) - self.assertIn(survivor, hooks.doctor(host, self.project)) - data = self.state(host) / "retained.json" - data.parent.mkdir(parents=True, exist_ok=True) - data.write_text("{}") - hooks.remove(host, self.project) - self.assertEqual(config.read_bytes(), original) - self.assertTrue(data.exists()) - self.assertTrue(plan["skill"].exists()) - - def test_absolute_shape_is_the_default_and_keeps_its_published_form(self): - """A shipped registration must keep its exact form once shape is a parameter.""" - for host, parent in (("codex", ".agents"), ("claude-code", ".claude")): - skill = self.project / parent / "skills/structured-coding" - for presets in ( - ("continuity",), - ("checkpoints",), - ("continuity", "checkpoints"), - ): - with self.subTest(host=host, presets=presets): - built = hooks.groups(host, self.project, skill, presets) + for removed in hooks.PRESETS: + with self.subTest(host=host, removed=removed): + _, config, _, receipt = hooks.locations(host, self.project) + config.parent.mkdir(exist_ok=True) + config.write_bytes(original) + for preset in hooks.PRESETS: + self.install(host, [preset]) + state = json.loads(config.read_bytes()) self.assertEqual( - built, - hooks.groups( - host, self.project, skill, presets, hooks.ABSOLUTE - ), + sum(map(len, state["hooks"].values())), self.TOTAL_GROUPS ) - for entries in built.values(): - for group in entries: - entry = group["hooks"][0] - self.assertEqual(entry["timeout"], 12) - words = shlex.split(entry["command"]) - script = Path(words[1]) - self.assertEqual(words[0], sys.executable) - self.assertEqual(script.parent, skill / "scripts") - self.assertTrue(script.is_absolute()) - self.assertEqual(words[2:5], ["event", "--host", host]) - self.assertEqual( - words[words.index("--project") + 1], str(self.project) - ) - - def test_unknown_shape_and_schema_are_refused(self): - skill = self.project / ".agents/skills/structured-coding" - with self.assertRaises(ValueError): - hooks.groups("codex", self.project, skill, ("continuity",), "relative") - with self.assertRaises(ValueError): - hooks.registered_command( - "relative", "codex", self.project, skill, "continuity", "pre-auto" - ) - for schema in (1, 2): - self.assertEqual(hooks.shape_for_schema(schema), hooks.ABSOLUTE) - self.assertEqual(hooks.shape_for_schema(3), hooks.PORTABLE) - for schema in (0, 4, 99, None, "1"): - with self.subTest(schema=schema), self.assertRaises(ValueError): - hooks.shape_for_schema(schema) + self.assertEqual(len(state["hooks"]["SessionStart"]), 1) + self.assertEqual( + json.loads(receipt.read_bytes())["presets"], list(hooks.PRESETS) + ) + before = config.read_bytes(), receipt.read_bytes() + self.install(host, [hooks.PRESETS[0]]) + self.assertEqual(before, (config.read_bytes(), receipt.read_bytes())) + hooks.remove(host, self.project, presets=[removed]) + survivors = [p for p in hooks.PRESETS if p != removed] + expected = set().union(*(self.EVENTS[p] for p in survivors)) + self.assertEqual(set(json.loads(config.read_bytes())["hooks"]), expected) + report = hooks.doctor(host, self.project) + for survivor in survivors: + self.assertIn(survivor, report) + data = self.state(host) / "retained.json" + data.parent.mkdir(parents=True, exist_ok=True) + data.write_text("{}") + hooks.remove(host, self.project) + self.assertEqual(config.read_bytes(), original) + self.assertTrue(data.exists()) + self.assertTrue(hooks.locations(host, self.project)[2].exists()) def owned_group_count(self, config): return sum(len(v) for v in json.loads(config.read_bytes())["hooks"].values()) diff --git a/scripts/test_standards.py b/scripts/test_standards.py index ed0a710..9cd3a62 100644 --- a/scripts/test_standards.py +++ b/scripts/test_standards.py @@ -942,5 +942,78 @@ def test_an_ordinary_tool_is_unaffected_by_the_new_session(self): self.assertIn("hello", output) +class HandlerTests(WorktreeTest): + """The one hook this preset registers: report after a commit, block nothing.""" + + def setUp(self): + super().setUp() + self.bin = self.root / "bin" + self.bin.mkdir() + stub = self.bin / "ruff" + stub.write_text("#!/bin/sh\nexit 0\n"); stub.chmod(0o755) + environment = patch.dict( + os.environ, {"PATH": f"{self.bin}{os.pathsep}{os.environ['PATH']}"}) + environment.start(); self.addCleanup(environment.stop) + + def config(self, trigger): + relative = dict(standards.LAYERS)["base"] + path = self.project / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text('```json\n{"schema": 1, "checks": {"trigger": "%s", "tools": [' + '{"name": "ruff", "scope": "repository"},' + '{"name": "pyright", "enabled": false}]}}\n```\n' % trigger, + encoding="utf-8") + + def payload(self, command="git commit -m x", **extra): + return {"hook_event_name": "PostToolUse", "session_id": self.session, + "cwd": str(self.project), "tool_name": "Bash", + "tool_input": {"command": command}, **extra} + + def fire(self, payload): + result = subprocess.run( + [sys.executable, str(RUNTIME), "event", "--project", str(self.project), + "--host", "codex", "--event", "post-commit"], + input=json.dumps(payload), text=True, capture_output=True) + self.assertEqual(result.returncode, 0, result.stderr) + return json.loads(result.stdout), result.stderr + + def test_a_commit_reports_when_the_trigger_matches(self): + self.config("commit") + value, _ = self.fire(self.payload()) + context = value["hookSpecificOutput"]["additionalContext"] + self.assertIn("ruff: PASS", context) + self.assertIn("pyright: NOT RUN", context) + self.assertIn("advisory", context) + + def test_nothing_is_reported_when_the_trigger_does_not_match(self): + self.config("pr") + self.assertEqual(self.fire(self.payload())[0], {}) + + def test_only_a_direct_commit_is_acted_on(self): + self.config("commit") + for label, payload in { + "another command": self.payload("git status"), + "a chained command": self.payload("git commit -m x && rm -rf /"), + "another tool": self.payload(**{"tool_name": "Read"}), + "a redirected cwd": {**self.payload(), + "tool_input": {"command": "git commit -m x", + "cwd": "/elsewhere"}}, + "a foreign cwd": {**self.payload(), "cwd": str(self.root)}, + }.items(): + with self.subTest(case=label): + self.assertEqual(self.fire(payload)[0], {}) + + def test_a_malformed_payload_fails_open(self): + self.config("commit") + for payload in ({}, {"hook_event_name": "Stop"}, {"hook_event_name": "PostToolUse"}): + with self.subTest(payload=str(payload)[:24]): + value, stderr = self.fire(payload) + self.assertEqual(value, {}) + + def test_a_project_without_configuration_reports_the_default_trigger(self): + """The default trigger is pr, so a commit produces nothing.""" + self.assertEqual(self.fire(self.payload())[0], {}) + + if __name__ == "__main__": unittest.main() diff --git a/structured-coding/references/platforms.md b/structured-coding/references/platforms.md index c43e913..a14f482 100644 --- a/structured-coding/references/platforms.md +++ b/structured-coding/references/platforms.md @@ -35,7 +35,7 @@ Use `claude-code` for Claude Code. Python 3.9+, Git and macOS/Linux/WSL are requ Codex requires trust at **two** levels, and a registration is inert until both are given. First the project: until it is trusted, Codex loads no project-local hooks at all and says so on stderr, naming the file it wants the entry in. That entry must live in the `config.toml` under `CODEX_HOME`; a `-c projects."".trust_level="trusted"` override on the command line does **not** satisfy it, which is the failure that looks like hooks being delivered but ignored. Second, each hook definition is trusted by its own content hash, so changing a command asks again. Both are Codex's own mechanism; neither is granted by this installer. Claude Code has no equivalent second level. -Installation is additive. Checkpoints alone registers shared SessionStart, PreToolUse and Stop, without compact handlers. Continuity alone registers shared SessionStart and manual/automatic PreCompact. Both use five groups, with SessionStart exactly once. Removing one keeps the other usable. Bare `--remove-hooks` removes all owned presets and preserves the skill and session data. Repeating a selection is a byte-preserving no-op; duplicate/unknown names are rejected. +Installation is additive. Standards alone registers shared SessionStart and PostToolUse, and is the only preset that runs anything; its PostToolUse hook carries a 60-second budget rather than 12, because a check is slower than a notice. Checkpoints alone registers shared SessionStart, PreToolUse and Stop, without compact handlers. Continuity alone registers shared SessionStart and manual/automatic PreCompact. Both use five groups, with SessionStart exactly once. Removing one keeps the other usable. Bare `--remove-hooks` removes all owned presets and preserves the skill and session data. Repeating a selection is a byte-preserving no-op; duplicate/unknown names are rejected. One aggregate receipt owns the original settings backup. Legacy continuity receipts remain readable and migrate only on an explicit state change; check, preview and unchanged reinstall do not rewrite them. Existing customized runtime dependencies are never overwritten: compare/back up the installed skill and explicitly update it first. Missing, changed or duplicate owned groups, unknown receipt schemas, and redirected/unverifiable paths cause refusal. New installations register commands that contain no machine-specific path, so a registration committed to a shared settings file works on a teammate's machine; each machine still reviews and trusts the hooks itself. An installation made by an older release keeps its absolute commands, stays inspectable and removable, and is rewritten only by `--upgrade-registration`, which changes every command and therefore requires trusting the hooks again. `--check-hooks` says whether a registration is portable, and names the recorded interpreter only for an absolute one. diff --git a/structured-coding/references/platforms.zh-CN.md b/structured-coding/references/platforms.zh-CN.md index c06c518..8b66fc9 100644 --- a/structured-coding/references/platforms.zh-CN.md +++ b/structured-coding/references/platforms.zh-CN.md @@ -35,7 +35,7 @@ Claude Code 用户把 `codex` 换成 `claude-code`。需要 Python 3.9+、Git Codex 的信任分**两层**,两层都给到之前注册都是空转的。第一层是项目:在项目被信任之前,Codex 根本不加载任何 project-local hook,并会在 stderr 上说明,同时指出它要求写入的文件。那条记录必须写在 `CODEX_HOME` 下的 `config.toml` 里;命令行上的 `-c projects."".trust_level="trusted"` 覆盖**不满足**这一条,而这种失败看起来像是 hook 被送达却被忽略了。第二层是每个 hook 定义按自身内容 hash 单独信任,所以改动命令会重新询问。两层都是 Codex 自己的机制,installer 都不会替你授予。Claude Code 没有对应的第二层。 -安装是追加操作。单装 checkpoints 会注册共享 SessionStart、PreToolUse 和 Stop,不注册 compact handler。单装 continuity 会注册共享 SessionStart 和手动/自动 PreCompact。组合安装共五个 group,SessionStart 只出现一次。移除一个后另一个仍可用。裸 `--remove-hooks` 移除全部自有 preset,保留 skill 和 session 数据。重复安装同一选择不会改变文件字节;重复或未知名称会被拒绝。 +安装是追加操作。单装 standards 会注册共享 SessionStart 和 PostToolUse,它是唯一会真正运行东西的 preset;它的 PostToolUse hook 预算是 60 秒而不是 12 秒,因为跑一次检查比发一条提示慢。单装 checkpoints 会注册共享 SessionStart、PreToolUse 和 Stop,不注册 compact handler。单装 continuity 会注册共享 SessionStart 和手动/自动 PreCompact。组合安装共五个 group,SessionStart 只出现一次。移除一个后另一个仍可用。裸 `--remove-hooks` 移除全部自有 preset,保留 skill 和 session 数据。重复安装同一选择不会改变文件字节;重复或未知名称会被拒绝。 一份 aggregate receipt 保存原始设置备份。旧 continuity receipt 仍可读取,仅在明确改变安装状态时迁移;检查、预览和未改变选择的重装都不改写它。installer 不覆盖定制过的 runtime 依赖:先比较、备份已有 skill,再明确升级。自有 group 缺失、被修改或重复,receipt schema 未知,以及路径被重定向或无法核验,都会导致拒绝。新安装注册的命令不含任何机器相关路径,因此提交到共享设置文件里的注册在同事的机器上同样有效;每台机器仍需自己审阅并信任这些 hook。旧版本装出来的注册保留其绝对路径命令,仍可检查、仍可移除,只有 `--upgrade-registration` 会重写它;重写会改变每一条命令,因此需要重新信任这些 hook。`--check-hooks` 会说明注册是否可移植,并且只对绝对路径的注册报告记录的 interpreter。 diff --git a/structured-coding/references/standards.md b/structured-coding/references/standards.md index 1fb1620..0f2a258 100644 --- a/structured-coding/references/standards.md +++ b/structured-coding/references/standards.md @@ -1,9 +1,9 @@ # Project standards configuration -Status: **RUNS ONLY WHEN INVOKED.** This release reads the configuration and, +Status: **REPORTS; NEVER BLOCKS.** This release reads the configuration and, on the `run` command, executes the declared checks and classifies each result. -It registers no hook, so nothing happens automatically, and it blocks no commit -and no merge. Automatic triggering is later work; the target behaviour for a hook +With the optional `standards` preset installed, it also reports after a direct +`git commit`. It blocks nothing: no commit, no merge, no tool call. Automatic triggering is later work; the target behaviour for a hook remains in the [hook contract](hook-contract.md). A project uses this to state, once, what is true of its whole codebase, so the @@ -118,6 +118,28 @@ is reported as `FAIL` and the report says its exit codes are unmapped. Runs are bounded: a per-tool timeout, a total budget, and captured output truncated with the truncation marked. +## The optional preset + +```sh +./scripts/install codex --project /path/to/project --hooks standards +``` + +It registers one hook: `PostToolUse` on a direct `git commit`. After the commit +exists, the checks whose trigger is `commit` run and the result is reported to +the agent as context. Nothing is blocked, and the commit has already happened. + +The registered budget is 60 seconds, larger than the 12 the other presets use, +because a check is slower than a notice. The host is blocked while it runs, so +keep the commit trigger to fast checks; a tool that outruns the budget is +`INCONCLUSIVE`, and its process group is ended rather than left running. + +**`trigger: "pr"` has no hook, deliberately.** Neither host has a PR-completed +event. `Stop` is the nearest moment, and it fires at the end of every agent turn, +which is not what `pr` means. Rather than map onto an event that means something +else and then suppress the noise, that granularity stays an explicit +`standards.py run` at review time, alongside the review discipline the workflow +already carries. + ## Commands this skill does not ship `ruff` and `pyright` run with argv this skill supplies, and a configuration diff --git a/structured-coding/scripts/standards.py b/structured-coding/scripts/standards.py index e13183b..464c724 100644 --- a/structured-coding/scripts/standards.py +++ b/structured-coding/scripts/standards.py @@ -25,8 +25,12 @@ from continuity import ( # noqa: E402 HOSTS, Repository, active_record, atomic_json, read_json, safe_path, ) +# recognized() is the careful direct-git-commit test; it is generic rather than +# checkpoints-specific, and both files always ship together. +from checkpoints import bounded, recognized # noqa: E402 MAX_INPUT = 256 * 1024 +ERRORS = (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError) # 1 is what the shipped defaults declare, because they need nothing newer. # 2 adds a project-supplied command, for a tool we ship no argv for. SCHEMA = 1 @@ -653,15 +657,68 @@ def approve(project): return 0 +COMMIT_TIMEOUT = 60 + + +def event(project, host, mode, payload): + """Report after a commit. Never blocks, never raises to the host.""" + if mode != "post-commit" or payload.get("hook_event_name") != "PostToolUse": + raise Invalid(project, "event", "unexpected lifecycle payload") + tool_input = payload.get("tool_input") + if payload.get("tool_name") != "Bash" or not isinstance(tool_input, dict): + return {} + if any(key in tool_input for key in ("cwd", "workdir")): + return {} + if not recognized(tool_input.get("command")): + return {} + cwd = payload.get("cwd") + if not isinstance(cwd, str) or not cwd or not Path(cwd).is_absolute(): + raise Invalid(project, "cwd", "missing or invalid lifecycle cwd") + repository = Repository(project) + # Exact root excludes nested repositories and shell cwd overrides. + if Path(cwd).resolve(strict=True) != repository.root: + return {} + effective, _ = resolved(project) + if effective["checks"]["trigger"] != "commit": + return {} + base = bound_base(project, host, payload.get("session_id")) + results = outcomes(project, effective, base) + lines = [f"{r['name']}: {r['outcome']} ({r['reason']})" for r in results] + return { + "hookSpecificOutput": { + "hookEventName": "PostToolUse", + "additionalContext": bounded( + "Structured Coding standards, reported after this commit. " + "These results are advisory; nothing was blocked, and an " + "INCONCLUSIVE or NOT RUN check is not a pass. " + + "; ".join(lines) + ), + } + } + + def main(argv=None): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("action", choices=("inspect", "run", "approve")) + parser.add_argument("action", choices=("inspect", "run", "approve", "event")) + parser.add_argument("--event", choices=("post-commit",)) parser.add_argument("--project", required=True, type=Path) parser.add_argument("--base", help="revision changed-file scope compares against") parser.add_argument("--host", choices=HOSTS, help="host of a session binding to read the base from") parser.add_argument("--session", help="session id of that binding") args = parser.parse_args(argv) try: + if args.action == "event": + raw = sys.stdin.buffer.read(MAX_INPUT + 1) + if len(raw) > MAX_INPUT: + raise Invalid(args.project, "event", "oversized lifecycle payload") + try: + print(json.dumps(event(args.project, args.host, args.event, + json.loads(raw)), ensure_ascii=True)) + except ERRORS: + # Fail open: a hook must never block the host it advises. + print("Standards hook unavailable; reconcile locally.", file=sys.stderr) + print("{}") + return 0 if args.action == "approve": return approve(args.project) value = report(args.project) diff --git a/translations.json b/translations.json index 9df51d8..62bd365 100644 --- a/translations.json +++ b/translations.json @@ -40,8 +40,8 @@ { "english": "structured-coding/references/platforms.md", "chinese": "structured-coding/references/platforms.zh-CN.md", - "english_sha256": "28194aa111ae5be67bd47ce64ce12f0b156846ce368d1901446725219df6e4e5", - "chinese_sha256": "4ac5f04457c5eee702c02ed281304d3cd2f5e3fbe5030aaf956d51d538c16e41" + "english_sha256": "c38c3a4029f8aa81fceff307fe0bf9d2053a4258104448ee3eb0231ececa1524", + "chinese_sha256": "b57aa667541f6d86712158cc2cf1ddfbce8be1bf06d2f7d23ee6a04f33ffd4ac" } ], "specification_sha256": {