diff --git a/engine/hooks/prove-it-ship-gate/README.md b/engine/hooks/prove-it-ship-gate/README.md index 7f9c88f9..89eebe2a 100644 --- a/engine/hooks/prove-it-ship-gate/README.md +++ b/engine/hooks/prove-it-ship-gate/README.md @@ -54,8 +54,11 @@ The stop is wired for Claude, Cursor, and Codex. Each harness gets that harness's own block: Claude exits 2, Cursor returns a followup message on its stop event, and Codex returns a block decision. A product command (a hyphenated program such as `invoker-cli doctor`) that -already ran in the session, including a subagent transcript beside it, has -to run again after the last file edit. Codex `exec_command` rows count as +ran between the previous file edit and the last one, or anywhere in a +subagent transcript beside the session, has to run again after the last +file edit. Commands from earlier edit cycles are that cycle's repro, not +this one's, so a long session does not accumulate every program it ever +ran into one demand. Codex `exec_command` rows count as that command, and `apply_patch` counts as the edit. A later `vitest` / `pnpm test` / `pytest` run does not replace it. The hook names the missing program and subcommand and blocks. Unix pipelines, `node -e` scripts, and a session that diff --git a/engine/hooks/prove-it-ship-gate/detect.py b/engine/hooks/prove-it-ship-gate/detect.py index 9552cbfc..917115b8 100644 --- a/engine/hooks/prove-it-ship-gate/detect.py +++ b/engine/hooks/prove-it-ship-gate/detect.py @@ -397,19 +397,21 @@ def same_repro_gap(transcript_path: str) -> list[str] | None: if primary is None: return None last_edit = None + previous_edit = -1 for index, (kind, _command) in enumerate(primary): if kind == "edit": - last_edit = index + previous_edit, last_edit = (last_edit if last_edit is not None else -1), index if last_edit is None: return [] if not any(kind == "cmd" and _is_test_runner(command) for index, (kind, command) in enumerate(primary) if index > last_edit): return [] seen: set[str] = set() for path in _session_paths(transcript_path): - events = primary if path == transcript_path else _read_events(path) - if events is None: - continue - for kind, command in events: + if path == transcript_path: + window = primary[previous_edit + 1:last_edit] + else: + window = _read_events(path) or [] + for kind, command in window: if kind == "cmd": seen.update(surface_keys(command)) rerun: set[str] = set() diff --git a/engine/hooks/prove-it-ship-gate/tests/test_hooks.py b/engine/hooks/prove-it-ship-gate/tests/test_hooks.py index a6faa139..5dbd187d 100644 --- a/engine/hooks/prove-it-ship-gate/tests/test_hooks.py +++ b/engine/hooks/prove-it-ship-gate/tests/test_hooks.py @@ -601,6 +601,24 @@ def test_fires_when_the_real_command_ran_in_a_subagent(self): self.assertIsNotNone(feedback) self.assertIn("invoker-cli --help", feedback) + def test_only_the_commands_since_the_previous_edit_count_as_the_repro(self): + with tempfile.TemporaryDirectory() as directory: + path = write_cursor_transcript(directory, "session.jsonl", [ + ("shell", "invoker-cli doctor"), + ("edit", ""), + ("shell", "pnpm test"), + ("shell", "other-tool run"), + ("edit", ""), + ("shell", "pnpm test"), + ]) + feedback = detect.decide({ + "last_assistant_message": "the test passed", + "transcript_path": path, + }) + self.assertIsNotNone(feedback) + self.assertIn("other-tool run", feedback) + self.assertNotIn("invoker-cli doctor", feedback) + def test_fails_open_when_the_same_repro_transcript_is_missing(self): self.assertIsNone(detect.same_repro_gap("/nonexistent/same-repro.jsonl")) self.assertIsNone(detect.decide({