← back to Terminal Status
terminal-status: keep lightblue label+ticket consistent (TK-11779) - bare set(lightblue) preserves both old label+ticket, explicit reason takes both new; +tests
e0a025a51079c2ecf546d3d9d25e6fd27f6cdbfc · 2026-09-15 18:16:40 -0700 · Steve
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pnon7dut9MfPekhhm9YY4Q
Files touched
M terminal_status.pyM test_terminal_status.py
Diff
commit e0a025a51079c2ecf546d3d9d25e6fd27f6cdbfc
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Sep 15 18:16:40 2026 -0700
terminal-status: keep lightblue label+ticket consistent (TK-11779) - bare set(lightblue) preserves both old label+ticket, explicit reason takes both new; +tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pnon7dut9MfPekhhm9YY4Q
---
terminal_status.py | 29 +++++++++++++++++++++-
test_terminal_status.py | 66 +++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 94 insertions(+), 1 deletion(-)
diff --git a/terminal_status.py b/terminal_status.py
index b11e54a..769c39d 100644
--- a/terminal_status.py
+++ b/terminal_status.py
@@ -629,11 +629,31 @@ class Store:
if ticket_update and (not re.fullmatch(r"TK-\d+", ticket_update) or
self.known_tickets is not None and ticket_update not in self.known_tickets):
raise StatusError("Ticket does not exist in the canonical ledger")
+ # An EXPLICIT caller reason (a label OR a ticket) must WIN over the T1-c
+ # preserve-path below. Captured before the default is applied, so a bare
+ # `set(lightblue)` (no reason) rides the base while `set(lightblue, "TK-x · why")`
+ # 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])
with self.lock(owner):
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]
# 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
@@ -752,6 +772,7 @@ class Store:
return result
label, _ = label_ticket(r["label"])
result.update(color=r["state"], label=status_title(r["state"], label, ticket["id"] or "TK REQUIRED", r.get("variant", "")) or "⚪ " + (ticket["id"] or "TK REQUIRED") + " · Status cleared",
+ variant=r.get("variant", ""),
updated_at=r.get("updated_at"), revision=r.get("revision"))
warnings = []
live_color = color_of(title)
@@ -832,7 +853,13 @@ def scan(store, rows=None, sessions=None):
row["terminal_visible"] = tty in sessions
row["terminal_api"] = ui_status
result.append(row)
- return sorted(result, key=lambda r: (PRIORITY.get(r["color"], len(PRIORITY)), r["tty"]))
+ # A "stopped" (needs-Steve) tab keeps its specific base colour (TK-11779) but must not
+ # lose the top-of-list rank the umbrella lightblue used to give it: PRIORITY keys only
+ # on colour, so a purple+stopped tab would otherwise sort level with a plain gated tab.
+ # Elevate the stopped group ahead of everything else, then order within it by base
+ # colour, so a preserved-reason stop still ranks above a same-colour non-stopped tab.
+ return sorted(result, key=lambda r: (0 if r.get("variant") == "stopped" else 1,
+ PRIORITY.get(r["color"], len(PRIORITY)), r["tty"]))
def main(argv=None):
diff --git a/test_terminal_status.py b/test_terminal_status.py
index 30279d7..4b86bc2 100644
--- a/test_terminal_status.py
+++ b/test_terminal_status.py
@@ -83,6 +83,52 @@ class StatusTests(unittest.TestCase):
self.assertEqual(self.paints[-1]["variant"], "monitoring")
self.assertEqual(self.store.row(self.owner)["color"], "green")
+ def test_lightblue_preserves_underlying_needs_steve_reason(self):
+ # T1-c (TK-11779): an explicit lightblue on a purple/orange/yellow base must NOT
+ # erase the specific reason — it rides as the additive 🔵 stopped marker on the
+ # preserved base colour, so a later resume reveals the still-pending reason.
+ for base in ("purple", "orange", "yellow"):
+ with self.subTest(base=base):
+ self.store.set(self.owner, base, "TK-11779 · the real reason")
+ r = self.store.set(self.owner, "lightblue")
+ self.assertEqual(r["state"], base, "base colour must be preserved")
+ self.assertEqual(r["variant"], "stopped", "needs-Steve rides as 🔵 marker")
+ self.assertEqual(r["label"], "the real reason", "reason label preserved")
+ self.assertEqual(r["ticket"], "TK-11779", "ticket preserved")
+ self.assertIn(ts.STOPPED_MARK, r["title"], "🔵 stopped marker shown")
+ # resuming (clear the stopped variant) reveals the still-pending reason
+ back = self.store.set_variant(self.owner, "")
+ self.assertEqual(back["state"], base)
+ self.assertEqual(back["variant"], "")
+ self.store.set(self.owner, "none") # reset for next subTest
+
+ def test_lightblue_is_solid_when_no_reason_to_preserve(self):
+ # A green/pink/none base carries no needs-Steve reason, so lightblue paints solid.
+ for base, seed in (("green", lambda: self.store.set(self.owner, "green", "Working")),
+ ("pink", lambda: self.store.set(self.owner, "pink", "parked")),
+ ("none", lambda: None)):
+ with self.subTest(base=base):
+ seed()
+ r = self.store.set(self.owner, "lightblue")
+ self.assertEqual(r["state"], "lightblue")
+ self.assertEqual(r["variant"], "")
+ self.store.set(self.owner, "none")
+
+ def test_explicit_lightblue_reason_wins_over_preserved_base(self):
+ # An explicit `set(lightblue, "TK-new · reason")` on a purple/orange/yellow base
+ # must NOT staple the NEW ticket onto the OLD label. The caller's reason wins:
+ # paints a solid lightblue carrying the new label AND the new ticket, consistently.
+ for base in ("purple", "orange", "yellow"):
+ with self.subTest(base=base):
+ self.store.set(self.owner, base, "TK-11779 · the OLD reason")
+ r = self.store.set(self.owner, "lightblue", "TK-11780 · the NEW reason")
+ self.assertEqual(r["state"], "lightblue", "explicit reason paints solid lightblue")
+ self.assertEqual(r["variant"], "")
+ self.assertEqual(r["label"], "the NEW reason", "new label must not be dropped")
+ self.assertEqual(r["ticket"], "TK-11780", "new ticket must not pair with old label")
+ self.assertNotIn("OLD", r["title"], "no leak of the preserved base's reason")
+ self.store.set(self.owner, "none")
+
def test_ledger_discovery_rejects_reused_pid_and_nested_agent(self):
self.rows[124] = ts.Process(124, 123, "??", self.process.started, "/bin/node")
self.rows[125] = ts.Process(125, 123, "??", self.process.started, "/bin/codex")
@@ -395,6 +441,26 @@ class StatusTests(unittest.TestCase):
self.rows = {}
self.assertEqual(ts.scan(self.store, sessions={"ttys010": "🟢 stale"}), [])
+ def test_scan_elevates_stopped_tab_above_same_colour_and_higher_colours(self):
+ # TK-11779 follow-up: a preserved-reason needs-Steve stop keeps its base colour but
+ # must still rank at the top. Build three sessions: a plain orange (PRIORITY 1, the
+ # highest base colour here), a plain purple (PRIORITY 2), and a purple that was
+ # explicitly declared needs-Steve (stored purple + variant=stopped).
+ other = dataclasses.replace(self.process, pid=200, tty="ttys011")
+ third = dataclasses.replace(self.process, pid=201, tty="ttys012")
+ self.rows[200] = other
+ self.rows[201] = third
+ self.store.set(other.owner(), "orange", "paste waiting") # ttys011
+ self.store.set(third.owner(), "purple", "plain gated") # ttys012
+ self.store.set(self.owner, "purple", "TK-11779 · the reason") # ttys010 base
+ stopped = self.store.set(self.owner, "lightblue") # -> purple+stopped
+ self.assertEqual((stopped["state"], stopped["variant"]), ("purple", "stopped"))
+ order = [(r["color"], r.get("variant", "")) for r in ts.scan(self.store, sessions={})]
+ # The stopped tab leads despite being purple, ahead of both the higher-priority
+ # plain orange and the same-colour plain purple.
+ self.assertEqual(order[0], ("purple", "stopped"))
+ self.assertEqual(order[1:], [("orange", ""), ("purple", "")])
+
def test_runtime_identification_is_exact_not_command_substrings(self):
self.rows[456] = ts.Process(456, 1, "ttys011", self.owner.started,
"/usr/bin/not-codex")
← df6f28c T2 (TK-11779): add dead-tty .dot/.json reaper (ps-liveness a
·
back to Terminal Status
·
TK-11794: pulse the 4 needs-Steve dot states; green/pink sta 585ee06 →