CRW-165 · 부모를 목표 없는 릴레이 이벤트 실행으로 전환 - #90
Merged
Merged
Conversation
A project parent no longer opens a native goal. It ends its turn when nothing but waiting remains, and a delivered relay event starts the next one. The issue child keeps its own goal and CXC Loop unchanged, and crw-loop becomes the opt-in for a parent goal somebody actually asked for. The recorded start policy splits one field into two, because one value was carrying two answers: run_mode says whether a goal exists, observation_path says what brings the parent back, and a pairing matrix declares which combinations are legal. A parent may wait idle only on five recorded readiness facts, and the fifth requires a wake already observed on this operating scope - by this parent earlier or by a probe recipient - because the obvious version of that fact is circular. New OPS-8.5 owns the wake path: dedupe by relationship, generation, revision and event id; apply in generation order with a superseded event recorded rather than dropped; a per-relationship marker that advances only behind a durable apply; and the separation of a send accepted from a turn that ran from an acknowledgement from an applied change. It also records two busy paths that behave oppositely. A recipient observed active before the claim is deferred without consuming an attempt and is retried indefinitely. A recipient that observed idle, was claimed, and then returned busy consumes one, and exhausting that budget holds the delivery terminally, which is the concrete form of a queue nobody is woken for. Goal status is documented as an input to deliverability, which changes the transition for a parent that already holds a goal. Pausing looks like the supported move and is the one that strands it: the relay treats a paused recipient as undeliverable, so the events stop arriving and nothing reports a fault. The supported routes are honest completion at the scope boundary, or a carried-over active goal, or blocked where the host's threshold is genuinely met. The pause guard itself is untouched.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
…dicting the new default Review found four things. A revision hash carries no order, so "apply by generation then revision" could make a parent pick one of two forked candidates and advance its marker past the other. Ordering now comes from the declared lineage and the current-head determination, and where that yields no single head the parent applies nothing, advances nothing, and routes the ambiguity. The no-goal precedence sentence and S26 still said no-goal leaves the run blocked without separate authorization, which contradicted the default they sit beside; the same request would run or stop depending on which clause was read first. No-goal now bars only an explicitly requested Loop. The midpoint table inferred that a Loop parent without a goal receives nothing. An absent goal is deliverable, so a mode mismatch is reported as one and the delivery path is read separately. A paused goal is the state that actually receives nothing. OPS-8.5 now says plainly that it binds the parent's behaviour and is not enforced by the installed relay.
…ne status The previous fix narrowed undeliverability to a paused goal, but the relay blocks three statuses and OPS-8.2 already classifies them. A checkpoint following the narrower text would keep investigating the delivery path for a parent whose block is already known. It now reads the classification from its owner rather than restating it, so the two cannot drift apart again.
…ooks like waiting Four surfaces the first pass did not reach. The restoration block was written for a task bound to an issue, so a block travelling to a parent carried nothing about how that parent waits; a goal-free parent that ended its turn cannot tell on restart whether that was a decision or an interruption, and without the mode, the path, the readiness and the outstanding event state it either resumes work that already has an owner or waits for an event already handled. relay.md described delivery phases but never the exhausted budget: a held delivery is excluded from eligibility, no supported command clears it, and its recovery is a fresh generation. It looks like waiting from every reader that does not check holdReason. A completion check had no way to tell an intentionally idle goal-free parent from a stalled one, so it now reads the parent's idleness and its delivery path as two things. And two claims in integrations.md still called the parent goal the role default, contradicting the same file's own goal-free statement a few hundred lines away.
An evidence run that points at its own state directory isolates its records and nothing else: it still resolves the shared scope authority, so the daemon it starts is the real service and every rule about not ending another parent's service applies to it. Nothing said so, and the reasonable reading was the wrong one. Isolating the scope is a separate explicit act - override the scope directory and pass the flag - and it still reaches the host through the same App Server socket, so sharing a socket and sharing a scope are different facts. An isolated authority is a scope of its own rather than an exception to having one, so exactly one service runs in it and a second start is refused on that store's lock. Measured here on 2026-09-21 rather than reasoned about: the isolated run registered in its own scope root, ran its bounded ticks and released while the real service kept its lock, and a concurrent second start was refused. Ticks against an empty store are tick progress, not delivery progress, and the clause says so.
Review found the qualification keyed to the wrong thing. State selection and scope selection are independent, so a run reading a scratch store without the scope override is still the shared scope's service and cleaning it up early is exactly the act the shared lifecycle forbids. Outright ownership now follows an isolated authority, and reaching a configured bound is kept separate from an owner-initiated early stop. S37's first case could not happen as written. While the real service holds the scope, a different-store start is refused as owned by another store: no ticks run and there is nothing to clean up, so a run claiming it exercised delivery there claimed something that did not occur. The scenario now splits on who owns the scope rather than on which store was selected.
OPS-3.6, the OPS-4.1 qualification and S37 were added at the end of this branch and are not part of what CRW-165 asked for. Two review rounds produced five findings against them and three are still open, all in the same area: what an isolated authority actually owns, what a stopped registration does to an evidence start, and which assignments a scratch daemon serves. They are worth landing, but not here. This PR's result is the goal-free parent transition, which was complete and clean at 0907c3c with every check green and every thread resolved, and holding it open behind an unrelated clause serves nobody. The measurement that motivated the clause is preserved outside the repository and loses nothing by waiting.
OPS-8.5 asserted behaviour that had only been read out of the source. Four of its claims have now been exercised against real recipient tasks on an isolated store, so the clause carries a measured subset in the form OPS-8.3 already uses, with the limits stated: it measures the relay and its store, not the parent-side drain and marker rules, which stay unenforced. The cap hold was induced in a synthetic store rather than reached naturally, and only paused was exercised among the protected states. Recovering a held delivery also turned out to have no reason of its own. The available reasons describe an initial assignment and a needs-changes revision, and a hold recovery is neither, so the record afterwards reads as a correction round nobody performed. The clause now says so and asks for the real reason to be kept beside it.
The Stop-continuation section named completing the goal or recording it blocked as the honest exits without anyone having watched either happen. A trivial test actor holding an active goal with nothing to do reached blocked within its first turn, with no further external prompt, then held flat; two goal-free actors in the same harness held flat with even their updatedAt unmoved. The paragraph says only that. It does not attribute the transition to the Stop behaviour, because the hook's firing was never read and only the outcome was observed. The token figure covers a whole first turn rather than an isolated idling cost, and one trivial actor leaves frequency, necessity, real project-parent behaviour and any saving unmeasured.
The measured paragraph qualified "the token figure" without ever stating one, so the caveat pointed at evidence the reader could not find. The figure is 2127 tokens, and it is the actor's whole first turn including creating the goal and answering its prompt.
This task's own completion receipt sat at withheld_pre_send with role_policy_unconfigured while every readiness fact it had written passed. Fact 2 asks whether the service's ticks progress, and they did. Fact 3 asks whether the recipient is reachable, and it was. Fact 5 pinned the scope's identity without pinning the configuration the service decides under. A parent following that gate would have yielded idle into silence. Fact 2 now also reads the store's current failure records and treats an applicable service-side pre-send refusal as a readiness failure, with the explicit caveat that their absence proves nothing on its own, since a store with nothing queued has no failures to show. Fact 5 adds role-policy identity, because a wake observed under a different policy is evidence about a different configuration. OPS-8.5 names the observed refusal beside the cap hold, scoped to what was seen rather than to every cause of withheld_pre_send, and records that its recovery differs: it is retry-safe, so making the configuration readable and restarting resumes the existing delivery with nothing lost.
Recording a role for a task quietly changes what delivery to that task requires: an unbound recipient is delivered to without a role check, a bound one is checked against the policy the delivering process can read, and a process that can read none refuses before sending. So a binding written without that policy declared where the service reads it stops delivery to that recipient while both conditions hold. OPS-7.1 now says to write them together and treats a binding as incomplete until the delivering process can resolve the policy it just made necessary. OPS-8.5 gains the observation behind it. One store's two role bindings were written at 07:43Z, every acknowledged delivery it holds settled at or before 06:35Z, and the one delivery afterwards to a bound recipient was refused. The timeline corroborates rather than proves, since those earlier deliveries predate the bindings and were never candidates for the check; the claim rests on the branch itself.
…rased them Two parent actors read this contract from an installed tree and behaved exactly as it says: no goal, ended the turn, waited idle, with the right reason. Both then reported field values that do not exist - goal_free and relay_only against the declared goal-free-run and event-driven-idle - and the second had the defining section in front of it and had been asked for exact values. These two fields are not labels. They are the keys the pairing matrix is checked against and the keys a later session restores from, so a value that reads correctly but is spelled differently is a record the next reader cannot match, and the restore it was written for silently becomes a fresh adjudication. The declaration now says they are closed vocabularies recorded verbatim, names the observed paraphrases as the failure it prevents, and the restoration item in the task packet links to it so a consumer can check the spelling where it is used.
I wrote that sentence and then tested it. A third parent read the corrected file and wrote run_only/relay, after the first two wrote relay_only/relay and goal_free/relay. All three observation_path answers were the same wrong value. The third had this very paragraph in front of it. So the requirement stays, because a producer should still write the literal, but the claim that it prevents the failure is withdrawn. The text now says what was measured: three readers, three paraphrases, one of them after reading the fix. It also tells a consumer to match tolerantly rather than assume the record is canonical, which is the half that actually protects restoration today.
Review caught that tolerant matching is an undefined normalisation layer: with no alias table, two readers can restore two different policies from the same invalid record, and relay_only is exactly that case - it names an observation path while sitting in run_mode, so the obvious reading disagrees with the field it was written in. A value outside the declared set is now an unreadable field, re-adjudicated the same way a missing one is. That is deterministic and leaves a record saying so, where guessing is neither. The writing-side problem is unchanged and still unsolved; this only stops it becoming a silent divergence on the reading side.
I shipped that rule and then tested it. A reader given a record containing run_mode: relay_only said the values were settled and would be restored and used as they stood - the exact behaviour the rule forbids. That is three interventions measured on one problem: literals asked for in prose, literals offered as a list to copy, and a consumer required to reject anything outside the set. None changed a reader's behaviour. The rule stays, because it is what a correct implementation does and because tolerant matching is worse, but the file no longer implies that stating it makes it happen. These two fields need validation in code where they are written and read, which a document cannot supply.
The two closed-vocabulary fields had no machine reader, so a paraphrase could only be caught by eye. start_policy.py parses the pairing table in start-policy.md rather than keeping its own copy of the literals, and answers from it: vocabulary prints the legal pairings as lines to copy, check exits non-zero on an out-of-set value or an illegal pairing, selftest carries the four recorded paraphrases as must-reject cases. scripts/ci/contracts.py runs the selftest, so the script and the table cannot drift apart unnoticed. Four fresh readers of the current text then wrote in-set values every time, two of them on copies with the wrong-value examples or this checker's own paragraph deleted, and none of them ran the check. So the contract no longer claims prose cannot carry these fields, and it does not claim the script is why records read correctly; it records that the failure stopped reproducing and that what changed is unidentified. Evidence: /state/crw/crw-165/artifacts/c2-readback-nonreproduction-20260921.md
The consumer rule said an unreadable value is re-adjudicated under the trigger list, but the list enumerated only changes in the world, so a reader following the restoration section found nothing changed and restored the unreadable value as it stood. Two consumers of one record could resume the same parent under two different policies, which is what the record exists to prevent. The missing case is now stated where a consumer looks for it, and it is checked even when every valid_while identity still holds. Reported by Devin on PR 90.
Case 7c covered every unsupported explicit Loop, including the narrower 7a where the user declined goal-free Run, and the table declares no first-match order, so two readers could answer one input with zero children or with D. 7c now names the not-declined case it was always meant to cover. crw-run promised no automatic future wake-up without qualifying it, which contradicted the event-driven-idle path the same file now prescribes. The sentence is qualified by the recorded observation_path. The checker kept the first occurrence of a repeated field, so a stale pair above a current one passed as the first legal pair while another consumer read the later value. It now keeps every occurrence and calls two different values unreadable; the same value stated twice is still readable and says so. It also stripped decoration and trailing punctuation in one pass, so a markdown value such as `loop`, kept a backtick and failed; undecorating now repeats until stable. The selftest's second copy of the vocabulary stays, with the reason written down: the table decides what check accepts, and that line is what makes a rename deliberate instead of silent. All four reported by Devin on PR 90.
str.strip over a set of marker characters removes them independently from both ends, so loop_ became loop and observation_path_ became the field name. That is the checker performing exactly the approximation the contract forbids a consumer to make, and it turned a malformed record into a legal pair. Only a wrapper that opens and closes with the same character comes off now, so backticked, quoted and bold values still read while an unmatched or mixed marker stays in the text and fails membership. Bullets are removed by a marker followed by whitespace rather than by stripping asterisks, which keeps **run_mode** intact. Reported by Devin on PR 90. Selftest covers the unmatched marker, the mismatched pair, the decorated field name and the bold value that must still pass.
A wake observed while the parent was loaded does not establish the route a long wait actually uses. An idle recipient is resumed with its own settings, while one the host reports notLoaded is resumed only when its pair derives from the role's declared pair, so the two take different transmission paths and only one of them was ever proven. A parent that waits long enough is evicted, which makes the unloaded route the one event-driven idle depends on. Both routes are now observed. A parent idle since 19:05 read notLoaded after three hours, and a queued event delivered by the daemon created the turn whose id the attempt record names, with recipientStatusBefore notLoaded. Eviction is not a deliberate process restart and the parent-side drain is still not exercised; that scope is recorded with the evidence. Evidence: /state/crw/crw-165/artifacts/c4-notloaded-wake-20260921.md
…state Adding load state to fact 5's match list was the wrong rule. Readiness is adjudicated while the parent is still loaded, so matching the state observed at that moment accepts a loaded wake as proof — and the wait itself is what invalidates it, because a wait long enough to be worth making ends in eviction and the next event takes the notLoaded route instead. Fact 5 now asks for the load state the wait ends up in. Where eviction is possible, which is every ordinary wait, event-driven-idle needs a notLoaded wake on the same scope; a loaded wake alone leaves the fact unmeasured and the path stays active-observation. Both wakes are now observed, so the stricter rule is satisfiable rather than theoretical. Reported by Devin on PR 90.
The probe exists so a parent does not have to gamble its assignment on a wake it has not seen, but an unqualified probe proves the probe's route rather than the parent's. The relay derives an unloaded resume's pair from the role's declared pair and refuses a record-based or exception-authorized one, so an ordinary parent-role probe waking from notLoaded says nothing about a parent running under a named exception; that parent is refused exactly where the probe succeeded. A probe now stands in only where it matches the role and the pair provenance. Where nothing can match, because an exception-authorized pair has no ordinary equivalent to probe with, the parent has no non-circular evidence for its own unloaded route: fact 5 is recorded unmeasured with the provenance that made it so and the path stays active-observation. Reported by Devin on PR 90.
attempt() resolved the relationship with registry.get() and never looked at its status, so for an assignment somebody had paused, cancelled or archived it read the host three times, wrote a recipient_lifecycle row calling that recipient deliverable, and then returned a bare None once _claim refused. Nothing was ever sent - the scheduler excludes those rows and the claim repeats the predicate atomically - but nothing recorded why, and None is indistinguishable from a delivery whose time has not come. The status is now checked before any host read, and the refusal is journalled with the status it refused on. It is deliberately not a hold_reason, because resume() lifts all three, and deliberately not a failed_operations row, because somebody stopping their own work is not a service fault and a parent reads this scope's failures to decide whether it may wait idle. The UPDATE re-checks the observed status and superseded_by in its own transaction, so a resume landing in the gap cannot be overwritten by a stale reading. Superseded relationships are excluded on purpose. They are deactivated too, but permanently, and closing that terminal means settling how every operator-facing reader renders it. A tick that withholds this way now counts a deferral rather than a skip, because quiet folds deferred and not skipped, and a tick reporting itself quiet after persisting a withhold is the evidence a parent reads before agreeing to wait.
OPS-8.2 named a paused, cancelled or archived TASK as one guarded class, which promoted an assignment status into a recipient state the host does not report, and the measured note claimed cancelled and archived "rest on the same code path" as paused. They do not: archived is its own earlier lifecycle branch, and a cancelled recipient task has no branch at all. The clauses now separate the two levels and say how each refusal is recorded. On the service's own path a deactivated assignment is enforced by exclusion and nothing is written, so a queued delivery under a cancelled assignment is not a stalled one; the assignment-level report is what says abandoned or paused. A direct attempt, and the narrow race where an assignment is deactivated after selection, refuse before any host read and journal the reason.
The recorded git trees for codex-session-relay still described the tree before the delivery change, so verify-definition passed locally only because the fix was sitting uncommitted beside it and failed on the pushed head. Recorded from HEAD instead. Two sentences also claimed more than the code does. A withheld delivery IS recorded as a failure when the recipient's own state caused it - _withhold writes a lifecycle_read record - and only an assignment-status withhold skips that, which is the distinction a parent reads failure records for. And the journal kind is delivery_withheld_inactive carrying the relationship and its status; relationship_not_active is in the returned record, not the journal.
…e-parent # Conflicts: # scripts/crw_runtime/components.json
The relationship check sat after the next_eligible_at guard, so a delivery that was eligible by state but waiting out a retry - a busy-deferred one, say - went back to returning a bare None once its assignment was stopped. An operator naming that event during the wait learned nothing until the timer expired, which is the same silence this change set out to remove. The check now runs before the timer. Recording the status never brings an existing retry time forward: whatever set it had its own reason, and a deactivation is not a reason to retry sooner.
The trees are git object ids, so they only settle once the change they describe is committed. Recorded from the commit above rather than from a working tree that still had it uncommitted.
…e-parent # Conflicts: # scripts/crw_runtime/components.json
Comparing against a next_eligible_at read before the transaction let two overlapping refusals race: the second one could overwrite an extension the first had already committed, and a resumed delivery would then retry before the newest backoff expired. The statement now takes MAX of the recheck floor and the row's own current value, so whichever deadline is later survives whatever order the callers arrive in.
The re-adjudication clause requires the superseded value to be kept beside what replaced it, and the checker collected every occurrence of a field and rejected any field holding two different values. So a record that obeyed the contract was reported as unreadable, which is precisely the drift this checker exists to stop. The second entry is now spelled run_mode (superseded), declared in the contract and recognised by the reader rather than skipped by accident. The ambiguity guard is untouched: an unqualified duplicate is still two current values and still fails, and a known field under any other qualifier now fails too, so an invented word cannot keep a second current value out of the reader's sight.
Lowercasing and trimming the parenthesised note accepted run_mode (SUPERSEDED) and spacing variants, so this checker would pass a record that a consumer enforcing the declared spelling rejects. Two readers, two policies, from one record - the thing this script exists to prevent. The comparison is now against the whole canonical string, and every other qualifier goes down the existing unreadable branch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A CRW project parent no longer holds a native host goal. It ends its turn when nothing but waiting remains, and a delivered relay event starts the next one. The issue child keeps its own goal and CXC Loop unchanged, the initiative supervisor was already goal-free, and
crw-loopbecomes the opt-in for a parent goal somebody explicitly asked for.Implements the 2026-09-21 decision in CRW-165, superseding the 2026-09-20 role decision that made the parent goal the default. Both superseded steps are kept in the lifecycle document so a reader meeting a parent that still holds a goal recognises it as carried over rather than as a second valid answer.
What changes
One recorded field became two.
goal-free-runpreviously meant both "no goal" and "no automatic resume is claimed or proven", which makes the new default self-contradictory if the name is simply promoted.run_modenow answers only whether a goal exists;observation_pathanswers what brings the parent back, takingevent-driven-idle,active-observationorblocked. A 3x3 pairing matrix declares the legal combinations:run_mode: blockedpairs only withobservation_path: blocked, andloopwithevent-driven-idleis legal as a compatibility mode reaching idle only after its bounded Stop budget releases, at a turn per block.A parent may wait idle only on five recorded facts: a registered assignment routing to it, a delivery service whose ticks are actually progressing, its own reachability including goal status, delivery coverage for every disposition it waits on rather than completion alone, and a wake already observed on this operating scope. The fifth is written against a probe recipient because the obvious form is circular — a parent cannot observe its own first wake without first yielding idle on the strength of the observation it has not made.
New OPS-8.5 owns the wake path. Dedupe by relationship, generation, revision and event id. Order by generation and the declared revision lineage, never by arrival time or by comparing hashes; where no single current head exists the parent applies nothing and routes the ambiguity. The per-relationship marker advances only behind a durable apply. A send accepted, a turn that ran, an acknowledgement and an applied change stay four facts. The clause states that it binds the parent's behaviour and is not enforced by the installed relay.
The two busy paths
Reading
delivery.pychanged this contract. Where the recipient is observed active before the delivery is claimed, the attempt is deferred without being consumed and the row is re-observed indefinitely — a parent that simply stays active keeps its deliveries. Where the recipient observed idle, the delivery was claimed, and the transport then returned busy, an attempt is consumed. Only that second path approaches the cap, and exhausting it holds the delivery terminally: held rows are excluded from eligibility and no supported command clears the hold. OPS-8.5 names its detector, owner and recovery — a fresh execution generation — and states plainly that nothing detects it automatically where the parent is never woken at all.Goal status is an input to deliverability
The installed relay treats
paused,usageLimitedandbudgetLimitedas blocking. An absent, active, blocked or completed goal is deliverable once the task reads idle. Verified against the installed runtime, byte-identical to this checkout for that module.This reverses the transition that looked obvious. Pausing an existing parent goal to reach the new default makes the parent permanently undeliverable: events withheld, nothing waking it, no fault reported. The supported routes are honest completion at the scope boundary, a carried-over active goal until that boundary, or
blockedwhere the host's own threshold is genuinely met. The pause guard is untouched — it protects a task a person deliberately stopped.The vocabulary readers kept paraphrasing
run_modeandobservation_pathare closed sets, and a paraphrase is not a restorable record: the next session either matches the literal or silently re-adjudicates. Four readers of the earlier revisions wroterelay_only,goal_free,run_onlyandrelay_onlyforrun_mode, andrelayforobservation_pathevery time. Asking for the literals in prose, offering them as a list to copy, and requiring a strict consumer were each measured, and none of the three changed a reader's behaviour.crw-run/scripts/start_policy.pymakes the enumeration runnable rather than only declared. It parses the pairing table instart-policy.mdinstead of carrying its own copy, so a renamed literal cannot leave the checker and the contract disagreeing.vocabularyprints the legal pairings as lines to copy;checkexits non-zero on a value outside the set, on one field recorded with two different values, or on an illegal pairing;selftestcarries sixteen cases including all four recorded paraphrases.scripts/ci/contracts.pyruns the selftest, so the pin is enforced by the gate.Then the failure stopped reproducing, and not because of that script. Four readers of the current text wrote in-set values every time, two of them on deliberately damaged copies — one with every previously wrong value deleted from the page, one with the checker's own paragraph removed — and none of the four ran the check. So the contract no longer claims prose cannot carry these fields, and it does not claim the checker is why records now read correctly; it records that the failure stopped reproducing and that what changed is unidentified.
Files
Thirteen skill documents:
crw-run/SKILL.md,crw-loop/SKILL.md,crw-check/SKILL.md,parent-goal.md,start-policy.md,operations.md,scenarios.md,integrations.md,task-packet.md,relay.md,midpoint-check.md,dispatch-verification.md,initiative-supervision.md,reevaluation.md.Beyond the default itself, the later commits close surfaces the first pass missed: the restoration block now carries what a restarted goal-free parent restores,
relay.mdnames the terminal hold beside the readers that surface it, andcrw-checkcan tell an intentionally idle parent from a stalled one.One new file,
plugins/crw/skills/crw-run/scripts/start_policy.py, and one entry added toscripts/ci/contracts.pyso the gate runs its selftest. No package, wiring or installer change:plugins/crw/wiring,scripts/install.pyandscripts/runtime_install.pybelong to CRW-178 in flight and are untouched, and CXC source and its installed cache are not patched.A scope-isolation clause (OPS-3.6 and scenario S37) was added late and reverted in
b2882b1, verified byte-identical to0907c3c. It was outside this issue's scope, drew five review findings, and is better landed in its own pass; the measurement motivating it is preserved outside the repository.Validation
check_operations_contract.py: 79 clause ids, 49 normative, 36 scenarios, 59 distinct citations, every normative clause cited by a fixture.validate.py: 10 skills, local link paths and Python syntax.plugin.pyderives permitted roots from the manifest with no violation.git diff --checkclean.start_policy.py selftest: 16 cases, the four recorded paraphrases and the malformed-record cases all rejected.Two independent review rounds ran against the plan before any contract text was edited. Devin then reviewed every head, including a real defect where ordering by revision hash could have made a parent apply one of two forked candidates and advance its marker past the other, an overlap where one case row would have created children that another forbade, and a checker bug where
loop_normalised into the declared literalloop. All nineteen review threads are resolved, each by a fix rather than a waiver except one kept deliberately with the reason recorded at the line.Not covered by this PR
The live evidence CRW-165 c2–c8 asks for is not in this diff and is not implied by it: the real event wake, the resilience matrix across paused, blocked and absent goal states, and the fixed idle-window token measurement. Of c4's eight cases, seven have offline tests that ran green in the
packagesjobs; the terminal-hold case has no end-to-end coverage and cannot be reached by waiting, so it must be induced.One follow-up is returned as a proposal rather than built: a recipient-scoped pending reader and a supported release for a terminal hold, identified as the gap between this contract and fully automatic recovery.