Repository navigation
feat(transcript): read a completed session transcript as an optional check input - #259
Conversation
CLOUD-95 Analyze completed session transcripts as a `check` input (optional, pointer-only)
Why Rejected alternative Approach — deterministic, structural, explainable Rejected alternative — in-process neural NLP (e.g. rust-bert) Definition of done
Acceptance
Refinement — Ready (a typed transcript event stream as an optional Refinement gate: Definition of Ready & Done. This body carries only specializations.
|
…check input Several divergence signals are invisible to synchronous mediation and fully recorded after the fact: declared-done with work not landed, a mediated deny followed by an enforcement-disabled retry, a stop with a non-empty frontier, an unprompted self-write. Four issues need the same parse, so it is factored out once here rather than re-derived four times. Every event comes from a TYPED field, never from reading prose. The find that shapes the module: a host records its hook runs structurally, with the code they returned, so "was this call denied" is exit_code == ExitCode::Violation -- the same table `hook` emits on, read from the other side -- rather than a substring match on an error message. A predicate built on prose would be a classifier wearing a gate's clothes. The other half of the bypass predicate is likewise a JSON boolean in the retained tool input, not a phrase. Free text is never interpreted and never emitted. A transcript holds every command, every file body and every prompt of a session, which makes it the richest source of secrets the engine can be pointed at, so Counts carries numbers and Record carries a line. A test plants a sentinel in each free-text position the host records and asserts none reaches either stream. Forward compatibility is a requirement rather than a nicety: the format is a host's and it moves. One captured session carried six top-level type values and eleven attachment subtypes, most meaningless to any predicate. So an unrecognized line yields no events instead of an error -- a host shipping a new record type must not turn every downstream gate red. A line that is not JSON at all is the one refusal. Three states, and the first two are the design. Unconfigured means the repository does not use the capability and is silent; absent means it was configured and is unavailable, so dependent rules did not run and that is reported; present-but-undecodable is a usage error. Collapsing the first two would fire on every minimal config, which is how a report teaches people to ignore it. The absent report rides both channels deliberately. output::message is ladder-gated, so --silent would erase the only signal that a gate was skipped, and a skipped gate exiting 0 in silence is the false green this engine exists to catch. The -J document has no Mode to consult, so it cannot be silenced, and a test pins that --silent -J still carries the state. Exit 1 for an undecodable transcript, not 2. The issue's own block writes `Usage` (`2`), where the name and the number contradict each other; the name wins. Section 7 spends 2 on the policy verdict alone, so a parse failure must never reach a mediating harness as a deny. No findings are emitted. FindingKind::Sequence and Observation::NotObserved both already exist with no producer, and they stay the reserved seam the four consuming issues occupy. run_rules' runner signature is likewise unchanged: the capability resolves beside it, because widening a fn pointer to carry an input no rule reads yet would be scaffolding built before the thing it holds. Refs: CLOUD-95 Claude-Session: https://claude.ai/code/session_01GQARxMUusqnDj4fSaJpdJL
8a3b958 to
1858746
Compare
|
|
/fast-forward |



Closes CLOUD-95.
Why
Declared-done-with-work-not-landed, deny-then-bypass, a stop with a non-empty frontier, an unprompted self-write — all invisible to synchronous mediation, all fully recorded after the fact. Four issues (CLOUD-97/98/219/267) need the same parse, so it lands once here.
The find that shaped the module
A host records its hook runs structurally. An
attachmentline carrieshookEvent,toolUseIDand — critically —exitCode. So "was this call denied" isexit_code == ExitCode::Violation: the same §7 tablehookemits on, read from the other side. Not a substring match on an error message.That is what makes CLOUD-98's deny-then-bypass a computable gate rather than a classifier, and its other half (
dangerouslyDisableSandbox: true) is likewise a JSON boolean in the retainedinputobject. Every event here comes from a typed field:assistant→content[].type == "tool_use"→id,name,inputuser→content[].type == "tool_result"→tool_use_id,is_errorattachment→hookEvent,toolUseID,exitCodemessage.roleThree states, and the first two are the design
[transcript]key-Jkey at all0UsageError, exit 1, pointer-onlyCollapsing the first two would fire on every minimal config, which is how a report teaches people to ignore it —
lint.rs's "absent is not empty", applied.The absent report rides two channels deliberately.
output::messageis ladder-gated, so--silenterases the stderr half; if that were the only channel, a skipped gate would exit0with nothing said — the false green this engine exists to catch. The-Jdocument has noModeto consult, so it cannot be silenced.the_absent_report_survives_silent_because_it_rides_the_data_channelpins it.Exit 1, not 2
The Ready block writes
`Usage` (`2`)— name and number contradicting each other inside one parenthesis. The name wins: §7 spends2on the policy verdict alone, and a parse failure reaching a mediating harness as a deny is the inversion that contract forbids. Corrected on the issue; the class is filed as CLOUD-307.Free text is never interpreted and never emitted
A transcript holds every command, every file body and every prompt of a session — the richest source of secrets the engine can be pointed at.
Countscarries numbers;Recordcarries a line. The fixture plants a sentinel in each free-text position the host records (thinking, a hook's stderr, a tool result's body) andno_transcript_text_reaches_any_output_streamasserts none reaches either stream on either channel.This also aligns with
judge.rsfor free: a span with no path provenance classifiesNoProvenance, whoseis_protected()is true. Nothing here synthesizes a path — doing so would silently downgrade transcript text toUnprotected.Forward compatibility is a requirement
The format is a host's and it moves. One captured session carried six top-level
typevalues and elevenattachment.typevalues, most meaningless to any predicate. An unrecognized line yields no events rather than an error — a host shipping a new record type must not turn every downstream gate red. A line that is not JSON at all is the one refusal.Deliberately not in scope
FindingKind::Sequenceandfindings::Observation::NotObserved{RuleSkipped}both already exist with no producer; they stay the reserved seam CLOUD-97/98/219/267 occupy. This issue is the substrate they consume, per its own §1.run_rules' runner signature is unchanged. It is afn(&[Rule], &Path)with nowhere to carry a transcript, so the capability resolves beside it. Widening it to pass an input no rule reads yet would be scaffolding built before the thing it scaffolds.checkstaysEffect::Read— no store write, no spawn.budget.rsbounds instruction files only); inventing one here is new surface.Verification
mise run fmt(fullhkgate): 33/33 steps green, zero failures.--silent -Jproperty, and the opacity assertion.module-map-check,memories-check,schema-check,config-lintgreen;mem:corerow added;schema/batten.schema.jsonregenerated.Generated by Claude Code