Teach the remote shim the Artifactory's five verbs - #282
Conversation
A loop on an ssh:// or Codespaces host is handed the briefing's full "notes for whoever comes next" section, told to check the board at the start of every pass, and then refused by its CLI on post, sync, read, list and watch: the Python shim dispatches seven verbs and artifactory is not one of them. Its own node sends are mirrored onto that board by the daemon, so a remote loop was not absent from the Artifactory, it was write-only and blind -- visible to every peer, unable to read a word anyone said back. The refusal was entirely client-side. GraphCommand carries no host discrimination, GraphStore dispatches the three board commands unconditionally, and ssh -N -R forwards bytes without parsing frames, so nothing in DaemonProtocol, RemoteSocketForwarder or GraphStore changes here. read and list need no command at all: openProject's graphChanged snapshot already carries the whole board and every node's cursor. status gains the board line too. renderArtifactoryStatusLine has been on the local CLI's render since the board shipped, and the shim's render never had it -- so the cheap "is there mail" check the briefing sends every loop to before claiming work was silently absent remotely, with the snapshot it needed already in hand. The renderer is now duplicated, Swift on the Mac and Python on the remote host, and cliShimStamp is content-derived: a defective shim propagates to every remote host on the next ensure and breaks node send and memo for every loop already running there. What makes that safe is RemoteCLIShimTests asserting the two byte-equal -- real session, real shim source, production renderer, never a constructed string. It earned its keep twice: the reference-date offset is exactly 11323 days, which is exactly 31 years across the fixture's leap days, so a dropped offset renders an identical MMM d, HH:mm stamp and the year had to be pinned through ISO-8601 instead; and Swift's JSONEncoder escapes / as \/ where json.dumps does not, which the fixture now carries a path and a URL to catch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoU8LMupkqeoemNSob7VuM
Five findings from review of the byte-equal port, four of them real divergences the first fixture could not reach. `--search` compared code points where Swift's String.contains compares canonically, so a body carrying decomposed text -- anything sourced from a macOS path routinely does -- was findable from the Mac and invisible from the remote host. That is the board answering that mail does not exist. Both sides now fold through NFC. `renderHeadline`'s budget counted code points where Swift counts extended grapheme clusters. The first commit documented this as a character either way; measured, it cuts a body of decomposed accents at 15 characters against the Mac's 30, and can land between a base character and its combining mark or inside a ZWJ sequence, ending a remote headline on a mangled glyph. UAX #29 reduced to the joins that actually reach a note -- combining marks, ZWJ, variation selectors, skin-tone modifiers, flags -- now backs the cut, with every one of them driven through both renderers. A token starting with -- was accepted as the project path, so `artifactory list --json` dialled the daemon and asked it to open a project named --json instead of refusing up front the way parseArtifactory does. Now refused before any dial. A post's topic was tested for truthiness rather than presence, so an empty topic rendered without the `()` Swift's Optional map produces. The daemon refuses an empty topic today, which is exactly why the renderer should not carry a second opinion about it. Also: the deliveryFragment doc comment's stranded-receipt argument cited a 14.6 KB shim; it is 31 KB now. The argument holds, the figure did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoU8LMupkqeoemNSob7VuM
Review round 1 appliedAll five findings addressed, each pinned by a test that fails when the fix is reverted (verified — reverting all four code fixes fails 16 assertions across 3 tests).
On #2 in particular — the review was right that the PR undersold it, and it turned out to be fixable rather than merely documentable. On #4: agreed it is unreachable through the daemon today. Fixed anyway for the reason the finding gives — the renderer should not carry a second opinion about a rule that lives in Gate re-run: 1573 tests pass (3 new), swiftlint 0 errors, swift-format strict clean, all three schemes build. Live diff against the real CLI on the now-132-post board re-confirmed byte-identical for Still unverified, unchanged from the PR body: no end-to-end run on a real remote host or Codespace — I have none available. 🤖 Generated with Claude Code |
Closes #281.
A loop on an
ssh://or Codespaces host is handed the briefing's full "The Artifactory — notes for whoever comes next" section, told to check the board at the start of every pass, and then refused by its CLI on all five verbs. Its ownnode sends are mirrored onto that board by the daemon, so a remote loop was not absent from the Artifactory — it was write-only and blind: visible to every peer, unable to read a word anyone said back.Shim-only, as #281 established
No
DaemonProtocol,RemoteSocketForwarder,GraphStoreorSessionBriefingchange.readandlistsend no command at all —openProject'sgraphChangedsnapshot already carries the whole board and every node'slastArtifactoryRead.postartifactoryPostframe; printsposted #N (topic)from the returned graphsyncartifactorySync, rendered from the snapshot taken before the cursor moves;--headlines--full--mark--json, same precedence as the Swift CLIreadlist--search--jsonwatchartifactoryWatchframe;--topic--offsyncandwatchrefuse a human shell up front in the Swift CLI's own wording, before dialling, so nothing is sent and nothing needs undoing.Plus the sixth gap from the issue: the shim's
render(graph)omitted the board status line the local CLI appends (renderArtifactoryStatusLine), sographcode statushid the board from a remote loop even with the snapshot in hand. Fixed here.The risk, and what answers it
This adds a second copy of the renderer — Python, inside the shim — and
cliShimStampis content-derived, so a defective shim propagates to every remote host on the next ensure and silently breaksnode sendandmemofor every loop already running there.What makes that safe is
RemoteCLIShimTestsasserting the two byte-equal, to the standard PR #275 set inCodexReadinessGateTests: a real socket, the productioncliShimSourceexecuted as delivered, and every expected value computed by callingGraphcodeCommand's own renderer — never a string written by hand. 13 new tests, 18 in the suite.The parity test earned its keep twice, on drift I would not have found by reading:
MMM d, HH:mm(Aug 23, 18:46either way). The stamp alone can never catch it. The year is pinned through--json's ISO-8601 instead, where the mistake is unmissable.JSONEncoderescapes/as\/;json.dumpsdoes not. The first fixture had no slash in any body, so the test passed while--jsonwas genuinely wrong. Caught by the live check below, then fixed and pushed back into the fixture, which now carries a path and a URL. Reverting the fix fails the test — verified.Also pinned: the 12-post / 4096-byte triage boundary on both sides and both directions; the identity refusals; that
read/listsend nothing past the open; and thatartifactoryno longer reaches the "Mac-only" refusal.Verification
Not verified: an end-to-end run on a real remote host or Codespace. I had none available. Everything below ran locally against the same daemon socket the ssh forward carries, and the byte-equal parity is what this PR stands on.
Beyond the suite, the delivered shim source was extracted and run against the live daemon socket on the real project board (136 posts), diffed against the real Swift CLI:
artifactory list <path>artifactory list --json\/fix — this is where it was found)artifactory list --search release/--search 0.1.59/ no matchartifactory read <id>(present, and #999 absent)status <path>board lineartifactory: 131 posts)statusstill differs from the local CLI in the pre-existing ways — the shim renders no edges block and no attention/exit-code suffixes. Out of scope here; the board line itself matches.Gate: 1573 tests pass,
swiftlint0 errors,swift format --strictclean, all three schemes (graphcode,graphcode-cli,graphcoded) build.Independent review
Reviewed by a separate loop (
ReviewPR282, OpenCode backend) againstb248f1a, which re-ran the gate in its own worktree and confirmed 1573 tests pass,swiftlint0 errors,swift format --strictclean, all three schemes building. Verdict: approve with notes, no blocking defects.It verified the parity harness runs the production
cliShimSourceunderpython3against a realAF_UNIXsocket serving productiongraphChangedframes, with every expected value produced by the realGraphcodeCommandrenderer — no hand-written renderer literals. It independently re-ran the live diff against the installed CLI on the real board, covering 63 non-ASCII bodies including a CJK post, and confirmed the\/escaping match.One divergence found, currently unobservable: the shim trims a note with Python
.strip()(all Unicode whitespace) where Swift uses.whitespaces(space and tab only). It cannot be observed today because the daemon re-trims with.whitespacesAndNewlineson arrival (GraphStore.swift:1594) — but it is a latent trap if that store-side trim ever changes.Recommended post-merge check: stamp propagation to a real host on first ensure, and live remote
sync/watchattribution viaZMX_SESSION, remain unproven here — both are pre-existing machinery rather than shim logic. The first remote ensure after this merges should be watched once.Known limits, deliberately not fixed
watchdelivery inheritsnode send's remote deliverability. A remote Codex turn-based loop willpost,sync,readandlistfine and miss only watch dings until Remote send gate silently drops messages to an attended Codex loop: pane-created sessions carry no readiness proof #278 lands. Neither fixed nor worsened here.renderHeadline's 80-character budget counts grapheme clusters in Swift and code points in Python, so a body built from combining sequences could cut a character differently. Noted in the shim; everything else about the line is identical.🤖 Generated with Claude Code
https://claude.ai/code/session_01AoU8LMupkqeoemNSob7VuM