[object Object]

← back to Terminal Status

TK-11317: fix deferential-set bypass + ticket-rebind hole; add carry age ceiling + revision continuity

f171fc92450b46f61631f93ce58ab9213d60648c · 2026-09-26 09:15:57 -0700 · Steve

Contrarian review of b594dbb found a reproducible CRITICAL hole: working-state.sh's
per-prompt UserPromptSubmit hook fires `set green WORKING --deferential --boot` on
every prompt. In set(), the deferential-yield guard requires a truthy `previous`, but
load(owner) returns None for reason owner_changed -- so the very first prompt after a
tty reuse silently overwrote a carried purple/orange/yellow/lightblue with plain green,
bypassing settle_owner_change entirely.

Fix: set() is now a thin lock-acquiring wrapper around a new _set_locked() write body.
When _set_locked sees previous is None, reason is owner_changed, and the caller is
deferential, it settles the owner change FIRST -- via a new _settle_locked() lock-free
write body (never re-enters self.lock(), which would deadlock; flock is not re-entrant
within one process) -- and uses the settled record as `previous`. Its colour is always
>= yellow priority, so the existing deferential-yield guard right after always fires and
repaints the settled record instead of falling through to plain green. A non-deferential
explicit set (e.g. /greendot on a genuinely new session) is unchanged -- the new branch
only engages for deferential=True.

Same-class hole #2: the interactive `ticket` command and auto-bind's _default_ticket_set
both did `previous, _ = store.read(owner)` and silently rebuilt the record as color
"none" whenever reason was actually owner_changed, dropping a carried colour the instant
a ticket got rebound before settle ever ran. Fixed via a new Store.effective_previous(),
backed by a shared Store._settlement_preview() (also now used by row()'s pre-backfill
preview and settle_owner_change itself, so all three can never disagree).

HIGH: prior_attention() now gates every candidate by CARRY_MAX_AGE_S (default 86400s /
24h, env TERMINAL_STATUS_CARRY_MAX_AGE_S) -- a source older than that, or one whose age
can't be determined, is never carried, floors to the yellow "New owner - status needed"
state instead (never green, never none, no ticket). Age is measured from the ORIGINAL
event: a record that is itself already a carry reports its age from its own
carried_from["at"], so a relay of owner_changed hops can't keep resetting the clock.
carried_from is now {"pid","started","at","hops"} -- pid/started/at are the ORIGINAL
carrier's identity (preserved across every hop), hops counts the relay length.

LOW: a carry write now continues the raw prior record's own revision (+1) instead of
resetting to 1, via a new revision_override param threaded through set()/_set_locked.

16 new tests (deferential-bypass repro for both the carried-attention and
nothing-carried cases, explicit-set-still-wins, staleness ceiling for both JSON and
legacy .dot sources, a 3-generation hop chain preserving original provenance, revision
continuity, effective_previous() passthrough, and the ticket-command/auto-bind repro) +
1 new selftest case (19/19). Negative control: 15/45 new tests fail/error against
b594dbb, including both deferential-bypass repros
(verification/TK-11317-integration/negative-control-v2.txt). Full suite green after the
fix (verification/TK-11317-integration/after-tests-v2.txt): test_terminal_status.py (75),
test_owner_change_carry.py (45), test_departed_asks.py (20), selftest (19/19),
test_stop_verdict.py (40), test_tk_required_fp_metric.py (13).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UqLpisVAyfYM4zoVP7fFya

Files touched

Diff

commit f171fc92450b46f61631f93ce58ab9213d60648c
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Sep 26 09:15:57 2026 -0700

    TK-11317: fix deferential-set bypass + ticket-rebind hole; add carry age ceiling + revision continuity
    
    Contrarian review of b594dbb found a reproducible CRITICAL hole: working-state.sh's
    per-prompt UserPromptSubmit hook fires `set green WORKING --deferential --boot` on
    every prompt. In set(), the deferential-yield guard requires a truthy `previous`, but
    load(owner) returns None for reason owner_changed -- so the very first prompt after a
    tty reuse silently overwrote a carried purple/orange/yellow/lightblue with plain green,
    bypassing settle_owner_change entirely.
    
    Fix: set() is now a thin lock-acquiring wrapper around a new _set_locked() write body.
    When _set_locked sees previous is None, reason is owner_changed, and the caller is
    deferential, it settles the owner change FIRST -- via a new _settle_locked() lock-free
    write body (never re-enters self.lock(), which would deadlock; flock is not re-entrant
    within one process) -- and uses the settled record as `previous`. Its colour is always
    >= yellow priority, so the existing deferential-yield guard right after always fires and
    repaints the settled record instead of falling through to plain green. A non-deferential
    explicit set (e.g. /greendot on a genuinely new session) is unchanged -- the new branch
    only engages for deferential=True.
    
    Same-class hole #2: the interactive `ticket` command and auto-bind's _default_ticket_set
    both did `previous, _ = store.read(owner)` and silently rebuilt the record as color
    "none" whenever reason was actually owner_changed, dropping a carried colour the instant
    a ticket got rebound before settle ever ran. Fixed via a new Store.effective_previous(),
    backed by a shared Store._settlement_preview() (also now used by row()'s pre-backfill
    preview and settle_owner_change itself, so all three can never disagree).
    
    HIGH: prior_attention() now gates every candidate by CARRY_MAX_AGE_S (default 86400s /
    24h, env TERMINAL_STATUS_CARRY_MAX_AGE_S) -- a source older than that, or one whose age
    can't be determined, is never carried, floors to the yellow "New owner - status needed"
    state instead (never green, never none, no ticket). Age is measured from the ORIGINAL
    event: a record that is itself already a carry reports its age from its own
    carried_from["at"], so a relay of owner_changed hops can't keep resetting the clock.
    carried_from is now {"pid","started","at","hops"} -- pid/started/at are the ORIGINAL
    carrier's identity (preserved across every hop), hops counts the relay length.
    
    LOW: a carry write now continues the raw prior record's own revision (+1) instead of
    resetting to 1, via a new revision_override param threaded through set()/_set_locked.
    
    16 new tests (deferential-bypass repro for both the carried-attention and
    nothing-carried cases, explicit-set-still-wins, staleness ceiling for both JSON and
    legacy .dot sources, a 3-generation hop chain preserving original provenance, revision
    continuity, effective_previous() passthrough, and the ticket-command/auto-bind repro) +
    1 new selftest case (19/19). Negative control: 15/45 new tests fail/error against
    b594dbb, including both deferential-bypass repros
    (verification/TK-11317-integration/negative-control-v2.txt). Full suite green after the
    fix (verification/TK-11317-integration/after-tests-v2.txt): test_terminal_status.py (75),
    test_owner_change_carry.py (45), test_departed_asks.py (20), selftest (19/19),
    test_stop_verdict.py (40), test_tk_required_fp_metric.py (13).
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01UqLpisVAyfYM4zoVP7fFya
---
 terminal_status.py                                 | 540 ++++++++++++++-------
 test_owner_change_carry.py                         | 232 ++++++++-
 .../TK-11317-integration/after-tests-v2.txt        | 250 ++++++++++
 .../TK-11317-integration/negative-control-v2.txt   | 197 ++++++++
 4 files changed, 1048 insertions(+), 171 deletions(-)

diff --git a/terminal_status.py b/terminal_status.py
index 5d1001f..1e61f14 100644
--- a/terminal_status.py
+++ b/terminal_status.py
@@ -75,6 +75,27 @@ ATTENTION = frozenset(("lightblue", "orange", "purple", "yellow"))
 # "unverified" yellow that the session's own hooks refine on its next turn.
 OWNER_STATUS_LABEL = "New owner — status needed"
 
+# TK-11317 contrarian review (2026-09-26): a carried ATTENTION colour must have an AGE
+# CEILING -- without one, an owner-change chain could carry a colour from days or weeks
+# ago forever, which is just a slower version of the original blind-green bug (an
+# unmeasured, no-longer-live claim being shown as current). Default 24h; override for
+# testing/tuning via TERMINAL_STATUS_CARRY_MAX_AGE_S. A source older than this (or whose
+# age cannot even be determined) is NEVER carried -- it falls through to the yellow
+# "New owner - status needed" floor exactly like "nothing pending" does.
+CARRY_MAX_AGE_S = float(os.environ.get("TERMINAL_STATUS_CARRY_MAX_AGE_S", "86400"))
+
+
+def _epoch_of(iso_text):
+    """Best-effort epoch seconds for a record's `updated_at` ISO string, or None on any
+    parse failure -- callers must treat None as 'age unknown' (never carried), not as
+    'fresh', per CLAUDE.md TK-11431 (an unmeasured input is never treated as good)."""
+    if not iso_text:
+        return None
+    try:
+        return dt.datetime.fromisoformat(iso_text).timestamp()
+    except (ValueError, TypeError):
+        return None
+
 # Steve's 2026-09-15 dot-flash directive: green (WORKING) and pink (PARKED) stay SOLID; the
 # four needs-Steve states PULSE. A true timed blink of the TAB itself is impossible without a
 # background per-tab loop -- and that loop IS the orphaned-stuck-color mechanism Steve's memory
@@ -704,6 +725,30 @@ def selftest():
         case("backfill missing floor still has no owner_status_unverified flag",
              green_rec is not None and "owner_status_unverified" not in green_rec)
 
+    # 5f. TK-11317 CONTRARIAN REVIEW (2026-09-26), CRITICAL hole #1: the injected
+    #    fault is EXACTLY working-state.sh's per-prompt UserPromptSubmit hook --
+    #    `set green WORKING --deferential --boot` -- landing on a tty whose only
+    #    record belongs to a PRIOR owner (reason owner_changed). Before the fix,
+    #    set()'s deferential-yield guard required a truthy `previous`, but
+    #    load(owner) returns None for owner_changed, so this call silently built
+    #    a brand-new PLAIN GREEN record over a carried purple. Runs headless, its
+    #    own tempdir Store (never touches the real state dir or a tty).
+    with tempfile.TemporaryDirectory() as df_dir:
+        df_old = Process(6464, 1, "ttys095", "Wed Sep 9 08:35:08 2026", "/usr/bin/claude")
+        df_new = Process(6565, 1, "ttys095", "Thu Sep 10 09:00:00 2026", "/usr/bin/claude")
+        df_old_store = Store(user_root=df_dir, process_provider=lambda **_: {6464: df_old},
+                             painter=lambda owner, record: None)
+        df_old_store.set(df_old.owner(), "purple", "TK-11317 gated reason", rows={6464: df_old})
+        df_store = Store(user_root=df_dir, process_provider=lambda **_: {6565: df_new},
+                         painter=lambda owner, record: None)
+        df_record = df_store.set(df_new.owner(), "green", "WORKING",
+                                 deferential=True, rows={6565: df_new})
+        df_on_disk, df_reason = df_store.load(df_new.owner())
+        case("deferential first-prompt paint carries purple, never plain green",
+             df_record["state"] == "purple" and df_on_disk is not None
+             and df_reason == "canonical" and df_on_disk["state"] == "purple"
+             and "carried_from" in df_record)
+
     # 6. TK-11870 heartbeat honesty (CLAUDE.md TK-11431: an unmeasured input is
     #    NEVER green; severity maps to capability). The injected faults are the
     #    two false-green shapes the old hardcoded-PASS heartbeat masked.
@@ -960,49 +1005,102 @@ class Store:
         except (OSError, ValueError, KeyError, TypeError):
             return None
 
-    def prior_attention(self, owner):
-        """TK-11317: what a PRIOR owner (before a tty-reuse owner_changed) left on
-        this tty, if it was an ATTENTION (needs-Steve) colour. Read-only; never
-        writes. Checks BOTH the canonical JSON record at this tty's path (owner
-        mismatch tolerated on purpose -- see _read_raw_for_prior) and the legacy
-        .dot mirror at ANY mtime: at the point this runs the NEW owner has not
-        painted anything yet, so anything sitting in either place can only be the
-        prior owner's. Returns {"color","label","ticket"} for whichever source is
-        HIGHER priority (PRIORITY: lower index wins), or None if neither carried
-        attention. The ticket is carried ONLY when the winning colour came from the
-        JSON record (it is the record that names the specific outstanding ask); a
-        legacy-.dot-only carry never invents a ticket id from its label text."""
-        candidates = []
+    def prior_attention(self, owner, *, now=None):
+        """TK-11317 (contrarian review, 2026-09-26): what a PRIOR owner (before a
+        tty-reuse owner_changed) left on this tty, if it was a FRESH ATTENTION
+        (needs-Steve) colour. Read-only; never writes.
+
+        Checks BOTH the canonical JSON record at this tty's path (owner mismatch
+        tolerated on purpose -- see _read_raw_for_prior) and the legacy .dot mirror,
+        each gated by CARRY_MAX_AGE_S: a source older than that (or one whose age
+        cannot even be determined) is NEVER carried -- it falls through exactly like
+        "nothing pending" does, floored to yellow "New owner - status needed" by
+        whichever caller resolves this into a write.
+
+        Age is measured from the ORIGINAL event, not the most recent hop: a JSON
+        record that is ITSELF already a carry (has `carried_from`) reports its age
+        from `carried_from["at"]`, so a relay of owner_changed hops cannot keep
+        resetting the clock and outrun the ceiling.
+
+        Returns None if nothing FRESH carried attention, else:
+          {"color", "label", "ticket", "carried_from", "prior_revision"}
+        `ticket` is carried ONLY when the winning colour came from the JSON record
+        (never invented from a legacy-.dot label). `carried_from` is
+        {"pid","started","at","hops"} -- pid/started are the ORIGINAL carrier's
+        identity when known (None for a legacy-.dot-only source, which has no
+        stored process identity); hops counts how many owner_changed carries this
+        attention has survived. `prior_revision` is the raw JSON record's own
+        `revision` (so a write can CONTINUE the revision sequence instead of
+        resetting to 1), or None when the winning source has no revision to
+        continue (legacy-only)."""
+        now = now if now is not None else time.time()
+        candidates = []  # (priority, color, label, ticket, carried_from, prior_revision)
+
         r = self._read_raw_for_prior(owner)
         if r is not None and r["state"] in ATTENTION:
