← 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
M test_terminal_status.pyM ticket_binding.py
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 →