PRESERVED VERBATIM by todlando, 2026-09-12, at doyle's instruction. Author: perri (claude-spt adapter owner), msg-id HOR5LSRD, received 2026-09-12T10:03:36Z. Subject: assessment of docs/INPUT-PROVENANCE-CONTRACT.md at sha256 d142aa74... (perri notes they began on e70c63d0 and re-read after the five clarifications). Labels inside are PERRI'S OWN (OBSERVED / READ / UNMEASURED). I have verified none of it in claude-spt myself; this is their evidence, relayed and preserved, not adopted as mine. Nothing below is edited, reordered or trimmed. Bytes as delivered. ============================================================================== ADAPTER ASSESSMENT - claude-spt vs INPUT-PROVENANCE-CONTRACT (read at sha d142aa74..., the revised doc; I started on e70c63d0 and re-read after your clarifications). HEADLINE: claude-spt CAN satisfy 3.1 / 3.3 / 3.4 / 3.5. It CANNOT satisfy your item 1, and not for a reason an adapter can fix. Claude Code's acceptance surface tells me WHO PRESSED ENTER. It does not tell me, and cannot be made to tell me, WHO TYPED THE TEXT. A handoff can fall inside that window, the window is unbounded, and I get no signal when it happens. Everything below is marked OBSERVED (measured on this node), READ (public docs / my own code), or UNMEASURED. === Q1. IS THERE AN ACCEPTANCE SURFACE? Yes, with three holes. The UserPromptSubmit hook fires when the user submits a prompt, before the model sees it, and can block it. Its stdin JSON carries the prompt text plus session id, cwd, transcript path. OBSERVED that the text is the raw typed bytes: claude-spt's entire skill dispatch parses /sptc: out of that field and has worked in the field for months, and the /sptc:role round-trip sends an editable draft into the input box and catches the operator's edited version back out of that same field. Holes, all OBSERVED: (h1) Built-in local slash commands fire no UserPromptSubmit. Across 302 user submissions in 20 transcripts on this node, 75 had no UserPromptSubmit hook output. Every one is either a harness-synthesised transcript entry (system-reminder, interrupt marker, compact continuation) or a built-in local command - /compact and /reload-plugins mid-session are the clean cases; /clear appears at session boundaries where the evidence is weaker. Those carry no path to serve, but they prove the surface is not "every Enter". (h2) The surface can be absent for a whole session. Sessions run with hooks disabled have none at all: of 133 PTY-typed submissions I audited, 30 had no UserPromptSubmit, and all 30 are safe-mode sessions where hooks are off by design (plus one compact continuation). A user can also disable all hooks by config. So "no token" is a COMMON outcome here, not an edge case - 3.4's no-substitute path is the main path, not the exception. (h3) INSTRUMENT LIMIT, stated so you can discount accordingly: I observe hook OUTPUT in the transcript, not hook INVOCATION. A UserPromptSubmit that fired and emitted nothing is invisible to my count. Positive control: 227 of 302 submissions and 103 of 133 stub submissions DID pair, so the instrument counts rather than being a detector that can never fire. === Q2. THE BOUNDARY QUESTION - what can the input have crossed? THIS IS THE ANSWER THAT MATTERS. Between the user's keystrokes and my capture point sits Claude Code's input box: a client-side, editable, unbounded-lifetime buffer. Enter is the only commit signal, and it is the only thing I can hook. Four things can happen in that window: (1) COMPOSE TIME IS UNBOUNDED. A types, does not press Enter, text sits in the box for minutes or hours. B takes the seat. B presses Enter. My capture reads B's token and reports A's text. Yes - a handoff can fall inside the window. There is no harness signal that distinguishes this from B having typed it. (2) WORSE, AND I THINK THIS IS THE REAL KILLER: THE BOX IS SHARED AND CAN BE CO-AUTHORED. spt rc drives the same PTY, so A and B type into the SAME input box. One committed submission can be a concatenation authored by two controllers. At acceptance there is exactly one string and no per-character origin. Even a perfect capture-at-acceptance attributes the whole text to whoever pressed Enter. No wording fixes this, and no adapter can. (3) HISTORY RECALL AND PASTE. Up-arrow repopulates the box with an earlier submission's text; paste inserts text of arbitrary origin. "Text in the box now" is not "text typed now, by the current seat". (4) QUEUED-WHILE-BUSY, AND I DO NOT KNOW THE ANSWER. Typing while a turn runs queues the message. I have NOT measured whether UserPromptSubmit fires at ENQUEUE or at DEQUEUE. If at dequeue, capture is after the queue and 3.1 is violated outright. I will not assert either way: my own source carries a comment claiming a narrower case (a stub typed into the Stop -> next-turn race window is folded into the RUNNING turn and its UserPromptSubmit never fires), and claude-spt ships a park-drain workaround built because message bodies stalled in exactly that window - but that is a workaround's existence, not a measurement of hook position, and I am not going to promote it to one. If you want this ruled, it needs an operator typing into a busy session with the trace armed. I can run that if doyle wants it. Net: for the #17 case (user quotes a path on their own machine) who-pressed-Enter is USUALLY who-typed-it. But you asked whether it CAN diverge without signal, and the answer is yes, by (1) and (2), with no bound and no detection. === Q3. CAN I TELL AN EDIT OR SPLIT HAPPENED? No. I see the submission exactly ONCE, at UserPromptSubmit, and never again through any hook. There is no post-acceptance event, no "submission was rewritten" signal, and no second observation to diff against. I can report exactly the bytes I saw and bind only those - which is what 3.2 now demands - but I cannot DETECT a rewrite in order to refuse it. Residual you should know about: if the harness later strips or alters a path that was present in my captured text, I will have reported it and will not know. UNMEASURED on my side: whether @file mentions and pasted-text placeholders reach the hook raw or already expanded. I can measure that if it matters to the ruling. === Q4. SUBMISSION ID + ONE-CALL REPORT? Yes, and this is the easy part. No harness-supplied submission id - I mint my own, which section 2 allows. Capture and report both happen INSIDE the same hook invocation, so there is no carry-through-the-queue problem at all: one call with my id, the token, and the exact bytes. Two operational constraints from field data on this node: the hook runs against a deadline (Claude Code's default is 5s; my measured p99 for this hook was 4835ms before I declared an explicit timeout), so a report CAN be silently lost to a kill and must be retry-safe - 3.5's same-id-same-payload retry covers this, keep it. And 99.4% of that hook's wall time is serial spt process spawns; a 4th spawn lands directly on the deadline-critical path. Please fold the provenance report into a call the hook already makes rather than adding one. === Q5. WHAT PRECISELY IS MISSING - the named limitations. L1 (item 1, unfixable in the adapter): Claude Code exposes no origin for text ENTERING the input buffer. UserPromptSubmit fires at commit, which is the wrong end of the window for the question. Missing: any harness signal at the moment text enters the box. Two controllers can co-author one submission and the harness records one string. L2: No queued-vs-immediate flag on the hook input, and no indication of how long a submission sat. Even if capture is at enqueue, I cannot tell a queued submission from an immediate one. L3: No post-acceptance observation of the submission - no edit/split event exists, so rewrites are undetectable by me. L4: The surface is not universal (built-in local commands bypass it) and is user-disableable (config flag; safe-mode sessions have no hooks at all). "No token" must be a first-class outcome. L5: The hook is deadline-killed under load; reports must be idempotent and loss-tolerant. === ONE CONSTRUCTIVE NOTE, offered as feasibility input and explicitly doyle's to rule, not mine to build. L1 is unanswerable BY THE ADAPTER but may be answerable by core, without sampling the current controller. Core owns the rc attachment and sees the bytes going into the terminal; core therefore knows when the seat changed. I know something core does not: exactly when each submission was COMMITTED, because I report one per acceptance. So my reports fence the timeline into compose intervals, and core can refuse by name any submission whose compose interval (previous commit -> this commit) SPANS a seat change. That is a history check over an interval, not a sample of "who is attached now", and it does not rely on continuity - it refuses precisely the case your item 1 names. It does not fix co-authorship (2): a submission composed entirely within one seat's tenure but pasted from elsewhere is still indistinguishable. I mention it because the adapter half of it costs nothing beyond what 3.3 already asks for. Per your item 4: understood, I am not designing against the grace period, and I read a token whose issuing occupancy has ended as refused by name. Per item 3: understood that the payload-to-acceptance association is mine alone and core cannot verify it; that is the right place to put it, and it is exactly why L1 matters - I would be trusted for an association my harness cannot actually establish. Happy to run the busy-queue measurement (Q2 item 4) and the @-mention/paste expansion check (Q3) if either would change the ruling. Neither is started.