Recorder API
Added GitRecorder, create_git_recorder, remove_git_recorder, and git_recorder_script.
Added the isolated recorder core for creating a temporary git wrapper around one Codex launch session. The wrapper writes sanitized JSONL records and can be removed without changing persistent PATH configuration.
The new recorder module creates a managed temporary git executable, returns the launch environment a caller should use, invokes a caller-specified real git executable, forwards output, preserves the exit code, and appends one JSON record per invocation.
Added GitRecorder, create_git_recorder, remove_git_recorder, and git_recorder_script.
The return object includes wrapper_dir, wrapper_path, trace_path, real_git, path, and env.
Trace records include timestamp, cwd, sanitized argv, duration, exit code, and stdout/stderr byte counts. Excerpts require FJ_SHIM_TRACE_BODY=1.
Added fake-git coverage for wrapper creation, execution, cleanup, output forwarding, byte counts, and redaction.
This task intentionally does not wire the recorder into cli.py. The main integration flow can call the helper, launch Codex with recorder.env or recorder.path, and call remove_git_recorder afterward.
create_git_recorder accepts a fake real_git path, which keeps tests independent from the machine's installed git.Focused unified diff fallback showing the new public API, trace fields, and representative tests.
diff --git a/src/gh_forgejo_shim/git_recorder.py b/src/gh_forgejo_shim/git_recorder.py
new file mode 100644
--- /dev/null
+++ b/src/gh_forgejo_shim/git_recorder.py
@@
+TRACE_PATH_ENV = "FJ_SHIM_GIT_TRACE"
+TRACE_BODY_ENV = "FJ_SHIM_TRACE_BODY"
+REAL_GIT_ENV = "FJ_SHIM_REAL_GIT"
+
+@dataclass(frozen=True)
+class GitRecorder:
+ wrapper_dir: Path
+ wrapper_path: Path
+ trace_path: Path
+ real_git: Path
+ path: str
+ env: dict[str, str]
@@
+def create_git_recorder(trace_path: str | Path, *, real_git: str | Path | None = None, root_dir: str | Path | None = None, wrapper_dir: str | Path | None = None, env: Mapping[str, str] | None = None) -> GitRecorder:
+ git_path = _resolve_real_git(real_git, values)
+ wrapper.write_text(git_recorder_script(git_path, trace), encoding="utf-8")
+ launch_env["PATH"] = path
+ launch_env[TRACE_PATH_ENV] = str(trace)
+ launch_env[REAL_GIT_ENV] = str(git_path)
@@
+ record["duration"] = round(duration, 6)
+ record["duration_ms"] = round(duration * 1000, 3)
+ record["exit_code"] = exit_code
+ record["stdout_bytes"] = stdout_bytes
+ record["stderr_bytes"] = stderr_bytes
+ if include_excerpt:
+ record["stdout_excerpt"] = _redact_text(_safe_decode(stdout_excerpt))
+ record["stderr_excerpt"] = _redact_text(_safe_decode(stderr_excerpt))
diff --git a/tests/test_git_recorder.py b/tests/test_git_recorder.py
new file mode 100644
--- /dev/null
+++ b/tests/test_git_recorder.py
@@
+def test_wrapper_records_invocation_and_preserves_real_git_result(self) -> None:
+ completed = subprocess.run(["git", "status", "--short"], cwd=root, env=recorder.env, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+ self.assertEqual(record["command_argv"], ["git", "status", "--short"])
+ self.assertEqual(record["stdout_bytes"], len(b"fake stdout\n"))
+ self.assertNotIn("stdout_excerpt", record)
@@
+def test_trace_body_excerpts_are_opt_in_and_redacted(self) -> None:
+ env[TRACE_BODY_ENV] = "1"
+ serialized = json.dumps(record)
+ self.assertIn("[REDACTED]", serialized)
+ self.assertNotIn("argv-secret", serialized)
+ self.assertIn("visible", record["stdout_excerpt"])
No CLI behavior changes yet. After integration, Codex launch sessions can temporarily shadow git, collect sanitized command telemetry, and cleanly remove the wrapper directory at the end of the session.
python3 -m unittest tests.test_git_recorder.python3 -m py_compile src/gh_forgejo_shim/git_recorder.py tests/test_git_recorder.py.python3 -m unittest ran 119 tests successfully in the shared tree.git diff --check -- docs/turns/ and git diff --cached --check before commit.