fix(ops-mission-control): never publish the incident index or app config over a failed read - #7794
Conversation
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound root-cause fix; the round-trip refusal rule quietly makes serializer idempotence and schema stability load-bearing availability constraints humans should ratify. Watch
[DESIGN-REVIEWED] 6d0bcd4 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsFINDING -- src/kiro_crew/apps/builtins/ops_mission_control/backend/routes.py:1029 -- False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All checks are done. The change decomposes cleanly, every item traces to the named data-loss cause or to an AGENTS.md-mandated invariant (coded non-2xx bodies), the sibling count is declared and tracked, and the one piece of new surface ( First-Principles-Verdict: PASS One cause — a lenient read used as the base of a whole-file rewrite — fixed at the reader contract, with every rider declared and derived from it. What this change shipsIntent: stop one transient read failure from silently deleting every incident and every provider setting on disk — a FIX.
The four unfixed siblings of the root cause ( [FIRST-PRINCIPLES-REVIEWED] 6d0bcd4 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsBoth candidates fail the falsification bar. Candidate 1 (strict round-trip wedge on a non-string label): I confirmed Candidate 2 (hygiene route answers an uncoded 500 on corruption): the No grounded self-originated finding survived falsification. No findings. [OPUS-REVIEWED] 6d0bcd4 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
70d0d8d to
ad6808f
Compare
Round 2 dispositions -- #7794Pushed Design Review -- CONCERNS, the guard is bypassed on any busy installReal, verified, and the most useful finding on either PR. Confirmed at And the transient case is worse than the persistent one, as you said: a signal claimed earlier in Fixed as suggested -- the claim loop catches Your critique of my tests was the part I most needed.
Suggestion: log the silent display degradations -- taken. Both display readers now split the GPT 5.6 -- no blocking; FINDING on the guard commentsCorrect, and fixed. My comments said "the index refused to be READ", but First Principles -- PASS
Opus 4.8 -- no findingsNoting one thing for the record: Opus considered the claim-loop candidate and dropped it as "a comment App suite |
ad6808f to
8da8007
Compare
Diagnosis of the one edited test, and a property it led me toPushed The lock's shape is unchanged, and the diff is the evidence. Across all six mutations in The read stays inside the lock, the write stays inside the lock, the span is identical, and I repointed that literal and added But the challenge did surface something real, and it is the better find. The strict read makes That matters more than an ordinary regression would:
One process note, for the record: I reported this test as "added and passing, mutation probe not |
8da8007 to
7b20738
Compare
7b20738 to
6009622
Compare
Round 3 -- lint fixed; the new BLOCKING finding is a false positive on its premisePushed
|
Design's sidecar suggestion is the right answer, and it holes my own rebuttalAccepting the finding and deferring the change -- reasoning below, because the deferral is a scope
This settles a standoff that had been running for two rounds. GPT filed the same underlying concern as But my rebuttal also asserted that corruption has "nothing left to lose by replacing it," and that What makes the sidecar better than either previous position is that it separates the two things GPT's Deferring it, and this is a scope call, not a judgement on the finding. This PR is being closed out Recorded in #7789 alongside the three remaining sites of this class, as the preferred resolution to the |
Two First Principles items, dispositioned without a push -- and the head is now frozen
Why the head is frozen, since this is my error to ownI pushed three heads in about twenty-five minutes. Each restarts twenty-seven checks and re-rolls five The
|
Design's second Watch item is a real coupling I chose, and I am escalating rather than patching itAll four lanes have now judged the frozen head The first item (the exception hierarchy) was already dispositioned; Design added a third option I had not The second item is new, and Design is right that it wants a human:
The coupling is real and I did choose it. The hygiene cron runs pull -> hygiene -> index -> That is worse than it sounds in one specific way. The argument I made for refusing corruption everywhere There is a clean fix and I am not making it under the freeze. The order is I am not doing it in this PR for two reasons. It is a behaviour change to the cron's step order rather So it goes to Raymond as a decision, which is what Design asked for. Three ways forward, in my order of
Filed as #7790's sibling rather than folded in here, and flagged in the summary so it is a decision |
9e29e4c to
dca487d
Compare
GPT's blocking finding is real: a third shape door, one level below the two I closedPushed Worth stating up front how this verdict arrived, because it matters for reading it: GPT reported no The blocking one: nested fields are coerced, not rejected
So a row whose The part I should own: I had already found the enabling fact and drew the wrong conclusion from it. Strict mode now rejects a nested field that is PRESENT with the wrong type. Absent stays legal, because The non-blocking one, which was also a false claim in my descriptionGPT is right that structural corruption reached VerificationTwenty-five mutation probes. The two new ones:
A third test pins that an ABSENT nested field is still a normal record, so the new refusal cannot creep App suite Where this leaves the headFrozen again at |
The guard was enumerating shapes when the invariant is about content -- replaced, and the reader got smallerPushed The diagnosis I should have made two rounds agoThree review rounds found the same class of loss, one layer deeper each time:
I fixed each one where it was found. All three fixes were the same mistake: they enumerated shapes GPT's remedy ("reject malformed nested structured fields before deserialization") is enumeration one The structural rule
The reader is smaller than it was a round ago, which is the honest signal that this is the rule Verification, and the evidence that it generalizesThe proof is not that the new tests pass; it is that the OLD ones still do. Every test written to pin Then the probe: disabling the one equivalence check reds four things at once -- App suite BodyThe mutation table row now names the equivalence refusal and what its redness means: an incident whose I also deleted the sentences describing the three shape guards, because those guards no longer exist and Body is 7,991 bytes, under the 8,000 truncation point, so a reviewer can still read all of it. GPT's advisory about the display paths degrading without logging landed in the previous push and is Frozen here. |
Design found a real bug in the structural rule I just landed: it read a NEWER file as corruptPushed The defect
My round-trip carve-out ignored keys the trip ADDED -- old record, new code -- and had no counterpart The fix keeps the refusal and corrects the taxonomyDesign's framing is what I implemented, because it is right: refusing to strip the unknown field is The equivalence check now separates two questions:
Verification
Mutation-verified: folding App suite The other itemThe Frozen at |
Conceding a false justification: I cited a topology this app explicitly forbidsPushed First Principles caught me justifying
|
Design's migration point is right and sharper than stated; FP's is a stale-body read with one live sub-pointPushed Design: the round trip outlaws migrate-on-writeCorrect, and the consequence is worse than "outlaws" -- it MISREPORTS. Renaming or retiring a field means Two changes, no new machinery:
I am not adding the carve-out mechanism now. There is no migration to serve, and a speculative allowlist First Principles: declared after their capture, and the sub-point answered on evidenceThe "undeclared rider" half was true when the lane captured the body and is not true now -- reviewer bots The live sub-point is the measurement: "0 hits for App suite |
GPT is clear on
|
Opus found a real uncoded 500 on a committed claim; its remedy would have misreported itPushed The gap
Why I did not apply the suggested fixThe suggested fix was to extend the same This is the same shape as GPT's remedy at So the annotation degrades instead. The ledger match is the deferrable half -- the dispatch cycle Mutation-verified both ways. Removing the guard reds the new test; applying the remedy Opus suggested The two CONCERNSDesign: the change installs two convention-only invariants -- exception-arm ordering and serializer First Principles: corruption-refusal is a point patch beside four counted lenient siblings (that is the App suite |
First Principles is right that the count is five; four of them can lose dataNo push this cycle -- both lanes are CONCERNS with standing items, and the one checkable new claim is a The fifth siblingI have said "four merged siblings" throughout. Enumerating every
So five readers share the shape and four share the harm. The description now says both numbers instead of Why the fifth is exempt, structurally rather than by argument. One thing I should have found earlier. The standing itemsDesign: two convention-only invariants (exception-arm ordering, serializer idempotence) that future code First Principles: the version-skew apparatus has one consumer and a trigger unreachable in this build. Zero failures on this head; 24 checks still in flight. |
All four lanes clean; GPT's advisory was a real hole and wider than it statedPushed Invalid UTF-8 bypassed every corruption clause
It was wider than the finding said, and the test is what showed me. GPT pointed at the strict readers. Mutation-verified: leaving The second advisory was another over-claim of mineGPT: Narrowed rather than "fixed", with the reason stated: the display read logs the degradations that explain a Standing items, unchangedDesign: two convention-only invariants (arm ordering, serializer idempotence) -- on #7805 with concrete App suite |
Terminal disposition: no blocking findings, and the two remaining items are for a humanHead stays at Why the loop had to be stopped rather than finishedEvery push re-rolls five review lanes on a new head, and each lane reliably produces one more advisory on Both lanes have now converged on repeating the same two items, and Design's own framing is the tell:
That is a request for a maintainer, not for another commit from me. The two standing items, and where each is recorded1. Two convention-only invariants (Design). 2. Two riders on inherited justifications (First Principles). The named exception type no catcher State
Whatever is decided on the two items above, this change does not depend on it: the invariants it installs |
Reversing my own freeze for one three-line fix, and saying whyPushed What it was
The last line of the function was It is also asymmetric with a fix I already made: the index display read grew exactly this warning one head Why it outweighed the freezeThe freeze exists so the head stops moving and 66 checks can settle. That is worth real money and I broke Three lines and a test. Mutation-verified: demoting the warning to Standing items, unchanged and not mine to closeDesign wants human ratification of two convention-only invariants (exception-arm ordering, serializer App suite Back to frozen at |
…fig over a failed read `store._read_index_unlocked` and `providers.read_config` collapse every read failure to an empty document. Both are correct as DISPLAY reads -- the board must render on an index it could not load, and every config accessor resolves to the caller's default -- and both are also the base of a whole-file rewrite, where an empty document means "delete every incident" and "drop every other provider's configuration". The index is not a view, it is the CLAIM ledger: `claim` is a compare-and-set against those rows. Emptied, every signal reads as unowned, so the next heartbeat re-claims alarms already being worked and opens a duplicate investigation of each one -- and in `act` mode a duplicate investigation is a second real write against the operator's production paging. An emptied config does not error, which is what makes it quiet: `provider_enabled` defaults to False, so it stops polling every provider the operator switched on while their credentials stay in the keystone store and Settings still shows each one as configured. Each module gains a private reader for its mutation path where only a MISSING file reads as empty. Both an unreadable file and a CORRUPT one propagate, so the mutation is abandoned rather than published over state nobody could read. Corruption propagating is a DELIBERATE divergence from the four merged siblings of this idiom -- `library.py:95`, `shares.py:60`, `secrets.py:231`, `policy_store.py:147` all still read an unparseable document as empty. Their justification is real: a document that failed to parse carries nothing to merge into. But "cannot merge into" is not "safe to destroy". A truncated file still holds most of its records verbatim, and replacing it discards the operator's only chance to recover them by hand. The divergence is temporary and tracked in #7805, `secrets.py` first, since there the discarded bytes are provider credentials that exist nowhere else on the box. The display reads stay lenient, and that asymmetry is the point: failing a render would turn a recoverable file into an unusable app. They now LOG when they degrade for any reason other than an absent file, because the state they degrade into looks exactly like health. Corruption and an unreadable file are deliberately NOT given one handler anywhere this change reaches, because `JSONDecodeError` subclasses `ValueError` and three tolerant callers already caught `ValueError` for the unrelated illegal-transition and raced-away cases -- so propagating alone would have been swallowed at every one of them, at debug level, by a handler written for something else. The distinction is persistence: an unreadable index is transient and the next pass retries, while a corrupt one fails identically forever until a person intervenes. What each of the three does about it differs by what has already happened when it runs. `dispatch.verify_pending_actions` and `slot_watch.reconcile` refuse, because nothing outside the app has changed yet. `routes._schedule_verification` REPORTS instead: both its callers have already performed the real external write by the time it runs, so raising would turn a completed action into a 500 and invite a retry that writes to the operator's production tooling a second time. It logs and records a SEL audit entry that the action ran with no recheck scheduled, then returns the same empty pair the transient case does. That is the same choice #7788 made for a partial ceiling apply in this file: the audit log is the durable reader, and it needs no `verification` value the dashboard cannot render. `claim` raises on both: a compare-and-set has no safe degraded answer, since `None` already means "another instance owns this signal". The same ordering rule is applied wherever the new strictness sits upstream of work worth keeping. `dispatch.run_cycle` degrades on its two maintenance passes, and its claim LOOP logs and breaks -- without that the maintenance guards were unreachable wherever they mattered, since the pre-filter reads leniently, so an unreadable index makes every firing signal a candidate and the claim raises before the webhook ack, the sweep, the Slack mirror, the notification bus and the SEL entry. On the hygiene cron, `prune_closed` degrades to zero pruned because it runs BEFORE `ledger_sync.sync_safely(direction="push")`: one EACCES there would otherwise skip pushing the ledger `hygiene` just deduped, which every other instance is waiting on, to save a prune the next run repeats. Two route handlers reach the newly-strict stores and could only answer a bare 500, so both now answer with a code, matching the helper #7788 added to this file. `POST .../proposal/decide` answers 503 `dispatch_index_unreadable`, because that request is a human's approval of a production action and "did my approval land?" must not be ambiguous. `PUT .../providers/{id}/config` answers 503 `app_config_unwritable` when the file is merely unwritable, and 500 `app_config_corrupt` when it is malformed -- corruption is not retryable, so advertising it as a 503 would tell the operator to retry something that cannot succeed until they repair the file. That helper's own docstring said `set_top_level` had no strict read "yet" and pointed at the companion PR for `providers/__init__.py`; this is that PR, so the caveat is discharged. The strict read makes "raise while holding `_IndexLock`" reachable for the first time. The release is already exception-safe and a test now pins it, because that regression would WEDGE rather than error: `flock` is per-descriptor, so a leaked lock deadlocks the same process against itself. No lock line moves, and the new reader holds no lock primitives of its own. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`test_providers.py` and `test_store_and_gate.py` became black-clean while being edited, and the gate requires a graduated file be removed so the baseline keeps shrinking. Separated per AGENTS.md, which says formatting a baselined file is optional and belongs in its own commit.
GPT found the one hole the round-trip rule structurally cannot seePushed Verified before acting, because the claim depended on a write I had not readGPT: a row whose index KEY disagrees with its own
Why my equivalence rule could not catch it, which is the interesting part. The round trip compares each Fixed at both endsThe read now asserts the referential invariant: the key IS the The write stops rekeying at all: Mutation-verified: disabling the identity check reds StateApp suite Standing advisories unchanged and still for a maintainer: Design's two convention-only invariants (on #7805 Frozen at |
GPT clear on
|
Green and handed off -- needs a human approval, and two decisions that are not mineAll 68 checks on Lanes, all four at this head: GPT 5.6 clear, Opus 4.8 clear ("No findings"), First Principles PASS, Two things genuinely want your judgement, not more work from me
One coupling worth a follow-up, flagged rather than fixed here
Suite |
buluoray
left a comment
There was a problem hiding this comment.
Verdict: 0 blocking, 2 non-blocking. Reviewed at head 6d0bcd4960dd648ec21cc1ec595a0a2760ec38c7. This is a data-loss guard, so I held it to the "a write must never proceed on a failed read" standard and verified the code rather than the prose.
What I verified
Files read from the PR head via the contents API: store.py, providers/__init__.py, dispatch.py, slot_watch.py, models.py; PR body; all bot comment bodies at the current SHA.
(a) Every read failure mode that feeds a whole-file rewrite is accounted for, enumerated against both store._read_index_for_update and providers._read_config_for_update:
- missing file ->
except FileNotFoundError: return {}— correct, an absent store is a fresh install, the one benign empty. - empty / 0-byte file ->
json.loads("")raisesJSONDecodeError-> re-wrappedCorruptDocumentError-> mutation refuses. Not treated as truncation. - unreadable (EACCES/EIO/PermissionError) ->
OSErroris deliberately NOT caught in the for-update readers, so it propagates and the mutation is abandoned. Fail-closed. - unparseable JSON ->
CorruptDocumentError. - non-UTF-8 bytes ->
UnicodeDecodeErrorre-wrapped toCorruptDocumentError(it is aValueErrorbut not aJSONDecodeError, so this closes the sibling-type bypass). - succeeds but returns a default-empty object — the dangerous case:
{}is only returned from a genuinely empty/absent file. A non-object root ([],null, string) raises in strict mode; a row/nested field that would not surviveto_dict(from_dict(x))raises via the_lostround-trip; a key/incident_idmismatch raises via the referential check. So an "empty because the read silently degraded" outcome cannot reach the writer.
(b) Fails closed. Every refusal raises inside the _IndexLock / _ConfigLock block before _write_index_unlocked / write_config, so no partial merge is published.
(c) Success path unchanged. Display reads (_read_index_unlocked, read_config) stay lenient and still return {}; only the mutation base swapped to the strict reader, which is a no-op for well-formed shipped data.
(d) Tests pin the refusal, not just absence of exception. The mutation-table tests assert the mutation refuses AND the file is byte-identical afterwards (e.g. test_a_read_that_failed_never_truncates_the_index, test_a_malformed_nested_field_is_not_silently_replaced). Caveat under "what I could not verify".
(e) Concurrent writers. Read-modify-write runs entirely under the pre-existing per-file flock (_IndexLock/_ConfigLock) and commits via atomic_write (mkstemp + os.replace). The PR changes only the reader inside the lock, introducing no new interleaving.
Caller policy is consistent with the fail-closed intent: dispatch.run_cycle and slot_watch.reconcile tolerate transient OSError (retry next cycle) but let CorruptDocumentError (a JSONDecodeError) propagate loudly; the except json.JSONDecodeError: raise arms are correctly ordered before the tolerant except ValueError arms, so corruption is never misfiled as a lost race.
Non-blocking findings
-
docs/system-specs/modules/ops-mission-control.md— spec not updated in this PR. AGENTS.md §Specification management (line 218) asks for a same-commit spec update when documented behavior changes. This PR adds a new documented read-on-mutation contract (*_for_update) and three new HTTP error surfaces (503*_unreadable, 500*_corrupt, 409*_version_skew); the existing spec still references only_read_index_unlockedand does not describe them. Consequence: the module spec drifts from the shipped error contract. Suggestion: add a short section on the strict-read contract and the three codes. Noting the First Principles lane (which checks AGENTS.md-mandated invariants) passed and Docs Lint is green, so a maintainer may already consider this acceptable. -
store.py(_schedule_verificationaudit wording) —UnknownFieldErroraudited as corruption. Already surfaced by GPT 5.6 at the current head and dispositioned to #7805 by the author.except json.JSONDecodeErroralso catchesUnknownFieldError(it subclassesCorruptDocumentError->JSONDecodeError), so a version-skew after a rollback would be audited as "index corrupt" rather than version skew. Consequence: misleading audit wording only, and unreachable in this build (no field is currently absent fromto_dict, so it needs a rollback plus a schema change). Suggestion: fix the wording alongside the schema-change work in #7805, as the author proposed.
What I could not verify
- I did not run the test suite or the mutation probes — the shared checkout is read-only and I made no local build. My confidence in (d) rests on reading the test assertions in the diff, not on executing them; the author reports 33 probes each reverting to red.
- The
_lost/_unknownround-trip logic and the_IndexLockexception-safety were verified by reading; I did not exercise the flock behavior under real concurrency.
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
Two reads in
ops_mission_controlcollapse every failure to an empty document and then feed awhole-file rewrite, so one transient read error makes the app believe a store is empty and publish
that emptiness back over it.
1. What is the problem?
store._read_index_unlockedandproviders.read_configbothreturn {}on any failure. As DISPLAYreads that is correct -- the board must render on an index it could not load. But they are also the BASE
of a read-modify-write that rewrites the whole file, where empty means "delete every incident".
The index is not a view, it is the CLAIM ledger:
claimis a compare-and-set against those rows.Emptied, every signal reads as unowned, so the next heartbeat re-claims alarms already being worked and
opens a duplicate investigation of each.
An emptied config does not error, which is what makes it quiet:
provider_enableddefaults to False,so polling stops for every provider the operator switched on while their credentials stay in the
keystone store and Settings still shows each one as configured.
2. Why this issue matters to the user
The failure is silent, durable, and looks like good news: a clean board and no firing signals is the
same picture as a quiet night. Nothing errors, nothing is logged, and the in-flight incidents are gone
from disk. In
actmode, duplicate investigations mean duplicate real writes to PagerDuty or Datadog.3. How our fix solves it
Each module gains a private
*_for_updatereader used only as the base of a mutation, in which amissing file still reads as empty (nothing has been written yet, so empty is the truth), and every
other way of failing to read it propagates so the mutation is abandoned:
That third rule is the one worth reviewing. Three review rounds each found the same loss one layer
deeper: the document root, then a row, then a nested field (
"signal": []coercing to an emptySignal). Every fix that enumerated a shape was beaten by a deeper example. So the index reader nowdeserializes, re-serializes, and refuses if anything on disk did not survive -- comparing parsed
structures so key order cannot cause a false refusal. It made the reader SMALLER: the per-row and
per-field
isinstanceguards are deleted, because dropping a row or blanking a field is exactly what theround trip detects.
The guarantee is "never publish over a read that failed", and a read that silently rewrote what it
could not understand is a read that failed.
Corruption propagating is a deliberate divergence from four merged siblings (
library.py:95,shares.py:60,secrets.py:231,policy_store.py:147), which read an unparseable document as empty.Their rationale is real -- such a document carries nothing to merge into. The counter is stronger:
"cannot merge into" is not "safe to destroy." A truncated file still holds most of its records.
Tracked in #7805,
secrets.pyfirst, where the discarded bytes are provider credentials that existnowhere else. A fifth reader shares the SHAPE (
ledger_index._read_cursor) and is exempt on the merits:its write is a set UNION, so an empty read only adds ids and can never drop one.
The display reads stay lenient -- failing a render would turn a recoverable file into an unusable app
-- and now LOG every degradation other than an absent file, structural ones included.
Caller policy differs by what has already happened.
claimraises on everything -- acompare-and-set has no safe degraded answer.
dispatch.run_cycle's maintenance passes andslot_watch.reconciletolerateOSError(transient) but refuse corruption (persistent).routes._schedule_verificationand the post-claim ledger annotation REPORT instead: their irreversiblestep has already happened, so raising would call a completed action failed and invite a retry that writes
twice. Both log and audit the degradation.
json.JSONDecodeErrorsubclassesValueError, so an existingexcept ValueErrorsilently claimscorruption and reports it as a validation error. All ten such arms were audited: one was reachable and
wrong (
_handle_proposeanswered400 invalid_proposalon a corrupt store), three already carried theclause, six cannot reach a strict reader. One shared
_store_read_refusalmaps three conditions at fivesites: 503
*_unreadable(retry works), 500*_corrupt(repair the file), and 409*_version_skew-- afile written by a NEWER build after a rollback, refused so the write cannot strip the newer field.
4. What tests we did
Is this change workable? The mechanism, not a count. Revert one line and a named test reddens:
_read_index_for_update'sexcept FileNotFoundError:back toreturn {}test_a_read_that_failed_never_truncates_the_index"a failed read was published back over the index", a byte comparison against a snapshot._coerce_index's round-trip equivalence refusaltest_a_malformed_nested_field_is_not_silently_replaced(3 subtests) andtest_a_skipped_entry_is_not_silently_deleted_by_the_next_mutationsignalis malformed comes back with the signal silently BLANKED and the original gone from disk -- source, native id, title, labels. Disabling the one rule reds all four, which is the evidence it covers the row and nested-field layers together rather than by enumeration.merge_provider_config's per-slot refusaltest_a_providers_key_of_the_wrong_shape_refuses_the_merge_handle_propose's corruption arm AFTER itsValueErrorarmtest_a_corrupt_index_is_not_reported_as_an_invalid_proposal400), so the operator re-types the form while the real fault sits on disk. Fails with400 == 400._schedule_verificationtest_a_corrupt_index_is_audited_rather_than_raising_after_the_action_ranEach was run in that reverted state and observed red, then restored. Twenty-eight probes in total; two
of them caught a test passing for the wrong reason, both from asserting something weaker than the
claim being made.
45 new tests. App suite
986 passed, 44 skipped, 266 subtests(from 921 onmain). Run with-o addopts=""to avoid the baked-in xdist parallelism. All seven repo gates green, includingmypy src/kiro_crew/(4 errors, all pre-existing).5. Any other suggestions on the work
formatting a baselined file "belongs in its own commit".
MAX_COMMITSis 2.secrets.pyfirst,plus the exception-type decision), Three remaining lenient-read-feeding-whole-file-rewrite sites, and a ratchet to close the class #7789 (remaining sites), ops-mission-control: store-write failures answer a bare untyped 500, not the app's coded error #7790 (handlers answering a bare
untyped 500 for other reasons).
Pattern harvest
A lenient read is safe as a DISPLAY read and unsafe as a MUTATION BASE, and the same function is often
both. The discriminator is not the read -- most lenient reads in
src/are legitimate -- it is whethera whole-file WRITE derives from it. The second lesson cost three review rounds: a guard that enumerates
bad SHAPES is defeated by a deeper example every time, so the check has to be about CONTENT survival.
Rule candidate: for any
read -> mutate -> write_all, the read must tell "absent" from "unreadable",and must refuse when deserializing would discard content the file already held.