[object Object]

← back to Terminal Status

require `export ` + a single declaration before binding a TK_AGENT (TK-11631)

b85d85b85fe933c6d5891f5fc3fa6cb8d4d09b96 · 2026-09-13 16:55:26 -0700 · Steve Abrams

Follow-up to 2315808, which was too loose in two ways the contrarian caught:

  * it matched a bare `TK_AGENT=` ANYWHERE in argv, so a prose mention bound a
    ticket: "fix the bug where TK_AGENT=claude-run-11630 fails to bind" bound
    TK-11630 with no export and no launch;
  * it took re.search's FIRST hit with no uniqueness test, making it LESS
    conservative than the len(ids)==1 rule it front-runs. The argv of an agent
    working this very ticket carries three mentions and bound the right one only
    by accident of sentence order -- reverse the sentences and it binds another
    session's ticket. That is the self-match trap in the memory note
    detector-argv-substring-self-match, in the one place it hurts most.

The match now requires BOTH the launcher's literal shape (`export ` prefix and
the trailing period, verified at run-ticket.sh:60 and against 44 of 44 live
sessions) and exactly ONE DISTINCT declared id via findall. Two declarations
abstain to the old rule rather than picking one. The in-known guard is unchanged.
`ps -o args=` flattens argv, so the prompt argument's own start is not locatable;
this literal is the anchor instead of a positional hack.

Controlled live measurement, discover() run three times over ONE shared rows +
argv snapshot: AGENT off = 41 bound, loose = 44, tight = 44; rescues 3, rebound
0, lost 0 in both directions. The tightening removes false-bind CAPABILITY and
changes nothing on today's population -- no live session carries a bad shape.

Each half is proven load-bearing by an injected fault: dropping `export `,
dropping the period, or dropping uniqueness each reddens the new test; dropping
the in-known guard reddens the anti-invention test; removing the source reddens
the positive test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011yMsG8b5h9zymuwmtabAMJ

Files touched

Diff

commit b85d85b85fe933c6d5891f5fc3fa6cb8d4d09b96
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Sep 13 16:55:26 2026 -0700

    require `export ` + a single declaration before binding a TK_AGENT (TK-11631)
    
    Follow-up to 2315808, which was too loose in two ways the contrarian caught:
    
      * it matched a bare `TK_AGENT=` ANYWHERE in argv, so a prose mention bound a
        ticket: "fix the bug where TK_AGENT=claude-run-11630 fails to bind" bound
        TK-11630 with no export and no launch;
      * it took re.search's FIRST hit with no uniqueness test, making it LESS
        conservative than the len(ids)==1 rule it front-runs. The argv of an agent
        working this very ticket carries three mentions and bound the right one only
        by accident of sentence order -- reverse the sentences and it binds another
        session's ticket. That is the self-match trap in the memory note
        detector-argv-substring-self-match, in the one place it hurts most.
    
    The match now requires BOTH the launcher's literal shape (`export ` prefix and
    the trailing period, verified at run-ticket.sh:60 and against 44 of 44 live
    sessions) and exactly ONE DISTINCT declared id via findall. Two declarations
    abstain to the old rule rather than picking one. The in-known guard is unchanged.
    `ps -o args=` flattens argv, so the prompt argument's own start is not locatable;
    this literal is the anchor instead of a positional hack.
    
    Controlled live measurement, discover() run three times over ONE shared rows +
    argv snapshot: AGENT off = 41 bound, loose = 44, tight = 44; rescues 3, rebound
    0, lost 0 in both directions. The tightening removes false-bind CAPABILITY and
    changes nothing on today's population -- no live session carries a bad shape.
    
    Each half is proven load-bearing by an injected fault: dropping `export `,
    dropping the period, or dropping uniqueness each reddens the new test; dropping
    the in-known guard reddens the anti-invention test; removing the source reddens
    the positive test.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011yMsG8b5h9zymuwmtabAMJ
---
 test_terminal_status.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++---
 ticket_binding.py       | 38 +++++++++++++++++++++------
 2 files changed, 95 insertions(+), 12 deletions(-)

diff --git a/test_terminal_status.py b/test_terminal_status.py
index 06cb918..8aefb6a 100644
--- a/test_terminal_status.py
+++ b/test_terminal_status.py
@@ -126,10 +126,11 @@ class StatusTests(unittest.TestCase):
         self.ledger("TK-11630-tk-11340-follow-on-2-live-gated-memos-we", "TK-11317")
         # The pre-existing rule cannot bind this argv: the slug embeds a second id.
         self.assertEqual(len(set(re.findall(r"\bTK-\d+\b", self.RUN_TICKET_ARGV, re.I))), 2)
