Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
89aa11e
Make a delivered message something the recipient can act on
thisisjun786 Sep 16, 2026
0bea954
Stop a work report from believing whatever its caller said it was
thisisjun786 Sep 16, 2026
1d95e19
Let the recorded verdict decide what a correction corrects
thisisjun786 Sep 16, 2026
7a9b75d
Keep the verdict last and the frozen manifest pointer present
thisisjun786 Sep 16, 2026
570340b
Record what a work report cannot know about a head commit
thisisjun786 Sep 16, 2026
66e62f0
Make the command the omission notice names actually return the report
thisisjun786 Sep 16, 2026
c0bfa9f
Keep the verification pointer when the file listing goes
thisisjun786 Sep 16, 2026
010af47
Bound every report field that lands on a line nobody can shorten
thisisjun786 Sep 16, 2026
de72bb8
Keep every submission of a report, not just the newest
thisisjun786 Sep 16, 2026
a22c020
Decide a resubmission inside the lock that writes it
thisisjun786 Sep 16, 2026
60f3163
Say which submission a message came from, and keep the commit context
thisisjun786 Sep 16, 2026
5aa00c2
Protect the submission identifier from the shortening that removed it
thisisjun786 Sep 16, 2026
9bb11cb
Treat a first report after a legacy delivery as a change too
thisisjun786 Sep 16, 2026
e05a1c2
Protect the completion identity and stop counting sends that never ha…
thisisjun786 Sep 16, 2026
89c12eb
Protect only the submissions that were actually frozen
thisisjun786 Sep 16, 2026
b67b876
Stop an inbox item from being rewritten behind the recipient
thisisjun786 Sep 16, 2026
7aea43e
Refuse a write that would report success and change nothing
thisisjun786 Sep 16, 2026
b22ca12
Make a submission clear both floors, not one of them
thisisjun786 Sep 16, 2026
7234672
Close the line-splicing route through nested values
thisisjun786 Sep 16, 2026
34ccdbc
Refuse the last handful of values that could not be rendered honestly
thisisjun786 Sep 16, 2026
9afd749
Stop requiring a disposition from a finding that only adds an anchor
thisisjun786 Sep 16, 2026
48bafc1
Render an absent disposition as absent, and never print a rejected nu…
thisisjun786 Sep 16, 2026
2da7f17
Carry the status into corrections and refuse what cannot be encoded
thisisjun786 Sep 16, 2026
06d38a6
Say where a finding came from when there is no recorded verdict
thisisjun786 Sep 16, 2026
3582516
Count the message instead of rebuilding it, and finish the unresolved…
thisisjun786 Sep 16, 2026
78fb02e
Keep one finding per criterion and one pass over the lines
thisisjun786 Sep 16, 2026
7354c00
Require text where text was being manufactured
thisisjun786 Sep 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions packages/codex-session-relay/docs/cxc-contract-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# The CXC report contract, as this relay reads it

Jun's managed workflow runs CXC and this relay together, so both have a vocabulary for how
an execution ended. Where the two touch, one of them has to say what the other one means.
This page is that statement. It copies no CXC prose and rebuilds no part of its state
machine: every row points at the installed file that owns the rule, so a version bump is
re-read rather than trusted from a transcription that has quietly gone stale.

Read against **codexclaw 0.2.28+codex.20260914090142**, installed under
`$CODEX_HOME/plugins/cache/codexclaw/`. The version string is reported identically by
`.codex-plugin/plugin.json` and `inventory.json`. `codex_session_relay.cxc.provenance()`
returns the same record at runtime, including the digest of every file below, and
`cxc.affected_by()` names which rules a changed install actually touches, so a bump
re-verifies those rules and their scenarios rather than all of them.

| Rule | Owning file | Anchor | sha256 |
| --- | --- | --- | --- |
| report outcomes are not phases | `skills/loop/SKILL.md` | 146-148 | `61167d15...a44e5e2b` |
| terminal-state vocabulary | `skills/pabcd/references/loop-engineering.md` | 32-40 | `c5fb8c4e...5db67fc7` |
| REVIEW-SYNTHESIS-01 | `skills/pabcd/references/loop-engineering.md` | 41-60 | `c5fb8c4e...5db67fc7` |
| DISPATCH-TASK-01 | `skills/pabcd/references/delegation.md` | 20-22 | `ba773dbd...d0c578b2` |
| plan output, nine concepts | `skills/pabcd/references/plan-output.md` | 7-20 | `4535762a...366c7868` |
| REVIEW-OUTPUT-01 | `skills/dev-code-reviewer/SKILL.md` | 86-95 | `1d66f377...2e5d2d28` |
| ATTEST-EVIDENCE-01 | `skills/pabcd/references/phase-control.md` | 45-48 | `6982abdd...5a8b476a` |
| LOOP-WAIT-EVIDENCE-01 | `skills/loop/references/waiting.md` | 31-63 | `988451de...b15150c8` |
| DISPATCH-SURFACE-01 | `skills/pabcd/references/dispatch-surfaces.md` | 10-20 | `dd3e6721...611c26e98` |