-            label, _ = label_ticket(r["label"])
-            candidates.append((PRIORITY[r["state"]], r["state"], label, r.get("ticket", "")))
+            origin = r.get("carried_from") if isinstance(r.get("carried_from"), dict) else None
+            if origin is not None and isinstance(origin.get("at"), (int, float)):
+                at = origin["at"]
+                hops = int(origin.get("hops", 1)) + 1
+                pid, started = origin.get("pid"), origin.get("started")
+            else:
+                at = _epoch_of(r.get("updated_at"))
+                hops = 1
+                owner_field = r.get("owner") if isinstance(r.get("owner"), dict) else {}
+                pid, started = owner_field.get("pid"), owner_field.get("started")
+            if at is not None and (now - at) <= CARRY_MAX_AGE_S:
+                label, _ = label_ticket(r["label"])
+                candidates.append((PRIORITY[r["state"]], r["state"], label, r.get("ticket", ""),
+                                  {"pid": pid, "started": started, "at": at, "hops": hops},
+                                  r.get("revision")))
+            # else: unparseable or stale -- NOT carried (CLAUDE.md TK-11431: an
+            # unmeasured/too-old input is never treated as good).
+
         legacy_path = self.legacy[owner.runtime] / (owner.tty + ".dot")
         try:
+            st = legacy_path.stat()
             title = legacy_path.read_text().strip()
         except OSError:
-            title = ""
-        if title and valid_label(title):
+            title, st = "", None
+        if (title and valid_label(title) and st is not None
+                and (now - st.st_mtime) <= CARRY_MAX_AGE_S):
             color = color_of(title)
             if color in ATTENTION:
                 raw_label = title[len(COLORS[color][0]):].strip()
                 label, _ = label_ticket(raw_label)
-                candidates.append((PRIORITY[color], color, label, ""))
+                candidates.append((PRIORITY[color], color, label, "",
+                                  {"pid": None, "started": None, "at": st.st_mtime, "hops": 1},
+                                  None))
         if not candidates:
             return None
         candidates.sort(key=lambda c: c[0])
