2026-06-19 Repository implementation Issue gh-forgejo-shim-6iw

Temporary Git Recorder Core

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.

Summary

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.

Changes Made

Recorder API

Added GitRecorder, create_git_recorder, remove_git_recorder, and git_recorder_script.

Launch Env

The return object includes wrapper_dir, wrapper_path, trace_path, real_git, path, and env.

Trace Safety

Trace records include timestamp, cwd, sanitized argv, duration, exit code, and stdout/stderr byte counts. Excerpts require FJ_SHIM_TRACE_BODY=1.

Unit Tests

Added fake-git coverage for wrapper creation, execution, cleanup, output forwarding, byte counts, and redaction.

Context

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.

Important Implementation Details

Relevant Diff Snippets

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"])

Expected Impact for End-Users

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.

Validation

Issues, Limitations, and Mitigations

Follow-up Work