The full digests live in `cxc.SOURCES`; the table abbreviates only for width.

## Report status against relay outcome

A CXC report status does not choose a relay outcome. The outcome is asserted by the child's
completion receipt, which is the only thing the frozen contract lets assert one, and the
status is recorded beside it and checked for compatibility. A status that picked the outcome
could overrule the evidence; a status checked against it cannot.

| CXC status | Means | Compatible relay outcome |
| --- | --- | --- |
| `DONE` | the child proved every recorded criterion | `ready_for_review` |
| `NOOP` | nothing needed doing | `ready_for_review` |
| `BLOCKED` | an external dependency is in the way | `blocked_needs_input` |
| `UNSAFE` | a human risk decision is required | `blocked_needs_input` |
| `NEEDS_HUMAN` | a judgment only the user can make | `blocked_needs_input` |
| `BUDGET_EXHAUSTED` | a stated bound ran out | `interrupted`, `failed` |

Three statuses land on one outcome because the frozen enum has a single slot for a human
having to decide something, and adding a second is not available to this layer. Nothing is
lost: the original word and the reason the child gave are stored and rendered, so a reader
can still tell a blocked dependency from a refused risk. A status this build does not know
is refused by name, with the accepted set and the contract version it was read against,
rather than being mapped to whichever neighbour comes first in a dictionary.

`NOOP` pairs with `ready_for_review` on purpose. A report that nothing needed doing is still
a finding, and the work that established it is the deliverable. This package already refuses
to complete a managed assignment verified against nothing; a no-change claim with nothing
behind it is the same shape of claim.

## The word both systems use for different things

CXC glosses `DONE` as verified success, meaning the child proved its own recorded criteria.
The relay's `verified` is the **parent's** disposition, written through `record_verdict`
against registered criteria on an acknowledged event. Same English word, two authorities.

`cxc.NOT_VERIFICATION` lists the facts that resemble verification and are not, each with the
reason it does not carry:

- a `DONE` report, the child proving its own criteria rather than the parent's verdict
- an opened pull request, a place to review rather than a review
- a review `PASS`, one reviewer's judgment rather than the parent's disposition
- a green required check, evidence for a verdict rather than a verdict
- a completed turn, the trigger to look, as protocol v1 section 2 already says
- a dispatched delivery, neither an acknowledgement nor a verification

## Review verdicts

REVIEW-OUTPUT-01 fixes the final line, and this relay renders exactly that: `VERDICT: PASS`,
`VERDICT: GO-WITH-FIXES (blockers=N)`, `VERDICT: FAIL`. A `GO-WITH-FIXES` without a blocker
count is refused, because it is a `PASS` wearing a hedge, and a count attached to `PASS` or
`FAIL` is refused as a number nobody can act on. A verdict line is rendered only for a
message that carries a review, so an ordinary progress or completion notice cannot be
dressed as a review judgment.

## Waiting

LOOP-WAIT-EVIDENCE-01 keeps five endings apart, and `cxc.classify_wait` returns exactly one
of them: `progress`, `suspected_stagnation`, `confirmed_failure`, `unobservable`,
`input_needed`, and `timed_out` for a wait that simply ended on its bound. A terminal error
outranks everything; a request for input is not a failure; fresh advancing evidence outranks
the clock. No result ever sets `authorisesRerun`, because running the work again is a
decision somebody makes from evidence, and these are the states in which that evidence does
not exist yet. This mirrors what the delivery layer has always done: protocol v1 section 3
says no delivery state is reachable by a timeout, and section 4 says elapsed time is not
affirmative evidence.

## Instruction shape

A revision request is an instruction, so it carries the DISPATCH-TASK-01 fields, `TASK`,
`SCOPE`, `MUST DO`, `MUST NOT`, `PROOF` and `RETURN FORMAT`, plus a decision boundary. It
leads with the criterion that was violated and the anchor that reproduces it, because a
correction whose first line is an identifier is a correction the child has to research
before it can start. The acknowledgement asymmetry is unchanged: contract v1 defines no
acknowledgement for the parent-to-child direction, so the message says so and tells the
child what does work instead.

## What this does not do

