Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

A create whose text gh's JSON sanitizer rewrites is recorded as a conflict on the controller's own object (found on PR #314)

Closed
#327 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

A pull request for this has already been merged.

  • #330 by @iceboundrock — merged

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
20/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
cli, github, python
Domain
backend

Research direction

Start from the controller's create read-back code implementing ADR 0004 where byte comparison with gh output occurs. Read go-gh's sanitizerRoundTripper to confirm the rewrite, then add a read-only REST GET using gh auth token for K2/K4/K5/K7/K8/K9. Run new tests A-G with fakes modelling the sanitizer; done when all pass, docs explain the limitation, and an older blocked run recovers via unblock without rewriting.

Written by the indexing model from the issue text.

Description

bug

Artifact: bug report. The bug was found when run af-20261025T011823Z-8df69a (#284, PR #314) blocked on its own PR. The fix, its tests and the run's recovery evidence follow in a PR that closes this issue. The working copy is a local branch in /home/ruoshi/code/github/.worktree/gh-exact-read.

Summary

A create's completion read-back (ADR 0004) compares the payload byte for byte with what gh prints. gh does not print GitHub's text as it is: go-gh's JSON sanitizer rewrites control characters, their JSON escapes and the literal text of an escape into caret notation. So:

  1. False conflict. A PR, issue or comment that the controller created byte for byte reads back as another text. The record becomes a conflict on the controller's own object, and the phase blocks. unblock only repeats the same comparison and blocks again.
  2. False match. A payload holding caret text (^C, ^[) reads back equal from an object that holds the control character (\x03, \x1b). This case did not occur on #284, but the same comparison allows it.

Evidence (PR #314)

  • The ANALYZE_EXECUTE agent's PR body explained terminal output and held the six characters \u001b as text (as in \u001b[0m).
  • The journal's K2 payload: 4408 characters, sha256 7e88c7877c7791e81bd117b39fe2a87f647e0c24e0d52b48622840ac8978d459.
  • gh pr list --json body read-back: 4405 characters, sha256 d0480d77…, three characters shorter per \u001b.
  • REST GET /repos/iceboundrock/AutoForge/pulls/314 (no gh): 4408 characters, sha256 7e88c787…, equal to the journal. The create landed byte for byte.
  • Head autoforge/284 @ 2f676de8, base main, repository, state and marker all matched. Only the body differed, and only on gh's read.
  • The run blocked with K2 in conflict. An earlier unblock re-blocked the same way.

Root cause

  • gh 2.103.2 links go-gh v2.17.0. Its API client installs asciisanitizer.Sanitizer{JSON: true} on the HTTP transport (sanitizerRoundTripper), and the rewrite happens on the reply's bytes before any JSON is decoded.
  • Each C0/C1 control character and its JSON escape become caret notation. So does the text of an escape: the wire form \\u001b (the body's literal \u001b) becomes \\^[, which decodes to \^[.
  • gh's cache and its GH_DEBUG log see the rewritten reply. No flag, Accept type or gh command turns the sanitizer off.
  • The rewrite is lossy and ambiguous: the literal text \^[ prints as \^[ too. No comparison of gh's print can therefore prove an object's text, and normalising either side would accept bodies the controller never wrote.

Expected behaviour

Case Result
gh's read equals the payload, which holds no ^ OBSERVE, with no extra request
One holder, every non-text condition holds, and the text differs on gh's read (or the payload holds a ^) read that one object exactly: equal → OBSERVE; different → CONFLICT
The exact read is unavailable or inconclusive explicit failure; never another create or edit
Two or more holders of one identity CONFLICT, with no exact read
A non-text condition fails (head, base, repo, state, marker, watermark) CONFLICT, with no exact read

Equality stays byte for byte. No .strip(), .replace() or Unicode normalisation, \u001b and \^[ are never treated as equal, and the persisted payload is never changed to fit a read-back. gh's terminal protection stays on for everything gh prints.

Acceptance criteria

  • The exact read is a read-only REST GET of one object under https://api.github.com/, using the credential gh holds. The credential stays in memory only. The read follows no redirect, is bounded in size and time, and fails closed on authentication, network, JSON and wrong-object errors.
  • It applies to the create read-backs of K2, K4, K5, K7, K8 and K9.
  • Tests:
    • A: the sanitizer's rewrite → OBSERVE.
    • B: genuine caret text or control characters → CONFLICT.
    • C: an exact read that is unavailable or inconclusive (timeout, 401/403/404, invalid JSON, oversize, gh auth token failure) → no create or edit.
    • D: duplicate holders → CONFLICT.
    • E: crash after a landed create → resume without an agent, push or create.
    • F: an ordinary body → no extra request.
    • G: hostile text is never quoted in a reason, error or log.
    • The fakes model the sanitizer with the same semantics as go-gh.
  • Documentation explains why gh's JSON cannot prove identity, what stays strict, why terminal protection is unaffected, and how to troubleshoot.
  • A run blocked this way by an earlier version recovers with unblock, with nothing written again.
Dominant language
Python
Stars
0
Forks
0
Avg merge
5h 51m
Merged PRs (30d)
73

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from iceboundrock/AutoForge

All issues in iceboundrock/AutoForge

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.