-        # The prompt sentence puts a period immediately after the token, so a greedy
-        # capture yields "claude-run-11630." and binds nothing. The digits are anchored.
-        self.assertIn("TK_AGENT=claude-run-11630. You are", self.RUN_TICKET_ARGV)
-        self.assertEqual(ts.tickets.AGENT.search(self.RUN_TICKET_ARGV)[1], "11630")
+        # The launcher emits `export ` then the token then a literal period, and the
+        # digits are anchored between them: a greedy capture would yield
+        # "claude-run-11630." and bind nothing.
+        self.assertIn("export TK_AGENT=claude-run-11630. You are", self.RUN_TICKET_ARGV)
+        self.assertEqual(ts.tickets.AGENT.findall(self.RUN_TICKET_ARGV), ["11630"])
         found, known = self.bind({123: self.RUN_TICKET_ARGV,
                                   124: "claude --model opus Please drive TK-11317 to done"})
         self.assertEqual(found.get("ttys010"), {"id": "TK-11630", "at": self.owner.epoch,
@@ -156,6 +157,66 @@ class StatusTests(unittest.TestCase):
         # TK-11369/TK-11505: an unavailable `ps` degrades the label, never the paint.
         self.assertEqual(self.bind({})[0], {})
 
+    def test_declared_agent_needs_an_export_and_exactly_one_declaration(self):
+        # TK-11631 follow-up. The first cut matched a bare `TK_AGENT=` anywhere and took
+        # re.search's FIRST hit, so an agent whose prompt merely NAMED the ticket it was
+        # debugging got bound to it (memory: detector-argv-substring-self-match). Every
+        # argv below is a real shape; `driving` is the only session actually launched for
+        # a ticket. Expected value is (id, source) or None for "nothing binds at all".
+        self.ledger("TK-11631-chronic-x", "TK-11630-tk-11340-x", "TK-11076-tk-11073-x")
+        agent, argument = "main_process_agent", "main_process_argument"
+        cases = {
+            "driving": (
+                "claude --model opus export TK_AGENT=claude-run-11631. You are driving "
+                "ticket TK-11631-chronic-x to completion.", ("TK-11631", agent)),
+            # A prose mention with no `export ` must not reach the new source at all.
+            # The loose regex bound TK-11630 here off nothing but a sentence; now NOTHING
+            # binds, because the old rule also abstains (the slug holds two bare ids).
+            "prose_only": (
+                "claude fix the bug where TK_AGENT=claude-run-11630 fails to bind on "
+                "TK-11630-tk-11340-x", None),
+            # Same prose mention, but a single bare id the OLD rule can still bind: proves
+            # that rule is untouched, and that the prose TK_AGENT did not steer it.
+            "prose_only_old_rule_unaffected": (
+                "claude fix the bug where TK_AGENT=claude-run-11630 fails to bind on "
+                "TK-11631", ("TK-11631", argument)),
+            "export_plus_prose": (
+                "claude export TK_AGENT=claude-run-11631. Compare against "
+                "TK_AGENT=claude-run-99999 behaviour", ("TK-11631", agent)),
+            # The argv of an agent working THIS ticket: three mentions, one export.
+            "export_plus_two_prose": (
+                "claude --model opus export TK_AGENT=claude-run-11631. Root-cause why "
+                "TK_AGENT=claude-run-11630 and TK_AGENT=claude-run-11076 read "
+                "TK REQUIRED", ("TK-11631", agent)),
+            # Identical content, prose FIRST: proves the bind is not order-luck.
+            "prose_before_export": (
+                "claude --model opus TK_AGENT=claude-run-11630 and "
+                "TK_AGENT=claude-run-11076 read TK REQUIRED. Root-cause: export "
+                "TK_AGENT=claude-run-11631. Fix", ("TK-11631", agent)),
+            # Two real declarations are ambiguous: abstain, never pick one.
+            "two_exports": (
+                "claude export TK_AGENT=claude-run-11631. Then export "
+                "TK_AGENT=claude-run-11630. Compare", None),
+            # `export ` present but no trailing period: not the launcher's shape.
+            "export_without_period": (
+                "claude the launcher should export TK_AGENT=claude-run-11630 but does "
+                "not", None),
+            # Trailing period present but NO `export `: a sentence that simply ends on
+            # the token. Isolates the `export ` half of the anchor -- without it this
+            # case binds another session's ticket off prose.
+            "period_without_export": (
+                "claude why does the tab for TK_AGENT=claude-run-11630. read "
+                "TK REQUIRED", None),
+        }
+        for name, (command, expected) in cases.items():
+            with self.subTest(name):
+                bound = self.bind({123: command})[0].get("ttys010")
+                actual = (bound["id"], bound["source"]) if bound else None
+                self.assertEqual(actual, expected)
+        # The two that MUST differ from the loose regex do so for opposite reasons.
+        self.assertEqual(ts.tickets.AGENT.findall(cases["prose_only"][0]), [])
+        self.assertEqual(len(set(ts.tickets.AGENT.findall(cases["two_exports"][0]))), 2)
+
     def test_all_colors_write_canonical_and_identical_mirrors(self):
         for color in ts.COLORS:
             r = self.store.set(self.owner, color, "" if color == "none" else "Task")
diff --git a/ticket_binding.py b/ticket_binding.py
index d4ac9df..2c8fce0 100644
--- a/ticket_binding.py
+++ b/ticket_binding.py
@@ -14,7 +14,7 @@ SHORT = re.compile(r"^TK-\d+(?=$|-)", re.I)
 CID = re.compile(r"^(?:assign|create|action)-[a-z0-9]+-(\d+)-[a-z0-9]+$")
 
 # TK-11631: the LAUNCHER-DECLARED ticket. run-ticket.sh bakes
-# `export TK_AGENT=<AGENT_PREFIX>-<IDNUM>` into the launch prompt, where IDNUM is
+# `export TK_AGENT=<AGENT_PREFIX>-<IDNUM>.` into the launch prompt, where IDNUM is
 # ${ID#TK-} truncated at the first dash -- i.e. the DRIVING ticket's number alone,
 # never a referenced one. That makes it unambiguous BY CONSTRUCTION, unlike the
 # bare-id scan in discover(): the house slug convention embeds a referenced ticket
@@ -23,8 +23,29 @@ CID = re.compile(r"^(?:assign|create|action)-[a-z0-9]+-(\d+)-[a-z0-9]+$")
 # and the tab reads "TK REQUIRED" for that session's entire life even while it logs
 # real work on the board (54 of 1783 tickets, 3.0%, carry that shape). The prefixes
 # are exactly the AGENT_PREFIX values run-ticket.sh can emit.
-AGENT = re.compile(r"\bTK_AGENT=(?:claude-run|codex-run|local-qwen-27b-run"
-                   r"|local-qwen-14b-run|local-qwen-14b-mac1-run)-(\d+)\b", re.I)
+#
+# TK-11631 follow-up -- the first cut matched a bare `TK_AGENT=` anywhere in argv and
+# took re.search's FIRST hit, which is the self-match trap from the memory note
+# `detector-argv-substring-self-match`: an agent whose prompt merely NAMES the thing
+# it is debugging got bound to it. Measured on four real argv shapes: a prose mention
+# ("fix the bug where TK_AGENT=claude-run-11630 fails to bind") bound 11630 with no
+# export at all, and the argv of an agent working THIS ticket carried three matches
+# and bound the right one only by accident of sentence order. So the match now needs
+# BOTH halves, and either one alone is insufficient:
+#   1. the literal `export ` prefix and the trailing `.` -- verified against the
+#      launcher, not assumed: run-ticket.sh:60 is the first line of the PROMPT
+#      heredoc and reads `export TK_AGENT=${AGENT_PREFIX}-${IDNUM}. You are driving`,
+#      unconditionally, and 44 of 44 live sessions carry exactly that shape. `ps
+#      -o args=` flattens argv into one space-joined string, so the prompt argument's
+#      own start cannot be located reliably -- this literal IS the anchor, in place of
+#      a positional hack that would only look rigorous.
+#   2. exactly ONE DISTINCT declared id (findall, not search) -- mirroring the
+#      len(ids)==1 rule this front-runs, so the new source can never be less
+#      conservative than the rule it defers to. Two declarations abstain.
+# If the launcher's wording ever changes, every binding falls back to "TK REQUIRED",
+# which is the honest not-measured state -- never a confident wrong ticket.
+AGENT = re.compile(r"\bexport TK_AGENT=(?:claude-run|codex-run|local-qwen-27b-run"
+                   r"|local-qwen-14b-run|local-qwen-14b-mac1-run)-(\d+)\.", re.I)
 
 
 def short(value):
@@ -106,11 +127,12 @@ def discover(root, rows, live, chain, argv=None):
         command = (argv or {}).get(owner.pid, "")
         # The launcher's own declaration is tried FIRST: it names only the driving
         # ticket, so it survives the slug collision that makes the scan below
-        # abstain. Same safety condition as that scan -- a declared id that is not
-        # a ticket we have seen created is ignored, so this can never invent one.
-        declared = AGENT.search(command)
-        if declared and "TK-" + declared[1] in known:
-            result[tty] = {"id": "TK-" + declared[1], "at": owner.epoch,
+        # abstain. Same safety conditions as that scan -- exactly one distinct id,
+        # and that id must be a ticket we have seen created -- so this can neither
+        # invent a ticket nor pick one of several by position.
+        declared = set(AGENT.findall(command))
+        if len(declared) == 1 and "TK-" + next(iter(declared)) in known:
+            result[tty] = {"id": "TK-" + next(iter(declared)), "at": owner.epoch,
                            "source": "main_process_agent"}
             continue
         ids = set(re.findall(r"\bTK-\d+\b", command, re.I))

← 2315808 bind a session by its launcher-declared TK_AGENT, so a slug  ·  back to Terminal Status  ·  TK-11672: fix permanently-red terminal-status test via injec 664d1de →