-        _, color, label, ticket = candidates[0]
-        return {"color": color, "label": label, "ticket": ticket}
-
-    def prior_owner_info(self, owner):
-        """Best-effort identity of the prior owner whose record occupied this tty,
-        for the settled record's `carried_from` provenance. None when there is no
-        JSON record to read it from (e.g. a legacy-.dot-only carry has no stored
-        pid/started)."""
-        r = self._read_raw_for_prior(owner)
-        if r is not None and isinstance(r.get("owner"), dict):
-            return {"pid": r["owner"].get("pid"), "started": r["owner"].get("started")}
-        return None
+        _, color, label, ticket, carried_from, prior_revision = candidates[0]
+        return {"color": color, "label": label, "ticket": ticket,
+               "carried_from": carried_from, "prior_revision": prior_revision}
+
+    def _settlement_preview(self, owner):
+        """The (unwritten) record settle_owner_change() would persist for this tty
+        RIGHT NOW -- the single shared computation behind row()'s <20s pre-backfill
+        preview, effective_previous() (ticket-only rebinds), and settle_owner_change's
+        own write, so all three can never disagree about what "the settled state" is.
+        Shaped like a loaded record's relevant fields: state/label/ticket/ticket_at/
+        ticket_source/variant, plus carried_from (attention carry) or
+        owner_status_unverified (nothing fresh pending), plus next_revision when the
+        winning source has a revision to continue from."""
+        prior = self.prior_attention(owner)
+        if prior is not None:
+            color = prior["color"]
+            label = prior["label"] or COLORS[color][2]
+            rec = {"state": color, "label": label, "ticket": prior["ticket"],
+                  "ticket_at": time.time() if prior["ticket"] else 0,
+                  "ticket_source": "carried" if prior["ticket"] else "unbound",
+                  "variant": "", "carried_from": prior["carried_from"]}
+            if prior.get("prior_revision") is not None:
+                rec["next_revision"] = prior["prior_revision"] + 1
+            return rec
+        return {"state": "yellow", "label": OWNER_STATUS_LABEL,
+               "ticket": "", "ticket_at": 0, "ticket_source": "unbound",
+               "variant": "", "owner_status_unverified": True}
 
     def _prior_raw_texts(self, owner):
         """Raw label/title text the prior owner left in BOTH sources, so ticket ids a
@@ -1033,33 +1131,57 @@ class Store:
         except (OSError, ValueError, KeyError, TypeError):
             return None
 
+    def _settle_locked(self, owner, *, rows=None):
+        """Write settle_owner_change()'s outcome using the CURRENTLY HELD lock --
+        never calls self.lock() (flock is not re-entrant within one process; a
+        second acquire from here would deadlock). Used both by the public, locking
+        settle_owner_change() below, and by _set_locked() itself (TK-11317
+        contrarian fix) when an automatic deferential caller -- the per-prompt
+        working-state.sh hook's `set green WORKING --deferential --boot` -- hits
+        reason owner_changed: previously `previous` was simply None there, so the
+        deferential-yield guard (which requires a truthy `previous`) never fired
+        and a carried purple/orange/yellow/lightblue was silently overwritten with
+        plain green. Settling first, inside the SAME lock, closes that hole."""
+        preview = self._settlement_preview(owner)
+        carried_from = preview.get("carried_from")
+        owner_status_unverified = preview.get("owner_status_unverified")
+        revision_override = preview.get("next_revision")
+        if carried_from is not None:
+            self.record_departed(owner, {"color": preview["state"], "label": preview["label"],
+                                         "ticket": preview["ticket"]}, carried_from)
+        try:
+            return self._set_locked(owner, preview["state"], preview["label"],
+                                    issued_ns=issued_clock(), ticket_update=preview["ticket"] or "",
+                                    variant="", deferential=False, rows=rows,
+                                    carried_from=carried_from,
+                                    owner_status_unverified=owner_status_unverified,
+                                    revision_override=revision_override)
+        except StatusError:
+            # An unknown/stale carried ticket id must never block restoring the
+            # carried ATTENTION colour itself -- drop the ticket, keep the colour.
+            return self._set_locked(owner, preview["state"], preview["label"],
+                                    issued_ns=issued_clock(), ticket_update="",
+                                    variant="", deferential=False, rows=rows,
+                                    carried_from=carried_from,
+                                    owner_status_unverified=owner_status_unverified,
+                                    revision_override=revision_override)
+
     def settle_owner_change(self, owner, *, rows=None, lock_wait=None):
         """TK-11317 (Steve's TK-11620 guard ruling, 2026-09-26): a tty-reuse
         owner_changed record must NEVER silently blank or green-floor a prior
-        needs-Steve colour. If the prior owner left attention pending (JSON record
-        or legacy .dot mirror), CARRY it forward onto the new owner's record with
-        provenance (`carried_from`: the prior owner's pid/started, when known).
-        Otherwise floor to yellow "New owner - status needed"
-        (`owner_status_unverified`) rather than a blind green -- a brand-new
-        session's real state has not been observed yet either, so green would be
-        just as unmeasured a claim as silence was."""
-        prior = self.prior_attention(owner)
-        info = self.prior_owner_info(owner)
-        if prior is not None:
-            self.record_departed(owner, prior, info)
-            label = prior["label"] or COLORS[prior["color"]][2]
-            try:
-                return self.set(owner, prior["color"], label,
-                                ticket_update=prior["ticket"] or "",
-                                rows=rows, lock_wait=lock_wait, carried_from=info)
-            except StatusError:
-                # An unknown/stale carried ticket id must never block restoring the
-                # carried ATTENTION colour itself -- drop the ticket, keep the colour.
-                return self.set(owner, prior["color"], label,
-                                ticket_update="", rows=rows, lock_wait=lock_wait,
-                                carried_from=info)
-        return self.set(owner, "yellow", OWNER_STATUS_LABEL,
-                        rows=rows, lock_wait=lock_wait, owner_status_unverified=True)
+        needs-Steve colour. If the prior owner left FRESH attention pending (JSON
+        record or legacy .dot mirror, within CARRY_MAX_AGE_S), CARRY it forward
+        onto the new owner's record with provenance (`carried_from`: the ORIGINAL
+        carrier's pid/started/at, plus a hop count). Otherwise floor to yellow
+        "New owner - status needed" (`owner_status_unverified`) rather than a
+        blind green -- a brand-new session's real state has not been observed yet
+        either, so green would be just as unmeasured a claim as silence was.
+
+        Public, LOCKING entry point for callers that do not already hold this
+        tty's lock (backfill, the `start` command). See _settle_locked for the
+        actual write logic, shared with _set_locked's own-lock presettle path."""
+        with self.lock(owner, rows=rows, wait=lock_wait):
+            return self._settle_locked(owner, rows=rows)
 
     def legacy_status(self, owner, live_title=""):
         # Only the owning runtime's legacy file can be considered during rollout.
@@ -1086,10 +1208,56 @@ class Store:
         # A clear tombstone, corruption, or reused tty can never fall back.
         return r, reason
 
+    def effective_previous(self, owner, live_title=""):
+        """TK-11317 (contrarian review, 2026-09-26): resolve "what does this tty
+        already show" for TICKET-ONLY / preserve-style writers that must never
+        treat owner_changed as "nothing here". Before this, both the `ticket`
+        command and auto-bind's ticket_set did `previous, _ = store.read(owner)`
+        and silently defaulted to a BLANK "none" record whenever reason was
+        actually owner_changed -- so rebinding a ticket on a reused tty dropped a
+        carried needs-Steve colour the instant it ran, even though the colour
+        itself was never touched otherwise.
+
+        Returns (record, reason) shaped exactly like read(): canonical/legacy/
+        missing pass through unchanged; owner_changed returns the SAME (unwritten)
+        settlement preview settle_owner_change() would persist, so
+        `effective_previous(owner)[0]["state"]` is always the tab's true
+        current-or-about-to-be-settled colour, never "none"."""
+        r, reason = self.load(owner)
+        if reason == "owner_changed":
+            return self._settlement_preview(owner), "owner_changed"
+        if reason == "missing":
+            return self.legacy_status(owner, live_title)
+        return r, reason
+
     def set(self, owner, color, label="", *, issued_ns=None, ticket_update=None,
             variant="", deferential=False, rows=None, lock_wait=None,
-            carried_from=None, owner_status_unverified=None):
+            carried_from=None, owner_status_unverified=None, revision_override=None):
+        # TK-11835: a cross-tty caller passes the already-resolved `rows` (so the
+        # in-lock assert_owner reuses main's table instead of re-scanning) and a
+        # short `lock_wait` (so a busy target is skipped fast, not waited on 65s).
+        # Both default to None -> byte-identical to the prior self-paint path.
         issued_ns = issued_ns if issued_ns is not None else issued_clock()
+        with self.lock(owner, rows=rows, wait=lock_wait):
+            return self._set_locked(owner, color, label, issued_ns=issued_ns,
+                                    ticket_update=ticket_update, variant=variant,
+                                    deferential=deferential, rows=rows,
+                                    carried_from=carried_from,
+                                    owner_status_unverified=owner_status_unverified,
+                                    revision_override=revision_override)
+
+    def _set_locked(self, owner, color, label, *, issued_ns, ticket_update, variant,
+                    deferential, rows, carried_from, owner_status_unverified,
+                    revision_override=None):
+        """The full write body of set() -- caller MUST already hold self.lock(owner)
+        (set() itself, or _settle_locked() via _set_locked's own presettle branch
+        below). Factored out of set() (TK-11317 contrarian fix, CRITICAL hole #1):
+        a deferential AUTOMATIC caller (working-state.sh's per-prompt
+        `set green WORKING --deferential --boot`) hitting reason owner_changed can
+        now settle the owner change FIRST, inside the SAME already-held lock,
+        WITHOUT re-entering self.lock() -- flock is not re-entrant within one
+        process, so a second acquire here would deadlock; _settle_locked() is the
+        lock-free write body shared with the public settle_owner_change()."""
         if color not in COLORS or not valid_label(label) or variant not in ("", "monitoring", "stopped", "waiting"):
             raise StatusError("Invalid status or label")
         label, embedded_ticket = label_ticket(label)
@@ -1105,103 +1273,127 @@ class Store:
         # paints its own reason — otherwise the new ticket lands on the OLD label.
         caller_gave_reason = bool(label) or ticket_update is not None
         label = "" if color == "none" else (label or COLORS[color][2])
-        # TK-11835: a cross-tty caller passes the already-resolved `rows` (so the
-        # in-lock assert_owner reuses main's table instead of re-scanning) and a
-        # short `lock_wait` (so a busy target is skipped fast, not waited on 65s).
-        # Both default to None -> byte-identical to the prior self-paint path.
-        with self.lock(owner, rows=rows, wait=lock_wait):
-            previous, _ = self.load(owner)
-            if previous and issued_ns <= previous["issued_ns"]:
-                raise StatusError("Obsolete status update; a newer event already won")
-            # T1-c (TK-11779) — an EXPLICIT lightblue must not ERASE an underlying
-            # needs-Steve reason. lightblue is the UMBRELLA "this stop needs Steve"; when
-            # a SPECIFIC reason already stands on the tab — purple (gated memo), orange
-            # (paste waiting), yellow (question) — express "needs Steve" as the additive
-            # 🔵 stopped marker ON that base colour instead of overwriting it. Clearing the
-            # marker later (session resumes) then REVEALS the still-pending reason instead
-            # of a dead/dotless tab that silently lost its gated memo. A green/teal/pink/
-            # none/lightblue base has no reason to preserve, so it takes solid lightblue as
-            # before. `variant == "stopped"` guards against re-routing the additive path
-            # itself (the Stop hook already sets the marker directly).
-            if (color == "lightblue" and variant != "stopped" and not caller_gave_reason
-                    and previous and previous.get("state") in ("purple", "orange", "yellow")):
-                color = previous["state"]
-                variant = "stopped"
-                label = label_ticket(previous.get("label", ""))[0]
-                # Defensive coherence (TK-11826 / TK-11779 Finding 2): the branch guard
-                # `not caller_gave_reason` already guarantees ticket_update is None here
-                # (caller_gave_reason is True whenever ticket_update is not None), so the
-                # overwrite below cannot fire on this path today. Pin it to None anyway so
-                # the preserved reason stays coherent as a WHOLE — colour + label + ticket —
-                # even if a future edit ever loosens the branch condition; never the
-                # mismatch of an OLD preserved label carrying a NEW caller ticket.
-                ticket_update = None
-            # TK-11378 (DTD verdict A) — an AUTOMATIC paint must never erase a pending
-            # request for Steve's attention. PRIORITY already encodes the ordering
-            # (orange > purple > yellow > green > pink > none) but until now was only
-            # used to sort scan() output, never to arbitrate a paint. "Sticky" = anything
-            # ranked above green, i.e. the three colors that mean a human is blocked.
-            # Only deferential callers (the per-prompt working-state hook) yield; every
-            # EXPLICIT paint (/greendot, /color, /pinkdot, --off) stays authoritative, so
-            # a session can still clear its own dot or reuse the tab for new work.
-            if (deferential and previous and color == "green"
-                    and PRIORITY.get(previous.get("state"), PRIORITY["none"]) < PRIORITY["green"]):
-                self.assert_owner(owner, rows)
-                try:
-                    self.painter(owner, previous)
-                except (OSError, StatusError):
-                    pass  # keep the record authoritative even if the repaint fails
-                return previous
-            ticket = self.ticket_info(owner, previous)
-            if ticket_update is not None:
-                ticket = {"id": ticket_update, "at": time.time(), "source": "explicit"}
-            record = {
-                "version": VERSION, "owner": dataclasses.asdict(owner),
-                "state": color, "label": label,
-                "title": status_title(color, label, ticket["id"],
-                                     variant if (color == "green" or variant == "stopped") else ""),
-                "ticket": ticket["id"], "ticket_at": ticket["at"],
-                "ticket_source": ticket["source"],
-                # "stopped" is colour-AGNOSTIC: it is an additive 🔵 marker beside ANY base
-                # dot, and the blocked colours (purple/yellow/orange) are its whole point.
-                # "monitoring" stays green-only because it is the teal TINT of green.
-                "variant": variant if (color == "green" or variant == "stopped") else "",
-                "revision": previous["revision"] + 1 if previous else 1,
-                "event_id": uuid.uuid4().hex, "issued_ns": issued_ns,
-                "updated_at": dt.datetime.now(dt.timezone.utc).isoformat(),
-                "render_status": "pending", "mirror_errors": [],
-            }
-            # TK-11317: both fields are OPT-IN per call, never inherited from
-            # `previous` implicitly -- an ordinary explicit set() (e.g. /greendot)
-            # that doesn't pass them builds a record without them, which is exactly
-            # how "only an explicit non-deferential set clears owner_status_unverified"
-            # is satisfied. A caller that means to PRESERVE either field across a
-            # rebuild (settle_owner_change's carry, the `ticket` command's rebind)
-            # must pass it through explicitly.
-            if carried_from is not None:
-                record["carried_from"] = carried_from
-            if owner_status_unverified:
-                record["owner_status_unverified"] = True
+        previous, load_reason = self.load(owner)
+        # TK-11317 contrarian fix (CRITICAL hole #1, 2026-09-26): previously
+        # `previous` was simply None whenever reason was owner_changed (the tty's
+        # only on-disk record belongs to a PRIOR owner), so the deferential-yield
+        # guard just below -- which requires a truthy `previous` -- never fired,
+        # and an AUTOMATIC deferential green call went on to build a brand-new
+        # green record, silently overwriting whatever the prior owner had left
+        # pending. Settle the owner change first (carry attention, or the yellow
+        # "New owner - status needed" floor) and use THAT as `previous`. Its
+        # colour is always >= yellow priority (never green/pink/none), so the
+        # deferential-yield guard right below this will always fire on it and
+        # repaint the settled record -- it never falls through to overwrite with
+        # green. A NON-deferential explicit caller (e.g. /greendot on a genuinely
+        # new session) is UNCHANGED -- this branch only engages for `deferential`.
+        if previous is None and load_reason == "owner_changed" and deferential:
+            previous = self._settle_locked(owner, rows=rows)
+            # The settle write's OWN issued_ns is necessarily fresher than the
+            # issued_ns this call captured before presettling (it is a later
+            # event, in the same call), so without this bump the very next line
+            # would see `issued_ns <= previous["issued_ns"]` and misreport this
+            # call as obsolete against its OWN side effect. +1 guarantees strictly
+            # newer regardless of clock resolution ties.
+            issued_ns = max(issued_ns, previous["issued_ns"] + 1)
+        if previous and issued_ns <= previous["issued_ns"]:
+            raise StatusError("Obsolete status update; a newer event already won")
+        # T1-c (TK-11779) — an EXPLICIT lightblue must not ERASE an underlying
+        # needs-Steve reason. lightblue is the UMBRELLA "this stop needs Steve"; when
+        # a SPECIFIC reason already stands on the tab — purple (gated memo), orange
+        # (paste waiting), yellow (question) — express "needs Steve" as the additive
+        # 🔵 stopped marker ON that base colour instead of overwriting it. Clearing the
+        # marker later (session resumes) then REVEALS the still-pending reason instead
+        # of a dead/dotless tab that silently lost its gated memo. A green/teal/pink/
+        # none/lightblue base has no reason to preserve, so it takes solid lightblue as
+        # before. `variant == "stopped"` guards against re-routing the additive path
+        # itself (the Stop hook already sets the marker directly).
+        if (color == "lightblue" and variant != "stopped" and not caller_gave_reason
+                and previous and previous.get("state") in ("purple", "orange", "yellow")):
+            color = previous["state"]
+            variant = "stopped"
+            label = label_ticket(previous.get("label", ""))[0]
+            # Defensive coherence (TK-11826 / TK-11779 Finding 2): the branch guard
+            # `not caller_gave_reason` already guarantees ticket_update is None here
+            # (caller_gave_reason is True whenever ticket_update is not None), so the
+            # overwrite below cannot fire on this path today. Pin it to None anyway so
+            # the preserved reason stays coherent as a WHOLE — colour + label + ticket —
+            # even if a future edit ever loosens the branch condition; never the
+            # mismatch of an OLD preserved label carrying a NEW caller ticket.
+            ticket_update = None
+        # TK-11378 (DTD verdict A) — an AUTOMATIC paint must never erase a pending
+        # request for Steve's attention. PRIORITY already encodes the ordering
+        # (orange > purple > yellow > green > pink > none) but until now was only
+        # used to sort scan() output, never to arbitrate a paint. "Sticky" = anything
+        # ranked above green, i.e. the three colors that mean a human is blocked.
+        # Only deferential callers (the per-prompt working-state hook) yield; every
+        # EXPLICIT paint (/greendot, /color, /pinkdot, --off) stays authoritative, so
+        # a session can still clear its own dot or reuse the tab for new work.
+        if (deferential and previous and color == "green"
+                and PRIORITY.get(previous.get("state"), PRIORITY["none"]) < PRIORITY["green"]):
             self.assert_owner(owner, rows)
-            atomic_write(self.path(owner), json.dumps(record, indent=2) + "\n")
-            error = None
             try:
-                self.assert_owner(owner, rows)
-                self.painter(owner, record)
-                record["render_status"] = "applied"
-            except (OSError, StatusError) as exc:
-                record["render_status"] = "failed"
-                record["render_error"] = str(exc)
-                error = exc
-            for directory in self.legacy.values():
-                try:
-                    atomic_write(directory / (owner.tty + ".dot"), record["title"])
-                except OSError as exc:
-                    record["mirror_errors"].append(str(exc))
-            atomic_write(self.path(owner), json.dumps(record, indent=2) + "\n")
-            if error or record["mirror_errors"]:
-                raise StatusError("Status saved, but display/mirror update failed; run audit")
-            return record
+                self.painter(owner, previous)
+            except (OSError, StatusError):
+                pass  # keep the record authoritative even if the repaint fails
+            return previous
+        ticket = self.ticket_info(owner, previous)
+        if ticket_update is not None:
+            ticket = {"id": ticket_update, "at": time.time(), "source": "explicit"}
+        record = {
+            "version": VERSION, "owner": dataclasses.asdict(owner),
+            "state": color, "label": label,
+            "title": status_title(color, label, ticket["id"],
+                                 variant if (color == "green" or variant == "stopped") else ""),
+            "ticket": ticket["id"], "ticket_at": ticket["at"],
+            "ticket_source": ticket["source"],
+            # "stopped" is colour-AGNOSTIC: it is an additive 🔵 marker beside ANY base
+            # dot, and the blocked colours (purple/yellow/orange) are its whole point.
+            # "monitoring" stays green-only because it is the teal TINT of green.
+            "variant": variant if (color == "green" or variant == "stopped") else "",
+            # TK-11317 item 4 (LOW, contrarian review): `revision_override` lets a CARRY
+            # write (settle_owner_change / effective_previous-driven ticket rebinds)
+            # CONTINUE the raw prior record's own revision sequence instead of resetting
+            # to 1 -- the carry is a continuation of the same outstanding ask, not a new
+            # one. None (the default for every ordinary call) preserves the original
+            # "previous+1 or 1" behaviour byte-for-byte.
+            "revision": revision_override if revision_override is not None else (
+                previous["revision"] + 1 if previous else 1),
+            "event_id": uuid.uuid4().hex, "issued_ns": issued_ns,
+            "updated_at": dt.datetime.now(dt.timezone.utc).isoformat(),
+            "render_status": "pending", "mirror_errors": [],
+        }
+        # TK-11317: both fields are OPT-IN per call, never inherited from
+        # `previous` implicitly -- an ordinary explicit set() (e.g. /greendot)
+        # that doesn't pass them builds a record without them, which is exactly
+        # how "only an explicit non-deferential set clears owner_status_unverified"
+        # is satisfied. A caller that means to PRESERVE either field across a
+        # rebuild (settle_owner_change's carry, the `ticket` command's rebind)
+        # must pass it through explicitly.
+        if carried_from is not None:
+            record["carried_from"] = carried_from
+        if owner_status_unverified:
+            record["owner_status_unverified"] = True
+        self.assert_owner(owner, rows)
+        atomic_write(self.path(owner), json.dumps(record, indent=2) + "\n")
+        error = None
+        try:
+            self.assert_owner(owner, rows)
+            self.painter(owner, record)
+            record["render_status"] = "applied"
+        except (OSError, StatusError) as exc:
+            record["render_status"] = "failed"
+            record["render_error"] = str(exc)
+            error = exc
+        for directory in self.legacy.values():
+            try:
+                atomic_write(directory / (owner.tty + ".dot"), record["title"])
+            except OSError as exc:
+                record["mirror_errors"].append(str(exc))
+        atomic_write(self.path(owner), json.dumps(record, indent=2) + "\n")
+        if error or record["mirror_errors"]:
+            raise StatusError("Status saved, but display/mirror update failed; run audit")
+        return record
 
     def repaint(self, owner, *, rows=None, lock_wait=None):
         with self.lock(owner, rows=rows, wait=lock_wait):
@@ -1312,19 +1504,16 @@ class Store:
             if reason == "owner_changed":
                 # TK-11317: this is the <20s window before backfill's next cycle
                 # actually settles the record -- render the WOULD-BE settled state
-                # (settle_owner_change's own logic, read-only here) instead of a bare
-                # ⚪ "status not set", so a carried needs-Steve colour is never even
-                # MOMENTARILY invisible.
-                prior = self.prior_attention(owner)
-                if prior is not None:
-                    color = prior["color"]
-                    label = prior["label"] or COLORS[color][2]
-                    tk_id = prior["ticket"] or ticket["id"]
-                else:
-                    color, label, tk_id = "yellow", OWNER_STATUS_LABEL, ticket["id"]
+                # (the SAME _settlement_preview() effective_previous() and
+                # settle_owner_change() itself use, so all three can never
+                # disagree) instead of a bare ⚪ "status not set", so a carried
+                # needs-Steve colour is never even MOMENTARILY invisible.
+                preview = self._settlement_preview(owner)
+                tk_id = preview["ticket"] or ticket["id"]
                 result.update(
-                    color=color,
-                    label=status_title(color, label, tk_id or "TK REQUIRED", "")
+                    color=preview["state"],
+                    label=status_title(preview["state"], preview["label"],
+                                       tk_id or "TK REQUIRED", "")
                         or "⚪ " + (tk_id or "TK REQUIRED") + " · Status cleared",
                     warnings=["owner_status_unverified"])
                 return result
@@ -1719,11 +1908,20 @@ def _default_tk_new(owner, topic, project):
 
 def _default_ticket_set(store, owner, ticket_id):
     """Same effect as the `ticket` subcommand: bind explicitly, preserving
-    whatever color/label the session already has."""
-    previous, _ = store.read(owner)
+    whatever color/label the session already has. TK-11317 contrarian review:
+    uses effective_previous() (not read()) so a reused tty whose only record
+    belongs to a PRIOR owner never gets silently rebound to "none" -- this is
+    the AUTOMATIC auto-bind path (backgrounded from working-state.sh's
+    UserPromptSubmit hook), so it must never blank a carried attention colour
+    any more than the deferential --boot paint may."""
+    previous, reason = store.effective_previous(owner)
     store.set(owner, previous["state"] if previous else "none",
              label_ticket(previous["label"])[0] if previous else "",
-             ticket_update=ticket_id, variant=previous.get("variant", "") if previous else "")
+             ticket_update=ticket_id, variant=previous.get("variant", "") if previous else "",
+             carried_from=previous.get("carried_from") if previous else None,
+             owner_status_unverified=previous.get("owner_status_unverified") if previous else None,
+             revision_override=(previous.get("next_revision")
+                                if previous and reason == "owner_changed" else None))
     # header() is a cosmetic tab-title refresh (adds the "up Nh Mm" suffix) on
     # top of the dot that set() already painted -- the ticket is already bound
     # in the record at this point, so a pty write hiccup here must not be
@@ -2121,16 +2319,22 @@ def main(argv=None):
         ticket = "" if ticket in ("CLEAR", "NONE") else ("TK-" + ticket if ticket.isdigit() else tickets.short(ticket))
         if not ticket and args.ticket.upper() not in ("CLEAR", "NONE"):
             raise StatusError("Expected a canonical TK number or clear")
-        previous, _ = store.read(caller)
+        # TK-11317 contrarian review: effective_previous() (not read()) -- a reused
+        # tty whose only record belongs to a PRIOR owner must never rebind to
+        # "none" just because a ticket command ran before the owner change settled.
+        previous, reason = store.effective_previous(caller)
         # TK-11317: the ticket command only REBINDS the ticket id -- it must preserve
         # a carried needs-Steve colour and the owner_status_unverified flag exactly
         # as store.set()'s opt-in-per-call contract requires (they are never
-        # inherited implicitly from `previous`).
+        # inherited implicitly from `previous`), and CONTINUE the revision sequence
+        # when this is the moment a carry first lands on disk.
         record = store.set(caller, previous["state"] if previous else "none",
                            label_ticket(previous["label"])[0] if previous else "",
                            ticket_update=ticket, variant=previous.get("variant", "") if previous else "",
                            carried_from=previous.get("carried_from") if previous else None,
-                           owner_status_unverified=previous.get("owner_status_unverified") if previous else None)
+                           owner_status_unverified=previous.get("owner_status_unverified") if previous else None,
+                           revision_override=(previous.get("next_revision")
+                                              if previous and reason == "owner_changed" else None))
         store.header(caller)
         print(f'/terminal-ticket → {caller.tty} {record["ticket"] or "TK REQUIRED"}')
         return 0
diff --git a/test_owner_change_carry.py b/test_owner_change_carry.py
index ecaaa28..a04e502 100644
--- a/test_owner_change_carry.py
+++ b/test_owner_change_carry.py
@@ -8,7 +8,11 @@ against a tempdir Store with a fake in-memory painter -- never a real tty, never
 the shared ~/.local/state/abrams-terminal-status store.
 """
 import dataclasses
+import datetime as dt
+import json
+import os
 import tempfile
+import time
 import unittest
 from pathlib import Path
 
@@ -37,6 +41,30 @@ class OwnerChangeCarryTests(unittest.TestCase):
         path.write_text(title)
         return path
 
+    def age_old_record(self, seconds_ago):
+        """Rewrite the OLD owner's on-disk `updated_at` to look `seconds_ago` in
+        the past -- the freshness clock prior_attention() reads for a JSON
+        source -- AND age its legacy .dot mirrors to match (Store.set() always
+        writes ALL runtimes' mirrors, so a same-tty realistic "this leftover
+        state is N days old" scenario ages both together; aging only the JSON
+        would leave the still-fresh .dot mirror as an unaged fallback source,
+        silently defeating the point of the test). Bypasses Store.set() (which
+        always stamps "now") so tests can exercise CARRY_MAX_AGE_S deterministically."""
+        path = self.store.root / (self.old_owner.tty + ".json")
+        data = json.loads(path.read_text())
+        aged = dt.datetime.now(dt.timezone.utc) - dt.timedelta(seconds=seconds_ago)
+        data["updated_at"] = aged.isoformat()
+        path.write_text(json.dumps(data, indent=2) + "\n")
+        when = time.time() - seconds_ago
+        for directory in self.store.legacy.values():
+            dot_path = directory / (self.old_owner.tty + ".dot")
+            if dot_path.exists():
+                os.utime(dot_path, (when, when))
+
+    def age_legacy(self, path, seconds_ago):
+        when = time.time() - seconds_ago
+        os.utime(path, (when, when))
+
     def make_old_record(self, color, label="reason"):
         """Write a canonical record for the OLD owner directly (no lock/assert_owner
         contention with the new owner, since the fixture's process table only
@@ -52,7 +80,15 @@ class OwnerChangeCarryTests(unittest.TestCase):
     def test_prior_purple_record_carried(self):
         self.make_old_record("purple", "gated memo")
         prior = self.store.prior_attention(self.new_owner)
-        self.assertEqual(prior, {"color": "purple", "label": "gated memo", "ticket": ""})
+        self.assertEqual(prior["color"], "purple")
+        self.assertEqual(prior["label"], "gated memo")
+        self.assertEqual(prior["ticket"], "")
+        # TK-11317 contrarian follow-up: carried_from/prior_revision provenance.
+        self.assertEqual(prior["carried_from"]["pid"], self.old_owner.pid)
+        self.assertEqual(prior["carried_from"]["started"], self.old_owner.started)
+        self.assertEqual(prior["carried_from"]["hops"], 1)
+        self.assertIsInstance(prior["carried_from"]["at"], (int, float))
+        self.assertEqual(prior["prior_revision"], 1)
 
     def test_prior_orange_record_carried(self):
         self.make_old_record("orange", "paste waiting")
@@ -164,8 +200,10 @@ class OwnerChangeCarryTests(unittest.TestCase):
     def test_carried_from_records_prior_owner_identity(self):
         self.make_old_record("purple", "gated memo")
         record = self.store.settle_owner_change(self.new_owner, rows=self.rows)
-        self.assertEqual(record["carried_from"],
-                         {"pid": self.old_owner.pid, "started": self.old_owner.started})
+        self.assertEqual(record["carried_from"]["pid"], self.old_owner.pid)
+        self.assertEqual(record["carried_from"]["started"], self.old_owner.started)
+        self.assertEqual(record["carried_from"]["hops"], 1)
+        self.assertIsInstance(record["carried_from"]["at"], (int, float))
 
     def test_load_accepts_records_with_and_without_carried_from(self):
         # load() must validate a record carrying the new optional fields exactly as
@@ -326,6 +364,194 @@ class OwnerChangeCarryTests(unittest.TestCase):
         with self.assertRaises(ts.StatusError):
             self.store.set(self.old_owner, "orange", "stale write", rows=self.rows)
 
+    # ---- CONTRARIAN REVIEW FOLLOW-UP (2026-09-26): CRITICAL hole #1 --------
+    # working-state.sh's per-prompt UserPromptSubmit hook fires
+    # `set green WORKING --deferential --boot` on EVERY prompt. Before this fix,
+    # set()'s deferential-yield guard required a truthy `previous`, but
+    # load(owner) returns None for reason owner_changed -- so the FIRST prompt
+    # after a tty reuse silently overwrote a carried purple/orange/yellow/
+    # lightblue with plain green. These tests are the exact repro.
+
+    def test_deferential_first_prompt_carries_purple_not_green(self):
+        self.make_old_record("purple", "gated memo")
+        record = self.store.set(self.new_owner, "green", "WORKING",
+                                deferential=True, rows=self.rows)
+        self.assertEqual(record["state"], "purple")
+        self.assertIn("gated memo", record["label"])
+        self.assertIn("carried_from", record)
+        # The record actually ON DISK (not just the return value) is the carry.
+        on_disk, reason = self.store.load(self.new_owner)
+        self.assertEqual(reason, "canonical")
+        self.assertEqual(on_disk["state"], "purple")
+
+    def test_deferential_first_prompt_unverified_yellow_stays_yellow(self):
+        # Prior owner had nothing pending (plain green) -- the deferential green
+        # call must land on the yellow "New owner - status needed" floor, not
+        # silently succeed at plain green.
+        self.make_old_record("green")
+        record = self.store.set(self.new_owner, "green", "WORKING",
+                                deferential=True, rows=self.rows)
+        self.assertEqual(record["state"], "yellow")
+        self.assertTrue(record["owner_status_unverified"])
+        on_disk, reason = self.store.load(self.new_owner)
+        self.assertEqual(on_disk["state"], "yellow")
+
+    def test_explicit_non_deferential_set_still_wins_over_owner_changed(self):
+        # "A NON-deferential explicit set by the new owner still wins" -- the
+        # presettle branch only engages for deferential=True. An explicit
+        # /greendot-style call is the new owner's own real status and must NOT
+        # be redirected into a carry.
+        self.make_old_record("purple", "gated memo")
+        record = self.store.set(self.new_owner, "green", "WORKING",
+                                deferential=False, rows=self.rows)
+        self.assertEqual(record["state"], "green")
+        self.assertNotIn("carried_from", record)
+
+    # ---- CONTRARIAN REVIEW FOLLOW-UP: age ceiling + provenance --------------
+
+    def test_stale_prior_purple_does_not_carry_falls_to_yellow(self):
+        self.make_old_record("purple", "TK-11317 · gated memo")
+        self.age_old_record(21 * 86400)  # 21 days -- far past the 24h ceiling
+        self.assertIsNone(self.store.prior_attention(self.new_owner))
+        record = self.store.settle_owner_change(self.new_owner, rows=self.rows)
+        self.assertEqual(record["state"], "yellow")
+        self.assertTrue(record["owner_status_unverified"])
+        self.assertEqual(record["ticket"], "")
+
+    def test_fresh_prior_purple_just_under_ceiling_still_carries(self):
+        self.make_old_record("purple", "gated memo")
+        self.age_old_record(ts.CARRY_MAX_AGE_S - 60)
+        prior = self.store.prior_attention(self.new_owner)
+        self.assertIsNotNone(prior)
+        self.assertEqual(prior["color"], "purple")
+
+    def test_prior_just_over_ceiling_does_not_carry(self):
+        self.make_old_record("purple", "gated memo")
+        self.age_old_record(ts.CARRY_MAX_AGE_S + 60)
+        self.assertIsNone(self.store.prior_attention(self.new_owner))
+
+    def test_fresh_legacy_dot_carried(self):
+        self.write_legacy("claude", "\U0001F7E0 paste waiting")  # freshly written = now
+        prior = self.store.prior_attention(self.new_owner)
+        self.assertIsNotNone(prior)
+        self.assertEqual(prior["color"], "orange")
+
+    def test_stale_legacy_dot_not_carried(self):
+        path = self.write_legacy("claude", "\U0001F7E0 paste waiting")
+        self.age_legacy(path, 21 * 86400)
+        self.assertIsNone(self.store.prior_attention(self.new_owner))
+
+    def test_three_generation_chain_keeps_original_carried_from_and_hops(self):
+        # Gen 1: owner A (old_owner) gets gated purple.
+        self.make_old_record("purple", "gated memo")
+        # Gen 2: owner B (new_owner, tty reused) settles, carrying A's attention.
+        gen2 = self.store.settle_owner_change(self.new_owner, rows=self.rows)
+        self.assertEqual(gen2["carried_from"]["pid"], self.old_owner.pid)
+        self.assertEqual(gen2["carried_from"]["hops"], 1)
+        # Gen 3: the SAME tty is reused again (owner B departs, owner C arrives).
+        gen3_process = ts.Process(333, 1, "ttys050", "Fri Sep 11 10:00:00 2026",
+                                  "/usr/bin/claude")
+        gen3_owner = gen3_process.owner()
+        gen3_rows = {333: gen3_process}
+        store3 = ts.Store(self.root, lambda **_: gen3_rows, lambda o, r: None)
+        prior = store3.prior_attention(gen3_owner)
+        self.assertIsNotNone(prior)
+        self.assertEqual(prior["color"], "purple")
+        # ORIGINAL provenance preserved -- pid/started/at are A's, not B's --
+        # only hops increments.
+        self.assertEqual(prior["carried_from"]["pid"], self.old_owner.pid)
+        self.assertEqual(prior["carried_from"]["started"], self.old_owner.started)
+        self.assertEqual(prior["carried_from"]["at"], gen2["carried_from"]["at"])
+        self.assertEqual(prior["carried_from"]["hops"], 2)
+        gen3 = store3.settle_owner_change(gen3_owner, rows=gen3_rows)
+        self.assertEqual(gen3["carried_from"]["pid"], self.old_owner.pid)
+        self.assertEqual(gen3["carried_from"]["started"], self.old_owner.started)
+        self.assertEqual(gen3["carried_from"]["hops"], 2)
+        self.assertEqual(gen3["revision"], 3)  # 1 (gen1) -> 2 (gen2) -> 3 (gen3)
+
+    # ---- CONTRARIAN REVIEW FOLLOW-UP: revision continuity (LOW) -------------
+
+    def test_revision_continues_across_carry_not_reset_to_one(self):
+        old_rec = self.make_old_record("purple", "gated memo")
+        self.assertEqual(old_rec["revision"], 1)
+        settled = self.store.settle_owner_change(self.new_owner, rows=self.rows)
+        self.assertEqual(settled["revision"], 2)  # continues, not reset to 1
+
+    def test_revision_resets_to_one_when_nothing_carried(self):
+        self.make_old_record("green")
+        settled = self.store.settle_owner_change(self.new_owner, rows=self.rows)
+        self.assertEqual(settled["revision"], 1)
+
+    # ---- CONTRARIAN REVIEW FOLLOW-UP: automatic-writer audit -----------------
+    # The `ticket` command handler AND auto-bind's _default_ticket_set both did
+    # `previous, _ = store.read(owner)` and silently defaulted to color "none"
+    # whenever reason was actually owner_changed -- the SECOND identified hole.
+    # effective_previous() closes it for both call sites.
+
+    def test_effective_previous_returns_settlement_preview_for_owner_changed(self):
+        self.make_old_record("purple", "gated memo")
+        record, reason = self.store.load(self.new_owner)
+        self.assertIsNone(record)
+        self.assertEqual(reason, "owner_changed")
+        previous, eff_reason = self.store.effective_previous(self.new_owner)
+        self.assertEqual(eff_reason, "owner_changed")
+        self.assertEqual(previous["state"], "purple")
+        self.assertIn("carried_from", previous)
+        self.assertEqual(previous["next_revision"], 2)
+
+    def test_effective_previous_passes_through_canonical_and_missing(self):
+        self.store.set(self.new_owner, "green", rows=self.rows)
+        previous, reason = self.store.effective_previous(self.new_owner)
+        self.assertEqual(reason, "canonical")
+        self.assertEqual(previous["state"], "green")
+        lone = ts.Process(444, 1, "ttys077", "Sat Sep 12 08:00:00 2026", "/usr/bin/claude")
+        previous2, reason2 = self.store.effective_previous(lone.owner())
+        self.assertEqual(reason2, "missing")
+        self.assertIsNone(previous2)
+
+    def test_ticket_command_preserves_carry_when_settle_has_not_run_yet(self):
+        # Reproduces the SECOND identified hole directly: binding a ticket on a
+        # reused tty BEFORE backfill/the deferential hook ever settled it must
+        # not blank the carried colour to "none".
+        self.make_old_record("purple", "gated memo")
+        record, reason = self.store.load(self.new_owner)
+        self.assertIsNone(record)
+        self.assertEqual(reason, "owner_changed")  # settle has NOT run yet
+        previous, eff_reason = self.store.effective_previous(self.new_owner)
+        self.assertEqual(eff_reason, "owner_changed")
+        bound = self.store.set(
+            self.new_owner, previous["state"],
+            ts.label_ticket(previous["label"])[0],
+            ticket_update="TK-22222",
+            variant=previous.get("variant", ""),
+            carried_from=previous.get("carried_from"),
+            owner_status_unverified=previous.get("owner_status_unverified"),
+            revision_override=previous.get("next_revision"),
+            rows=self.rows)
+        self.assertEqual(bound["state"], "purple")
+        self.assertEqual(bound["ticket"], "TK-22222")
+        self.assertIn("carried_from", bound)
+        self.assertEqual(bound["revision"], 2)  # continued, not reset to 1
+
+    def test_default_ticket_set_preserves_carried_purple_on_owner_changed(self):
+        self.make_old_record("purple", "gated memo")
+        ts._default_ticket_set(self.store, self.new_owner, "TK-33333")
+        record, reason = self.store.load(self.new_owner)
+        self.assertEqual(reason, "canonical")
+        self.assertEqual(record["state"], "purple")
+        self.assertEqual(record["ticket"], "TK-33333")
+        self.assertIn("carried_from", record)
+        self.assertEqual(record["revision"], 2)
+
+    def test_default_ticket_set_floors_to_yellow_when_nothing_carried(self):
+        self.make_old_record("green")
+        ts._default_ticket_set(self.store, self.new_owner, "TK-44444")
+        record, reason = self.store.load(self.new_owner)
+        self.assertEqual(reason, "canonical")
+        self.assertEqual(record["state"], "yellow")
+        self.assertTrue(record["owner_status_unverified"])
+        self.assertEqual(record["ticket"], "TK-44444")
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/verification/TK-11317-integration/after-tests-v2.txt b/verification/TK-11317-integration/after-tests-v2.txt
new file mode 100644
index 0000000..6cbcd3c
--- /dev/null
+++ b/verification/TK-11317-integration/after-tests-v2.txt
@@ -0,0 +1,250 @@
+=== python3 test_terminal_status.py ===
+test_ambiguous_multiple_tickets_in_prompt_falls_through_to_create (__main__.AutoBindTests.test_ambiguous_multiple_tickets_in_prompt_falls_through_to_create) ... ok
+test_concurrent_prompt_on_same_tty_cannot_double_create (__main__.AutoBindTests.test_concurrent_prompt_on_same_tty_cannot_double_create) ... ok
+test_explicit_ticket_in_prompt_binds_without_creating (__main__.AutoBindTests.test_explicit_ticket_in_prompt_binds_without_creating) ... ok
+test_explicit_ticket_not_in_known_ledger_fails_open (__main__.AutoBindTests.test_explicit_ticket_not_in_known_ledger_fails_open) ... ok
+test_fails_open_when_tk_errors_and_rate_limits_the_retry (__main__.AutoBindTests.test_fails_open_when_tk_errors_and_rate_limits_the_retry) ... ok
+test_never_binds_an_empty_ticket_id (__main__.AutoBindTests.test_never_binds_an_empty_ticket_id) ... ok
+test_never_creates_a_duplicate_for_the_same_session (__main__.AutoBindTests.test_never_creates_a_duplicate_for_the_same_session) ... ok
+test_slash_command_with_arguments_is_substantive (__main__.AutoBindTests.test_slash_command_with_arguments_is_substantive) ... ok
+test_trivial_prompt_never_consumes_the_rate_limit_window (__main__.AutoBindTests.test_trivial_prompt_never_consumes_the_rate_limit_window) ... ok
+test_trivial_prompts_never_create_a_ticket (__main__.AutoBindTests.test_trivial_prompts_never_create_a_ticket) ... ok
+test_unbound_plus_substantive_prompt_creates_and_binds (__main__.AutoBindTests.test_unbound_plus_substantive_prompt_creates_and_binds) ... ok
+test_cache_key_is_pid_reuse_safe (__main__.EnrichmentCacheTests.test_cache_key_is_pid_reuse_safe) ... ok
+test_inconclusive_read_is_never_cached_as_a_negative (__main__.EnrichmentCacheTests.test_inconclusive_read_is_never_cached_as_a_negative) ... ok
+test_merge_never_downgrades_a_peer_value (__main__.EnrichmentCacheTests.test_merge_never_downgrades_a_peer_value) ... ok
+test_merge_reads_fresh_and_keeps_a_concurrent_peers_entry (__main__.EnrichmentCacheTests.test_merge_reads_fresh_and_keeps_a_concurrent_peers_entry) ... ok
+test_non_string_cache_values_are_not_served (__main__.EnrichmentCacheTests.test_non_string_cache_values_are_not_served) ... ok
+test_stale_entries_age_out (__main__.EnrichmentCacheTests.test_stale_entries_age_out) ... ok
+test_unreadable_cache_degrades_to_the_uncached_answer (__main__.EnrichmentCacheTests.test_unreadable_cache_degrades_to_the_uncached_answer) ... ok
+test_warm_cache_is_exact_and_shells_out_zero_times (__main__.EnrichmentCacheTests.test_warm_cache_is_exact_and_shells_out_zero_times) ... ok
+test_another_users_cache_file_is_never_trusted (__main__.ItermEnumCacheTests.test_another_users_cache_file_is_never_trusted) ... ok
+test_blind_falls_back_to_the_cached_map_not_green (__main__.ItermEnumCacheTests.test_blind_falls_back_to_the_cached_map_not_green) ... ok
+test_empty_successful_enum_is_not_cached_so_siblings_re_enumerate (__main__.ItermEnumCacheTests.test_empty_successful_enum_is_not_cached_so_siblings_re_enumerate) ... ok
+test_fresh_forces_a_live_enumeration_past_the_cache (__main__.ItermEnumCacheTests.test_fresh_forces_a_live_enumeration_past_the_cache) ... ok
+test_single_flight_peer_holding_lock_coalesces_and_fires_no_osascript (__main__.ItermEnumCacheTests.test_single_flight_peer_holding_lock_coalesces_and_fires_no_osascript) ... ok
+test_single_flight_peer_lock_no_fresh_map_blinds_without_osascript (__main__.ItermEnumCacheTests.test_single_flight_peer_lock_no_fresh_map_blinds_without_osascript) ... ok
+test_stale_map_past_the_bound_is_not_trusted (__main__.ItermEnumCacheTests.test_stale_map_past_the_bound_is_not_trusted) ... ok
+test_success_enumerates_once_then_siblings_hit_the_cache (__main__.ItermEnumCacheTests.test_success_enumerates_once_then_siblings_hit_the_cache) ... ok
+test_truly_blind_with_no_cache_records_enum_blind_and_is_unavailable (__main__.ItermEnumCacheTests.test_truly_blind_with_no_cache_records_enum_blind_and_is_unavailable) ... ok
+test_all_colors_write_canonical_and_identical_mirrors (__main__.StatusTests.test_all_colors_write_canonical_and_identical_mirrors) ... ok
+test_ambiguous_legacy_gate_never_guesses_green (__main__.StatusTests.test_ambiguous_legacy_gate_never_guesses_green) ... ok
+test_busy_target_skips_fast_with_short_lock_wait (__main__.StatusTests.test_busy_target_skips_fast_with_short_lock_wait) ... ok
+test_canonical_green_beats_stale_orange_title_and_cache (__main__.StatusTests.test_canonical_green_beats_stale_orange_title_and_cache) ... ok
+test_clear_tombstone_prevents_title_cache_and_repaint_resurrection (__main__.StatusTests.test_clear_tombstone_prevents_title_cache_and_repaint_resurrection) ... ok
+test_complete_osc_packet_contains_one_consistent_title_and_badge (__main__.StatusTests.test_complete_osc_packet_contains_one_consistent_title_and_badge) ... ok
+test_concurrent_writers_keep_the_newest_event_and_valid_json (__main__.StatusTests.test_concurrent_writers_keep_the_newest_event_and_valid_json) ... ok
+test_control_sequences_cannot_be_injected_through_labels (__main__.StatusTests.test_control_sequences_cannot_be_injected_through_labels) ... ok
+test_corrupt_record_never_falls_back_to_green (__main__.StatusTests.test_corrupt_record_never_falls_back_to_green) ... ok
+test_dead_writer_cannot_modify_new_terminal_owner (__main__.StatusTests.test_dead_writer_cannot_modify_new_terminal_owner) ... ok
+test_declared_agent_binds_driving_ticket_when_slug_embeds_a_second_id (__main__.StatusTests.test_declared_agent_binds_driving_ticket_when_slug_embeds_a_second_id) ... ok
+test_declared_agent_needs_an_export_and_exactly_one_declaration (__main__.StatusTests.test_declared_agent_needs_an_export_and_exactly_one_declaration) ... ok
+test_declared_agent_never_invents_a_ticket_nor_launders_a_bare_session (__main__.StatusTests.test_declared_agent_never_invents_a_ticket_nor_launders_a_bare_session) ... ok
+test_event_clock_is_comparable_between_python_processes (__main__.StatusTests.test_event_clock_is_comparable_between_python_processes) ... ok
+test_explicit_lightblue_reason_wins_over_preserved_base (__main__.StatusTests.test_explicit_lightblue_reason_wins_over_preserved_base) ... ok
+test_headless_nested_agent_cannot_paint_parent (__main__.StatusTests.test_headless_nested_agent_cannot_paint_parent) ... ok
+test_ledger_discovery_rejects_reused_pid_and_nested_agent (__main__.StatusTests.test_ledger_discovery_rejects_reused_pid_and_nested_agent) ... ok
+test_legacy_is_runtime_scoped_and_stale_records_are_rejected (__main__.StatusTests.test_legacy_is_runtime_scoped_and_stale_records_are_rejected) ... ok
+test_lightblue_is_solid_when_no_reason_to_preserve (__main__.StatusTests.test_lightblue_is_solid_when_no_reason_to_preserve) ... ok
+test_lightblue_never_mismatches_old_label_with_new_ticket (__main__.StatusTests.test_lightblue_never_mismatches_old_label_with_new_ticket) ... ok
+test_lightblue_preserves_underlying_needs_steve_reason (__main__.StatusTests.test_lightblue_preserves_underlying_needs_steve_reason) ... ok
+test_monitoring_shade_remains_green_and_survives_repaint (__main__.StatusTests.test_monitoring_shade_remains_green_and_survives_repaint) ... ok
+test_new_assignment_wins_and_generic_hook_preserves_binding (__main__.StatusTests.test_new_assignment_wins_and_generic_hook_preserves_binding) ... ok
+test_old_request_cannot_overwrite_a_newer_clear (__main__.StatusTests.test_old_request_cannot_overwrite_a_newer_clear) ... ok
+test_owner_for_paint_bridge_marker_never_rescues_an_intermediate_agent (__main__.StatusTests.test_owner_for_paint_bridge_marker_never_rescues_an_intermediate_agent) ... ok
+test_owner_for_paint_bridge_marker_never_rescues_undeterminable_ownership (__main__.StatusTests.test_owner_for_paint_bridge_marker_never_rescues_undeterminable_ownership) ... ok
+test_owner_for_paint_bridge_session_paints_when_it_owns_its_tty_directly (__main__.StatusTests.test_owner_for_paint_bridge_session_paints_when_it_owns_its_tty_directly) ... ok
+test_owner_for_paint_rail_paints_top_level_refuses_subagent (__main__.StatusTests.test_owner_for_paint_rail_paints_top_level_refuses_subagent) ... ok
+test_paint_failure_is_reported_and_auditable (__main__.StatusTests.test_paint_failure_is_reported_and_auditable) ... ok
+test_pid_reuse_and_runtime_change_fail_closed (__main__.StatusTests.test_pid_reuse_and_runtime_change_fail_closed) ... ok
+test_pulse_marker_toggles_across_legitimate_repaints (__main__.StatusTests.test_pulse_marker_toggles_across_legitimate_repaints) ... ok
+test_repaint_never_adopts_unbound_legacy_state (__main__.StatusTests.test_repaint_never_adopts_unbound_legacy_state) ... ok
+test_repaint_never_changes_record_or_mirror_timestamps (__main__.StatusTests.test_repaint_never_changes_record_or_mirror_timestamps) ... ok
+test_runtime_identification_is_exact_not_command_substrings (__main__.StatusTests.test_runtime_identification_is_exact_not_command_substrings) ... ok
+test_scan_elevates_stopped_tab_above_same_colour_and_higher_colours (__main__.StatusTests.test_scan_elevates_stopped_tab_above_same_colour_and_higher_colours) ... ok
+test_scan_survives_terminal_api_outage_and_drops_dead_sessions (__main__.StatusTests.test_scan_survives_terminal_api_outage_and_drops_dead_sessions) ... ok
+test_session_ledger_binds_resumed_session_via_term_session_id (__main__.StatusTests.test_session_ledger_binds_resumed_session_via_term_session_id) ... ok
+test_session_ledger_is_non_vacuous_and_never_launders (__main__.StatusTests.test_session_ledger_is_non_vacuous_and_never_launders) ... ok
+test_ticket_header_is_read_only_and_does_not_resurrect_clear (__main__.StatusTests.test_ticket_header_is_read_only_and_does_not_resurrect_clear) ... ok
+test_ticket_survives_every_generic_color_and_clear (__main__.StatusTests.test_ticket_survives_every_generic_color_and_clear) ... ok
+test_ticket_validation_and_tty_reuse (__main__.StatusTests.test_ticket_validation_and_tty_reuse) ... ok
+test_two_ttys_do_not_share_status (__main__.StatusTests.test_two_ttys_do_not_share_status) ... ok
+test_waiting_shade_is_brighter_green_and_survives_repaint (__main__.StatusTests.test_waiting_shade_is_brighter_green_and_survives_repaint) ... ok
+test_waiting_states_pulse_and_solid_states_stay_solid (__main__.StatusTests.test_waiting_states_pulse_and_solid_states_stay_solid) ... ok
+test_explicit_ticket_extraction (__main__.TicketPromptHelperTests.test_explicit_ticket_extraction) ... ok
+test_short_topic_collapses_and_caps (__main__.TicketPromptHelperTests.test_short_topic_collapses_and_caps) ... ok
+test_trivial_prompt_guard (__main__.TicketPromptHelperTests.test_trivial_prompt_guard) ... ok
+
+----------------------------------------------------------------------
+Ran 75 tests in 4.933s
+
+OK
+
+=== python3 -m unittest test_owner_change_carry -v ===
+test_backfill_carries_purple_through_owner_changed (test_owner_change_carry.OwnerChangeCarryTests.test_backfill_carries_purple_through_owner_changed) ... ok
+test_backfill_never_flattens_carried_attention_to_green (test_owner_change_carry.OwnerChangeCarryTests.test_backfill_never_flattens_carried_attention_to_green) ... ok
+test_carried_from_records_prior_owner_identity (test_owner_change_carry.OwnerChangeCarryTests.test_carried_from_records_prior_owner_identity) ... ok
+test_default_ticket_set_floors_to_yellow_when_nothing_carried (test_owner_change_carry.OwnerChangeCarryTests.test_default_ticket_set_floors_to_yellow_when_nothing_carried) ... ok
+test_default_ticket_set_preserves_carried_purple_on_owner_changed (test_owner_change_carry.OwnerChangeCarryTests.test_default_ticket_set_preserves_carried_purple_on_owner_changed) ... ok
+test_deferential_first_prompt_carries_purple_not_green (test_owner_change_carry.OwnerChangeCarryTests.test_deferential_first_prompt_carries_purple_not_green) ... ok
+test_deferential_first_prompt_unverified_yellow_stays_yellow (test_owner_change_carry.OwnerChangeCarryTests.test_deferential_first_prompt_unverified_yellow_stays_yellow) ... ok
+test_deferential_green_keeps_carried_attention (test_owner_change_carry.OwnerChangeCarryTests.test_deferential_green_keeps_carried_attention) ... ok
+test_deferential_green_keeps_owner_status_unverified (test_owner_change_carry.OwnerChangeCarryTests.test_deferential_green_keeps_owner_status_unverified) ... ok
+test_effective_previous_passes_through_canonical_and_missing (test_owner_change_carry.OwnerChangeCarryTests.test_effective_previous_passes_through_canonical_and_missing) ... ok
+test_effective_previous_returns_settlement_preview_for_owner_changed (test_owner_change_carry.OwnerChangeCarryTests.test_effective_previous_returns_settlement_preview_for_owner_changed) ... ok
+test_explicit_non_deferential_set_still_wins_over_owner_changed (test_owner_change_carry.OwnerChangeCarryTests.test_explicit_non_deferential_set_still_wins_over_owner_changed) ... ok
+test_explicit_set_green_clears_carried_from (test_owner_change_carry.OwnerChangeCarryTests.test_explicit_set_green_clears_carried_from) ... ok
+test_explicit_set_green_clears_owner_status_unverified (test_owner_change_carry.OwnerChangeCarryTests.test_explicit_set_green_clears_owner_status_unverified) ... ok
+test_fresh_legacy_dot_carried (test_owner_change_carry.OwnerChangeCarryTests.test_fresh_legacy_dot_carried) ... ok
+test_fresh_prior_purple_just_under_ceiling_still_carries (test_owner_change_carry.OwnerChangeCarryTests.test_fresh_prior_purple_just_under_ceiling_still_carries) ... ok
+test_legacy_dot_only_attention_carried_when_json_prior_is_green (test_owner_change_carry.OwnerChangeCarryTests.test_legacy_dot_only_attention_carried_when_json_prior_is_green) ... ok
+test_legacy_dot_only_attention_carried_when_no_json_record_at_all (test_owner_change_carry.OwnerChangeCarryTests.test_legacy_dot_only_attention_carried_when_no_json_record_at_all) ... ok
+test_load_accepts_records_with_and_without_carried_from (test_owner_change_carry.OwnerChangeCarryTests.test_load_accepts_records_with_and_without_carried_from) ... ok
+test_missing_reason_still_green_floors_via_backfill (test_owner_change_carry.OwnerChangeCarryTests.test_missing_reason_still_green_floors_via_backfill) ... ok
+test_no_prior_record_at_all_settles_to_yellow_status_needed (test_owner_change_carry.OwnerChangeCarryTests.test_no_prior_record_at_all_settles_to_yellow_status_needed) ... ok
+test_old_owner_cannot_write_after_tty_reuse (test_owner_change_carry.OwnerChangeCarryTests.test_old_owner_cannot_write_after_tty_reuse) ... ok
+test_prior_green_yields_none_and_settles_to_yellow_status_needed (test_owner_change_carry.OwnerChangeCarryTests.test_prior_green_yields_none_and_settles_to_yellow_status_needed) ... ok
+test_prior_just_over_ceiling_does_not_carry (test_owner_change_carry.OwnerChangeCarryTests.test_prior_just_over_ceiling_does_not_carry) ... ok
+test_prior_lightblue_record_carried (test_owner_change_carry.OwnerChangeCarryTests.test_prior_lightblue_record_carried) ... ok
+test_prior_orange_record_carried (test_owner_change_carry.OwnerChangeCarryTests.test_prior_orange_record_carried) ... ok
+test_prior_pink_yields_none_and_settles_to_yellow_status_needed (test_owner_change_carry.OwnerChangeCarryTests.test_prior_pink_yields_none_and_settles_to_yellow_status_needed) ... ok
+test_prior_purple_record_carried (test_owner_change_carry.OwnerChangeCarryTests.test_prior_purple_record_carried) ... ok
+test_prior_record_ticket_is_carried_from_json_source (test_owner_change_carry.OwnerChangeCarryTests.test_prior_record_ticket_is_carried_from_json_source) ... ok
+test_prior_yellow_record_carried (test_owner_change_carry.OwnerChangeCarryTests.test_prior_yellow_record_carried) ... ok
+test_priority_choice_lightblue_outranks_everything (test_owner_change_carry.OwnerChangeCarryTests.test_priority_choice_lightblue_outranks_everything) ... ok
+test_priority_choice_when_json_and_legacy_disagree (test_owner_change_carry.OwnerChangeCarryTests.test_priority_choice_when_json_and_legacy_disagree) ... ok
+test_revision_continues_across_carry_not_reset_to_one (test_owner_change_carry.OwnerChangeCarryTests.test_revision_continues_across_carry_not_reset_to_one) ... ok
+test_revision_resets_to_one_when_nothing_carried (test_owner_change_carry.OwnerChangeCarryTests.test_revision_resets_to_one_when_nothing_carried) ... ok
+test_row_for_canonical_owner_status_unverified_record_carries_warning (test_owner_change_carry.OwnerChangeCarryTests.test_row_for_canonical_owner_status_unverified_record_carries_warning) ... ok
+test_row_for_owner_changed_shows_carried_colour_not_none (test_owner_change_carry.OwnerChangeCarryTests.test_row_for_owner_changed_shows_carried_colour_not_none) ... ok
+test_row_for_owner_changed_with_no_prior_attention_shows_yellow (test_owner_change_carry.OwnerChangeCarryTests.test_row_for_owner_changed_with_no_prior_attention_shows_yellow) ... ok
+test_row_never_renders_none_for_owner_changed (test_owner_change_carry.OwnerChangeCarryTests.test_row_never_renders_none_for_owner_changed) ... ok
+test_stale_legacy_dot_not_carried (test_owner_change_carry.OwnerChangeCarryTests.test_stale_legacy_dot_not_carried) ... ok
+test_stale_prior_purple_does_not_carry_falls_to_yellow (test_owner_change_carry.OwnerChangeCarryTests.test_stale_prior_purple_does_not_carry_falls_to_yellow) ... ok
+test_start_command_settles_owner_changed_via_carry (test_owner_change_carry.OwnerChangeCarryTests.test_start_command_settles_owner_changed_via_carry) ... ok
+test_three_generation_chain_keeps_original_carried_from_and_hops (test_owner_change_carry.OwnerChangeCarryTests.test_three_generation_chain_keeps_original_carried_from_and_hops) ... ok
+test_ticket_binding_preserves_carried_attention_and_flag (test_owner_change_carry.OwnerChangeCarryTests.test_ticket_binding_preserves_carried_attention_and_flag) ... ok
+test_ticket_binding_preserves_owner_status_unverified (test_owner_change_carry.OwnerChangeCarryTests.test_ticket_binding_preserves_owner_status_unverified) ... ok
+test_ticket_command_preserves_carry_when_settle_has_not_run_yet (test_owner_change_carry.OwnerChangeCarryTests.test_ticket_command_preserves_carry_when_settle_has_not_run_yet) ... ok
+
+----------------------------------------------------------------------
+Ran 45 tests in 1.133s
+
+OK
+
+=== python3 test_departed_asks.py -v ===
+test_ack_resolves_and_unknown_ack_is_refused (__main__.DepartedAsksTests.test_ack_resolves_and_unknown_ack_is_refused) ... ok
+test_cli_exit_code_tracks_unresolved (__main__.DepartedAsksTests.test_cli_exit_code_tracks_unresolved) ... ok
+test_cli_hides_resolved_unless_all (__main__.DepartedAsksTests.test_cli_hides_resolved_unless_all) ... ok
+test_deep_body_mention_is_weak_evidence (__main__.DepartedAsksTests.test_deep_body_mention_is_weak_evidence) ... ok
+test_done_ticket_without_memo_is_resolved (__main__.DepartedAsksTests.test_done_ticket_without_memo_is_resolved) ... ok
+test_filed_memo_is_not_open (__main__.DepartedAsksTests.test_filed_memo_is_not_open) ... ok
+test_legacy_only_carry_still_captures_ticket_mention (__main__.DepartedAsksTests.test_legacy_only_carry_still_captures_ticket_mention) ... ok
+test_memo_body_mention_counts (__main__.DepartedAsksTests.test_memo_body_mention_counts) ... ok
+test_mentions_word_boundaries (__main__.DepartedAsksTests.test_mentions_word_boundaries) ... ok
+test_open_memo_beats_done_ticket (__main__.DepartedAsksTests.test_open_memo_beats_done_ticket) ... ok
+test_open_memo_makes_ask_outstanding (__main__.DepartedAsksTests.test_open_memo_makes_ask_outstanding) ... ok
+test_open_ticket_without_memo_is_unverified_not_resolved (__main__.DepartedAsksTests.test_open_ticket_without_memo_is_unverified_not_resolved) ... ok
+test_register_failure_never_blocks_the_carry (__main__.DepartedAsksTests.test_register_failure_never_blocks_the_carry) ... ok
+test_register_is_idempotent_per_departed_owner (__main__.DepartedAsksTests.test_register_is_idempotent_per_departed_owner) ... ok
+test_register_survives_new_owner_explicit_set (__main__.DepartedAsksTests.test_register_survives_new_owner_explicit_set) ... ok
+test_seed_registers_carried_record_under_original_owner (__main__.DepartedAsksTests.test_seed_registers_carried_record_under_original_owner) ... ok
+test_seed_registers_dead_owner_attention_records_only (__main__.DepartedAsksTests.test_seed_registers_dead_owner_attention_records_only) ... ok
+test_settle_with_attention_registers_departed_ask (__main__.DepartedAsksTests.test_settle_with_attention_registers_departed_ask) ... ok
+test_settle_without_attention_registers_nothing (__main__.DepartedAsksTests.test_settle_without_attention_registers_nothing) ... ok
+test_unknown_ticket_and_no_ticket_are_unverified (__main__.DepartedAsksTests.test_unknown_ticket_and_no_ticket_are_unverified) ... ok
+
+----------------------------------------------------------------------
+Ran 20 tests in 0.164s
+
+OK
+
+=== python3 terminal_status.py selftest ===
+  PASS top-level interactive session PAINTS
+  PASS subagent REFUSES
+  PASS session-hook --boot PAINTS
+  PASS --force opt-in PAINTS
+  PASS bridge session owning its tty directly PAINTS
+  PASS intermediate claude process REFUSES
+  PASS undeterminable ownership REFUSES
+  PASS backfill repaints the owner WITH a record
+  PASS backfill NEVER flattens a valid gated dot to green
+  PASS backfill green-floors the recordless live owner
+  PASS backfill carries a prior purple record through owner_changed
+  PASS backfill floors owner_changed-from-green to yellow status-needed
+  PASS backfill owner_changed floors both new owners (f2 == 2)
+  PASS backfill missing floor still has no owner_status_unverified flag
+  PASS deferential first-prompt paint carries purple, never plain green
+  PASS heartbeat WARN + NOT-MEASURED when the ps scan failed
+  PASS heartbeat WARN when live tabs exist but NONE could be asserted
+  PASS heartbeat PASS when it acted on the live tabs
+  PASS heartbeat PASS on a measured 0-of-0 (no live tabs), still carrying the count
+selftest: PASS (19/19)
+
+=== python3 test_stop_verdict.py ===
+test_fixture_flag_with_base_override_never_touches_engine (__main__.CliTests.test_fixture_flag_with_base_override_never_touches_engine) ... ok
+test_injected_fault_goes_red (__main__.CliTests.test_injected_fault_goes_red) ... ok
+test_selftest_passes (__main__.CliTests.test_selftest_passes) ... ok
+test_stdin_missing_transcript_exits_zero (__main__.CliTests.test_stdin_missing_transcript_exits_zero) ... ok
+test_done_beats_silent_spinning_task (__main__.DecideTests.test_done_beats_silent_spinning_task) ... ok
+test_done_labels (__main__.DecideTests.test_done_labels) ... ok
+test_explicit_park_beats_waiting_on_steve (__main__.DecideTests.test_explicit_park_beats_waiting_on_steve) ... ok
+test_idle_pink_toggle (__main__.DecideTests.test_idle_pink_toggle) ... ok
+test_labels_respect_engine_valid_label (__main__.DecideTests.test_labels_respect_engine_valid_label) ... ok
+test_needs_steve_base_only_gets_stopped_variant (__main__.DecideTests.test_needs_steve_base_only_gets_stopped_variant) ... ok
+test_next_check_beats_done (__main__.DecideTests.test_next_check_beats_done) ... ok
+test_order_gated_beats_question (__main__.DecideTests.test_order_gated_beats_question) ... ok
+test_order_paste_beats_everything (__main__.DecideTests.test_order_paste_beats_everything) ... ok
+test_order_question_beats_waiting (__main__.DecideTests.test_order_question_beats_waiting) ... ok
+test_order_waiting_on_steve_beats_monitoring_and_done (__main__.DecideTests.test_order_waiting_on_steve_beats_monitoring_and_done) ... ok
+test_output_contract_keys (__main__.DecideTests.test_output_contract_keys) ... ok
+test_parked_base_only_repaints (__main__.DecideTests.test_parked_base_only_repaints) ... ok
+test_parked_beats_hold_and_done (__main__.DecideTests.test_parked_beats_hold_and_done) ... ok
+test_s3_keeps_base_label_when_no_time (__main__.DecideTests.test_s3_keeps_base_label_when_no_time) ... ok
+test_silent_spinning_task_alone_stays_green (__main__.DecideTests.test_silent_spinning_task_alone_stays_green) ... ok
+test_unknown_base_is_evaluated_like_green (__main__.DecideTests.test_unknown_base_is_evaluated_like_green) ... ok
+test_clean_view_strips_fences_code_tables_rules_emphasis (__main__.MatchTests.test_clean_view_strips_fences_code_tables_rules_emphasis) ... ok
+test_d1_captures_time (__main__.MatchTests.test_d1_captures_time) ... ok
+test_done_anywhere_in_closing_message (__main__.MatchTests.test_done_anywhere_in_closing_message) ... ok
+test_question_is_last_line_only (__main__.MatchTests.test_question_is_last_line_only) ... ok
+test_s2_time_is_the_next_check_not_the_done_time (__main__.MatchTests.test_s2_time_is_the_next_check_not_the_done_time) ... ok
+test_s3_comes_from_base_variant (__main__.MatchTests.test_s3_comes_from_base_variant) ... ok
+test_signal_table (__main__.MatchTests.test_signal_table) ... ok
+test_weekly_limit_matches_nothing (__main__.MatchTests.test_weekly_limit_matches_nothing) ... ok
+test_null_text_block_is_skipped (__main__.NullTextBlockTest.test_null_text_block_is_skipped) ... ok
+test_done_with_time_beats_parked_word (__main__.StdinFirstTests.test_done_with_time_beats_parked_word) ... ok
+test_live_background_task_keeps_green (__main__.StdinFirstTests.test_live_background_task_keeps_green) ... ok
+test_message_from_stdin_beats_missing_transcript (__main__.StdinFirstTests.test_message_from_stdin_beats_missing_transcript) ... ok
+test_no_message_returns_none (__main__.StdinFirstTests.test_no_message_returns_none) ... ok
+test_closing_text_is_after_last_user_line (__main__.TranscriptTests.test_closing_text_is_after_last_user_line) ... ok
+test_malformed_lines_are_skipped_and_no_text_is_none (__main__.TranscriptTests.test_malformed_lines_are_skipped_and_no_text_is_none) ... ok
+test_missing_transcript_is_benign (__main__.TranscriptTests.test_missing_transcript_is_benign) ... ok
+test_sidechain_and_noise_records_are_skipped (__main__.TranscriptTests.test_sidechain_and_noise_records_are_skipped) ... ok
+test_spinning_pairs_bg_start_with_notification (__main__.TranscriptTests.test_spinning_pairs_bg_start_with_notification) ... ok
+test_tail_lines_reads_only_the_end (__main__.TranscriptTests.test_tail_lines_reads_only_the_end) ... ok
+
+----------------------------------------------------------------------
+Ran 40 tests in 0.304s
+
+OK
+
+=== python3 test_tk_required_fp_metric.py ===
+test_argv_channel_also_contradicts (__main__.MetricTests.test_argv_channel_also_contradicts) ... ok
+test_clean_bound_session_passes (__main__.MetricTests.test_clean_bound_session_passes) ... ok
+test_contradicted_row_goes_red (__main__.MetricTests.test_contradicted_row_goes_red) ... ok
+test_empty_enumeration_is_warn_not_pass (__main__.MetricTests.test_empty_enumeration_is_warn_not_pass) ... ok
+test_launch_grace_defers_young_sessions_to_pending (__main__.MetricTests.test_launch_grace_defers_young_sessions_to_pending) ... ok
+test_ledger_ticket_must_be_known (__main__.MetricTests.test_ledger_ticket_must_be_known) ... ok
+test_not_measured_is_never_an_accusation_and_does_not_fail (__main__.MetricTests.test_not_measured_is_never_an_accusation_and_does_not_fail) ... ok
+test_only_green_sessions_are_scored (__main__.MetricTests.test_only_green_sessions_are_scored) ... ok
+test_proc_ages_resolve_for_a_live_pid (__main__.MetricTests.test_proc_ages_resolve_for_a_live_pid) ... ok
+test_read_events_are_noise_not_tracking (__main__.MetricTests.test_read_events_are_noise_not_tracking) ... ok
+test_self_test_entrypoint_runs (__main__.MetricTests.test_self_test_entrypoint_runs) ... ok
+test_unknown_age_is_not_hidden_as_young (__main__.MetricTests.test_unknown_age_is_not_hidden_as_young) ... ok
+test_unmeasured_input_is_warn_not_pass (__main__.MetricTests.test_unmeasured_input_is_warn_not_pass) ... ok
+
+----------------------------------------------------------------------
+Ran 13 tests in 0.010s
+
+OK
+self-test OK: contradicted→FAIL, clean→PASS, not_measured→PASS, young→pending
diff --git a/verification/TK-11317-integration/negative-control-v2.txt b/verification/TK-11317-integration/negative-control-v2.txt
new file mode 100644
index 0000000..5391c91
--- /dev/null
+++ b/verification/TK-11317-integration/negative-control-v2.txt
@@ -0,0 +1,197 @@
+test_backfill_carries_purple_through_owner_changed (test_owner_change_carry.OwnerChangeCarryTests.test_backfill_carries_purple_through_owner_changed) ... ok
+test_backfill_never_flattens_carried_attention_to_green (test_owner_change_carry.OwnerChangeCarryTests.test_backfill_never_flattens_carried_attention_to_green) ... ok
+test_carried_from_records_prior_owner_identity (test_owner_change_carry.OwnerChangeCarryTests.test_carried_from_records_prior_owner_identity) ... ERROR
+test_default_ticket_set_floors_to_yellow_when_nothing_carried (test_owner_change_carry.OwnerChangeCarryTests.test_default_ticket_set_floors_to_yellow_when_nothing_carried) ... FAIL
+test_default_ticket_set_preserves_carried_purple_on_owner_changed (test_owner_change_carry.OwnerChangeCarryTests.test_default_ticket_set_preserves_carried_purple_on_owner_changed) ... FAIL
+test_deferential_first_prompt_carries_purple_not_green (test_owner_change_carry.OwnerChangeCarryTests.test_deferential_first_prompt_carries_purple_not_green) ... FAIL
+test_deferential_first_prompt_unverified_yellow_stays_yellow (test_owner_change_carry.OwnerChangeCarryTests.test_deferential_first_prompt_unverified_yellow_stays_yellow) ... FAIL
+test_deferential_green_keeps_carried_attention (test_owner_change_carry.OwnerChangeCarryTests.test_deferential_green_keeps_carried_attention) ... ok
+test_deferential_green_keeps_owner_status_unverified (test_owner_change_carry.OwnerChangeCarryTests.test_deferential_green_keeps_owner_status_unverified) ... ok
+test_effective_previous_passes_through_canonical_and_missing (test_owner_change_carry.OwnerChangeCarryTests.test_effective_previous_passes_through_canonical_and_missing) ... ERROR
+test_effective_previous_returns_settlement_preview_for_owner_changed (test_owner_change_carry.OwnerChangeCarryTests.test_effective_previous_returns_settlement_preview_for_owner_changed) ... ERROR
+test_explicit_non_deferential_set_still_wins_over_owner_changed (test_owner_change_carry.OwnerChangeCarryTests.test_explicit_non_deferential_set_still_wins_over_owner_changed) ... ok
+test_explicit_set_green_clears_carried_from (test_owner_change_carry.OwnerChangeCarryTests.test_explicit_set_green_clears_carried_from) ... ok
+test_explicit_set_green_clears_owner_status_unverified (test_owner_change_carry.OwnerChangeCarryTests.test_explicit_set_green_clears_owner_status_unverified) ... ok
+test_fresh_legacy_dot_carried (test_owner_change_carry.OwnerChangeCarryTests.test_fresh_legacy_dot_carried) ... ok
+test_fresh_prior_purple_just_under_ceiling_still_carries (test_owner_change_carry.OwnerChangeCarryTests.test_fresh_prior_purple_just_under_ceiling_still_carries) ... ERROR
+test_legacy_dot_only_attention_carried_when_json_prior_is_green (test_owner_change_carry.OwnerChangeCarryTests.test_legacy_dot_only_attention_carried_when_json_prior_is_green) ... ok
+test_legacy_dot_only_attention_carried_when_no_json_record_at_all (test_owner_change_carry.OwnerChangeCarryTests.test_legacy_dot_only_attention_carried_when_no_json_record_at_all) ... ok
+test_load_accepts_records_with_and_without_carried_from (test_owner_change_carry.OwnerChangeCarryTests.test_load_accepts_records_with_and_without_carried_from) ... ok
+test_missing_reason_still_green_floors_via_backfill (test_owner_change_carry.OwnerChangeCarryTests.test_missing_reason_still_green_floors_via_backfill) ... ok
+test_no_prior_record_at_all_settles_to_yellow_status_needed (test_owner_change_carry.OwnerChangeCarryTests.test_no_prior_record_at_all_settles_to_yellow_status_needed) ... ok
+test_old_owner_cannot_write_after_tty_reuse (test_owner_change_carry.OwnerChangeCarryTests.test_old_owner_cannot_write_after_tty_reuse) ... ok
+test_prior_green_yields_none_and_settles_to_yellow_status_needed (test_owner_change_carry.OwnerChangeCarryTests.test_prior_green_yields_none_and_settles_to_yellow_status_needed) ... ok
+test_prior_just_over_ceiling_does_not_carry (test_owner_change_carry.OwnerChangeCarryTests.test_prior_just_over_ceiling_does_not_carry) ... ERROR
+test_prior_lightblue_record_carried (test_owner_change_carry.OwnerChangeCarryTests.test_prior_lightblue_record_carried) ... ok
+test_prior_orange_record_carried (test_owner_change_carry.OwnerChangeCarryTests.test_prior_orange_record_carried) ... ok
+test_prior_pink_yields_none_and_settles_to_yellow_status_needed (test_owner_change_carry.OwnerChangeCarryTests.test_prior_pink_yields_none_and_settles_to_yellow_status_needed) ... ok
+test_prior_purple_record_carried (test_owner_change_carry.OwnerChangeCarryTests.test_prior_purple_record_carried) ... ERROR
+test_prior_record_ticket_is_carried_from_json_source (test_owner_change_carry.OwnerChangeCarryTests.test_prior_record_ticket_is_carried_from_json_source) ... ok
+test_prior_yellow_record_carried (test_owner_change_carry.OwnerChangeCarryTests.test_prior_yellow_record_carried) ... ok
+test_priority_choice_lightblue_outranks_everything (test_owner_change_carry.OwnerChangeCarryTests.test_priority_choice_lightblue_outranks_everything) ... ok
+test_priority_choice_when_json_and_legacy_disagree (test_owner_change_carry.OwnerChangeCarryTests.test_priority_choice_when_json_and_legacy_disagree) ... ok
+test_revision_continues_across_carry_not_reset_to_one (test_owner_change_carry.OwnerChangeCarryTests.test_revision_continues_across_carry_not_reset_to_one) ... FAIL
+test_revision_resets_to_one_when_nothing_carried (test_owner_change_carry.OwnerChangeCarryTests.test_revision_resets_to_one_when_nothing_carried) ... ok
+test_row_for_canonical_owner_status_unverified_record_carries_warning (test_owner_change_carry.OwnerChangeCarryTests.test_row_for_canonical_owner_status_unverified_record_carries_warning) ... ok
+test_row_for_owner_changed_shows_carried_colour_not_none (test_owner_change_carry.OwnerChangeCarryTests.test_row_for_owner_changed_shows_carried_colour_not_none) ... ok
+test_row_for_owner_changed_with_no_prior_attention_shows_yellow (test_owner_change_carry.OwnerChangeCarryTests.test_row_for_owner_changed_with_no_prior_attention_shows_yellow) ... ok
+test_row_never_renders_none_for_owner_changed (test_owner_change_carry.OwnerChangeCarryTests.test_row_never_renders_none_for_owner_changed) ... ok
+test_stale_legacy_dot_not_carried (test_owner_change_carry.OwnerChangeCarryTests.test_stale_legacy_dot_not_carried) ... FAIL
+test_stale_prior_purple_does_not_carry_falls_to_yellow (test_owner_change_carry.OwnerChangeCarryTests.test_stale_prior_purple_does_not_carry_falls_to_yellow) ... FAIL
+test_start_command_settles_owner_changed_via_carry (test_owner_change_carry.OwnerChangeCarryTests.test_start_command_settles_owner_changed_via_carry) ... ok
+test_three_generation_chain_keeps_original_carried_from_and_hops (test_owner_change_carry.OwnerChangeCarryTests.test_three_generation_chain_keeps_original_carried_from_and_hops) ... ERROR
+test_ticket_binding_preserves_carried_attention_and_flag (test_owner_change_carry.OwnerChangeCarryTests.test_ticket_binding_preserves_carried_attention_and_flag) ... ok
+test_ticket_binding_preserves_owner_status_unverified (test_owner_change_carry.OwnerChangeCarryTests.test_ticket_binding_preserves_owner_status_unverified) ... ok
+test_ticket_command_preserves_carry_when_settle_has_not_run_yet (test_owner_change_carry.OwnerChangeCarryTests.test_ticket_command_preserves_carry_when_settle_has_not_run_yet) ... ERROR
+
+======================================================================
+ERROR: test_carried_from_records_prior_owner_identity (test_owner_change_carry.OwnerChangeCarryTests.test_carried_from_records_prior_owner_identity)
+----------------------------------------------------------------------
+Traceback (most recent call last):
+  File "/private/tmp/claude-501/-Users-macstudio3/81614591-265b-48ed-97b7-6518070fa586/scratchpad/b594dbb-check/test_owner_change_carry.py", line 205, in test_carried_from_records_prior_owner_identity
+    self.assertEqual(record["carried_from"]["hops"], 1)
+                     ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^
+KeyError: 'hops'
+
+======================================================================
+ERROR: test_effective_previous_passes_through_canonical_and_missing (test_owner_change_carry.OwnerChangeCarryTests.test_effective_previous_passes_through_canonical_and_missing)
+----------------------------------------------------------------------
+Traceback (most recent call last):
+  File "/private/tmp/claude-501/-Users-macstudio3/81614591-265b-48ed-97b7-6518070fa586/scratchpad/b594dbb-check/test_owner_change_carry.py", line 504, in test_effective_previous_passes_through_canonical_and_missing
+    previous, reason = self.store.effective_previous(self.new_owner)
+                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+AttributeError: 'Store' object has no attribute 'effective_previous'
+
+======================================================================
+ERROR: test_effective_previous_returns_settlement_preview_for_owner_changed (test_owner_change_carry.OwnerChangeCarryTests.test_effective_previous_returns_settlement_preview_for_owner_changed)
+----------------------------------------------------------------------
+Traceback (most recent call last):
+  File "/private/tmp/claude-501/-Users-macstudio3/81614591-265b-48ed-97b7-6518070fa586/scratchpad/b594dbb-check/test_owner_change_carry.py", line 496, in test_effective_previous_returns_settlement_preview_for_owner_changed
+    previous, eff_reason = self.store.effective_previous(self.new_owner)
+                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+AttributeError: 'Store' object has no attribute 'effective_previous'
+
+======================================================================
+ERROR: test_fresh_prior_purple_just_under_ceiling_still_carries (test_owner_change_carry.OwnerChangeCarryTests.test_fresh_prior_purple_just_under_ceiling_still_carries)
+----------------------------------------------------------------------
+Traceback (most recent call last):
+  File "/private/tmp/claude-501/-Users-macstudio3/81614591-265b-48ed-97b7-6518070fa586/scratchpad/b594dbb-check/test_owner_change_carry.py", line 423, in test_fresh_prior_purple_just_under_ceiling_still_carries
+    self.age_old_record(ts.CARRY_MAX_AGE_S - 60)
+                        ^^^^^^^^^^^^^^^^^^
+AttributeError: module 'terminal_status' has no attribute 'CARRY_MAX_AGE_S'
+
+======================================================================
+ERROR: test_prior_just_over_ceiling_does_not_carry (test_owner_change_carry.OwnerChangeCarryTests.test_prior_just_over_ceiling_does_not_carry)
+----------------------------------------------------------------------
+Traceback (most recent call last):
+  File "/private/tmp/claude-501/-Users-macstudio3/81614591-265b-48ed-97b7-6518070fa586/scratchpad/b594dbb-check/test_owner_change_carry.py", line 430, in test_prior_just_over_ceiling_does_not_carry
+    self.age_old_record(ts.CARRY_MAX_AGE_S + 60)
+                        ^^^^^^^^^^^^^^^^^^
+AttributeError: module 'terminal_status' has no attribute 'CARRY_MAX_AGE_S'
+
+======================================================================
+ERROR: test_prior_purple_record_carried (test_owner_change_carry.OwnerChangeCarryTests.test_prior_purple_record_carried)
+----------------------------------------------------------------------
+Traceback (most recent call last):
+  File "/private/tmp/claude-501/-Users-macstudio3/81614591-265b-48ed-97b7-6518070fa586/scratchpad/b594dbb-check/test_owner_change_carry.py", line 87, in test_prior_purple_record_carried
+    self.assertEqual(prior["carried_from"]["pid"], self.old_owner.pid)
+                     ~~~~~^^^^^^^^^^^^^^^^
+KeyError: 'carried_from'
+
+======================================================================
+ERROR: test_three_generation_chain_keeps_original_carried_from_and_hops (test_owner_change_carry.OwnerChangeCarryTests.test_three_generation_chain_keeps_original_carried_from_and_hops)
+----------------------------------------------------------------------
+Traceback (most recent call last):
+  File "/private/tmp/claude-501/-Users-macstudio3/81614591-265b-48ed-97b7-6518070fa586/scratchpad/b594dbb-check/test_owner_change_carry.py", line 450, in test_three_generation_chain_keeps_original_carried_from_and_hops
+    self.assertEqual(gen2["carried_from"]["hops"], 1)
+                     ~~~~~~~~~~~~~~~~~~~~^^^^^^^^
+KeyError: 'hops'
+
+======================================================================
+ERROR: test_ticket_command_preserves_carry_when_settle_has_not_run_yet (test_owner_change_carry.OwnerChangeCarryTests.test_ticket_command_preserves_carry_when_settle_has_not_run_yet)
+----------------------------------------------------------------------
+Traceback (most recent call last):
+  File "/private/tmp/claude-501/-Users-macstudio3/81614591-265b-48ed-97b7-6518070fa586/scratchpad/b594dbb-check/test_owner_change_carry.py", line 520, in test_ticket_command_preserves_carry_when_settle_has_not_run_yet
+    previous, eff_reason = self.store.effective_previous(self.new_owner)
+                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+AttributeError: 'Store' object has no attribute 'effective_previous'
+
+======================================================================
+FAIL: test_default_ticket_set_floors_to_yellow_when_nothing_carried (test_owner_change_carry.OwnerChangeCarryTests.test_default_ticket_set_floors_to_yellow_when_nothing_carried)
+----------------------------------------------------------------------
+Traceback (most recent call last):
+  File "/private/tmp/claude-501/-Users-macstudio3/81614591-265b-48ed-97b7-6518070fa586/scratchpad/b594dbb-check/test_owner_change_carry.py", line 551, in test_default_ticket_set_floors_to_yellow_when_nothing_carried
+    self.assertEqual(record["state"], "yellow")
+    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
+AssertionError: 'none' != 'yellow'
+- none
++ yellow
+
+
+======================================================================
+FAIL: test_default_ticket_set_preserves_carried_purple_on_owner_changed (test_owner_change_carry.OwnerChangeCarryTests.test_default_ticket_set_preserves_carried_purple_on_owner_changed)
+----------------------------------------------------------------------
+Traceback (most recent call last):
+  File "/private/tmp/claude-501/-Users-macstudio3/81614591-265b-48ed-97b7-6518070fa586/scratchpad/b594dbb-check/test_owner_change_carry.py", line 541, in test_default_ticket_set_preserves_carried_purple_on_owner_changed
+    self.assertEqual(record["state"], "purple")
+    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
+AssertionError: 'none' != 'purple'
+- none
++ purple
+
+
+======================================================================
+FAIL: test_deferential_first_prompt_carries_purple_not_green (test_owner_change_carry.OwnerChangeCarryTests.test_deferential_first_prompt_carries_purple_not_green)
+----------------------------------------------------------------------
+Traceback (most recent call last):
+  File "/private/tmp/claude-501/-Users-macstudio3/81614591-265b-48ed-97b7-6518070fa586/scratchpad/b594dbb-check/test_owner_change_carry.py", line 379, in test_deferential_first_prompt_carries_purple_not_green
+    self.assertEqual(record["state"], "purple")
+    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
+AssertionError: 'green' != 'purple'
+- green
++ purple
+
+
+======================================================================
+FAIL: test_deferential_first_prompt_unverified_yellow_stays_yellow (test_owner_change_carry.OwnerChangeCarryTests.test_deferential_first_prompt_unverified_yellow_stays_yellow)
+----------------------------------------------------------------------
+Traceback (most recent call last):
+  File "/private/tmp/claude-501/-Users-macstudio3/81614591-265b-48ed-97b7-6518070fa586/scratchpad/b594dbb-check/test_owner_change_carry.py", line 394, in test_deferential_first_prompt_unverified_yellow_stays_yellow
+    self.assertEqual(record["state"], "yellow")
+    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
+AssertionError: 'green' != 'yellow'
+- green
++ yellow
+
+
+======================================================================
+FAIL: test_revision_continues_across_carry_not_reset_to_one (test_owner_change_carry.OwnerChangeCarryTests.test_revision_continues_across_carry_not_reset_to_one)
+----------------------------------------------------------------------
+Traceback (most recent call last):
+  File "/private/tmp/claude-501/-Users-macstudio3/81614591-265b-48ed-97b7-6518070fa586/scratchpad/b594dbb-check/test_owner_change_carry.py", line 478, in test_revision_continues_across_carry_not_reset_to_one
+    self.assertEqual(settled["revision"], 2)  # continues, not reset to 1
+    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
+AssertionError: 1 != 2
+
+======================================================================
+FAIL: test_stale_legacy_dot_not_carried (test_owner_change_carry.OwnerChangeCarryTests.test_stale_legacy_dot_not_carried)
+----------------------------------------------------------------------
+Traceback (most recent call last):
+  File "/private/tmp/claude-501/-Users-macstudio3/81614591-265b-48ed-97b7-6518070fa586/scratchpad/b594dbb-check/test_owner_change_carry.py", line 442, in test_stale_legacy_dot_not_carried
+    self.assertIsNone(self.store.prior_attention(self.new_owner))
+    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+AssertionError: {'color': 'orange', 'label': 'paste waiting', 'ticket': ''} is not None
+
+======================================================================
+FAIL: test_stale_prior_purple_does_not_carry_falls_to_yellow (test_owner_change_carry.OwnerChangeCarryTests.test_stale_prior_purple_does_not_carry_falls_to_yellow)
+----------------------------------------------------------------------
+Traceback (most recent call last):
+  File "/private/tmp/claude-501/-Users-macstudio3/81614591-265b-48ed-97b7-6518070fa586/scratchpad/b594dbb-check/test_owner_change_carry.py", line 415, in test_stale_prior_purple_does_not_carry_falls_to_yellow
+    self.assertIsNone(self.store.prior_attention(self.new_owner))
+    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+AssertionError: {'color': 'purple', 'label': 'gated memo', 'ticket': 'TK-11317'} is not None
+
+----------------------------------------------------------------------
+Ran 45 tests in 2.365s
+
+FAILED (failures=7, errors=8)

← 468627f TK-12326: durable departed-asks register + resolver so a reu  ·  back to Terminal Status  ·  (newest)