It builds no dispatch mechanism, no state machine and no provider retry ladder; CXC owns all
three. It adds no value to any frozen enum: not the receipt outcomes, not the delivery
states, not the verdict dispositions. It does not require the recovery daemon to run a CXC
cycle. And it neither reads nor depends on an installed CXC runtime at delivery time, since
the provenance record above is pinned data rather than a live lookup.
60 changes: 60 additions & 0 deletions packages/codex-session-relay/docs/invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,64 @@ status. Every row below is implemented and carries a test; the suite is the proo
| I-63 | Unchanged states stay quiet | a tick that changes nothing writes no journal rows | implemented |
| I-64 | An unbounded daemon loop is not constructible | `run` requires a tick count, a deadline or a stop signal | implemented |

## Work reports and the CXC report contract

| # | Invariant | Enforced in | Status |
|---|---|---|---|
| I-90 | A CXC report status never chooses a relay outcome; it is checked against the one the receipt asserted | `cxc.check_status`, called from `report.record` | implemented |
| I-91 | An unrecognised report status is refused by name, with the accepted set and the contract version | `cxc.check_status` | implemented |
| I-92 | DONE, an open pull request, a review PASS and a green check are never a relay verdict | `cxc.NOT_VERIFICATION`, `cxc.refuse_promotion`; no path writes a verdict outside `ack.record_verdict` | implemented |
| I-93 | A verdict line is rendered only for a message carrying a review, in the fixed PASS / GO-WITH-FIXES (blockers=N) / FAIL form | `cxc.verdict_line`, `cxc.assert_reviewed` | implemented |
| I-94 | A report is bound to one event, generation and revision, and cannot answer for a later head | `report.assert_current` | implemented |
| I-95 | A pull request number is never rendered or compared without its repository | `report.pr_ref`, `report.pr_key` | implemented |
| I-96 | A report naming a pull request names the head commit it is about | `report.record` | implemented |
| I-97 | A shortened message names what it dropped and where to read it; a budget too small to hold the required parts refuses | `report._compose` | implemented |
| I-98 | An event with no work report renders the pre-contract message unchanged | `delivery._render_completion`, `delivery._render_revision` | implemented |
| I-99 | A wait result never authorises a re-run, and a bare timeout is neither failure nor success | `cxc.classify_wait` | implemented |
| I-100 | A report reads its relationship, generation, revision and outcome from the stored event; no caller supplies them | `report.record` | implemented |
| I-101 | A correction names the criteria the recorded verdict names; a review only adds notes and anchors, and anything it raises alone is labelled | `report._finding_lines` | implemented |
| I-102 | A shape or length that could only fail at render time is refused at record time, because rendering runs inside the delivery claim | `report._check_evidence`, `_check_unresolved`, `_bounded` | implemented |
| I-103 | A submission already frozen into a delivered attempt cannot be replaced in place; one never sent stays correctable | `report._assert_resubmission` against `attempt_report_submissions` | implemented |
| I-104 | An omission notice is placed before the final verdict, so an elided correction still ends on its judgment | `report._compose` | implemented |
| I-105 | A revision request cannot carry a PASS verdict | `report.record` | implemented |
| I-106 | A report-backed message keeps the receipt manifestRef the pre-contract message carried | `report._manifest_lines` | implemented |
| I-107 | The command an omission notice names returns the whole report, so every elided field stays recoverable | `cli.cmd_show` | implemented |
| I-108 | A restore section naming a skill owner nobody has is refused at record time | `report._check_restore` | implemented |
| I-109 | The frozen-manifest pointer is its own section, so shortening the file listing never drops it | `report._manifest_ref_lines` | implemented |
| I-110 | Every malformed report shape is a named refusal, never a host exception from the validator itself | `report._check_restore`, `_check_evidence`, `_check_unresolved` | implemented |
Comment thread
thisisjun786 marked this conversation as resolved.
| I-111 | Every report field that lands on a line the composer cannot shorten is length-bounded at record time | `report._bounded`, `_bounded_optional` | implemented |
| I-112 | A collection field that is not an ordered sequence is refused rather than iterated, so a mapping never becomes a list of its own keys and a string never becomes a list of characters | `report._sequence` | implemented |
| I-113 | Recording a later submission preserves the earlier one, so a recipient holding an older elided message can still recover what it promised | `report.record` keyed on (event, submission); `report.read_all`; `cli.cmd_show` | implemented |
| I-114 | Every delivered message states its report submission and survives elision doing so, so the frozen bytes identify which stored submission produced them | `report.render_completion`, `render_revision`; the identity is its own section with a floor covering it | implemented |
| I-115 | A report with no pull request still renders its base, head and criteria digest rather than dropping them unannounced | `report._commit_lines` | implemented |
| I-116 | An attempt that is proven never to have sent is not counted as a delivered submission; anything unproven is | `report._may_have_reached` | implemented |
| I-117 | A manifest reference too long to render is truncated visibly rather than making the event unsendable | `report._manifest_ref_lines` | implemented |
| I-118 | A submission number is a positive integer or a named refusal, never a coerced one, because it is half the identity and is printed in frozen bytes | `report._submission` | implemented |
| I-119 | An exit code is an integer or absent, so evidence a reader cannot interpret is refused rather than delivered | `report._exit_code` | implemented |
| I-120 | An inbox-only attempt counts as having reached the recipient, because its frozen message is the durable inbox item | `report._may_have_reached` | implemented |
| I-121 | No report value, top-level or nested, may contain a line break, so nothing can splice an extra line into the message protocol | `report._single_line`, applied to fields, evidence, unresolved, findings and restore | implemented |
| I-125 | A blocker count too large to render is refused, because it lands on a line the message cannot shorten | `cxc.verdict_line` | implemented |
| I-126 | A required report field must be text, not a value coerced through `str()` into a Python repr | `report._required` | implemented |
| I-127 | A line break is anything `str.splitlines` treats as one, so a separator other than CR or LF cannot splice a line either | `report._single_line` | implemented |
| I-128 | A correction renders the unresolved items the report marked open, rather than storing them unseen | `report.render_revision` | implemented |
| I-129 | A pull request number outside what the store can hold is refused, not left to raise `OverflowError` on insert | `report.record` | implemented |
| I-130 | A finding disposition is one of the frozen criteria values, checked rather than passed through | `report._disposition` | implemented |
| I-131 | The verdict parser and the verdict renderer accept the same language, including the blocker ceiling | `cxc.parse_verdict_line` | implemented |
| I-132 | Every producer-supplied integer is inside what the store can hold, and its refusal never tries to print an unprintable value | `report._submission`, `report.record` | implemented |
| I-133 | A blank evidence entry is refused, because an empty verification line is not verification | `report._check_evidence` | implemented |
| I-134 | A finding that exists only to enrich an authoritative one needs no disposition of its own | `report._disposition` | implemented |
| I-135 | A line value that cannot be encoded as UTF-8 is refused where it is recorded, not where it is measured or sent | `report._single_line` | implemented |
| I-136 | A correction carries the CXC status and its reason, like a completion does | `report.render_revision` | implemented |
| I-137 | One criterion carries one finding; a duplicate id is refused rather than silently replacing the first | `report._check_review` | implemented |
| I-138 | Shortening carries a running byte total rather than recounting, so it stays linear inside the claim transaction | `report._compose` | implemented |
| I-139 | Fixed protocol prose is never shortened away, because the advertised command returns records and not template text | `report._preserve_lines` | implemented |
| I-140 | Every producer-supplied text field is required to be text, never coerced through `str()` into a representation of itself | `report._required`, `_text_or_none`, and the evidence, unresolved and finding checks | implemented |
| I-141 | An exit code is a number a process could have exited with, so it can always be serialised | `report._exit_code` | implemented |
| I-142 | An unrenderable receipt manifestRef is reported as present rather than blocking the delivery, because the receipt is contract-validated and the recipient is not at fault | `report._manifest_ref_lines` | implemented |
| I-122 | Pull-request fields are refused when no pull request is named, rather than stored and never rendered | `report.record` | implemented |
| I-123 | Every restore field is a supported, bounded, single-line string; an unsupported or unrenderable one is refused | `report._check_restore` | implemented |
| I-124 | A submission must clear both floors, the delivered one and the highest stored one, so no write is accepted that nobody would ever see | `report._assert_resubmission` | implemented |


## Recorded limits, so a row above is not read as more than it is

Expand All @@ -112,3 +170,5 @@ status. Every row below is implemented and carries a test; the suite is the proo
| The JSON date-time format is unvalidated | the available validator has no working format checker, so timestamp format is unverified rather than implied |
| Terminal turns are polled, not subscribed | the transport cannot subscribe, so automatic invocation is a bounded poll that then dispatches |
| Archive state can be unknown | an inconclusive listing withholds rather than guessing, and a later observation releases it |
| A head commit is not observable from here | the relay cannot watch a forge, so `assert_current` enforces generation on the delivery path and takes `head_sha` only from a caller that already knows the current head. A push that changes the declared manifest is structurally a new event, because the revision hash and therefore the event id change with it; a push that changes nothing declared is not, and `_check_resubmission` is what stops an old report standing for it silently |
| `work_reports` ships with its composite key | the schema is applied with `CREATE TABLE IF NOT EXISTS`, which never reshapes an existing table, so a store created from an intermediate revision of this change that used an event-only key cannot hold a second submission. No released version has this table, so there is nothing to migrate; a store built from such a revision is recreated rather than upgraded. The write itself no longer names a conflict target, so it does not depend on which revision created the table |
Loading
Loading