@junghanacs/entwurf 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/AGENTS.md +2 -2
  2. package/BASELINE.md +3 -1
  3. package/CHANGELOG.md +278 -0
  4. package/DELIVERY.md +156 -26
  5. package/README.md +64 -13
  6. package/VERIFY.md +67 -11
  7. package/docs/external-mcp-host.md +16 -6
  8. package/docs/setup-clean-host.md +63 -22
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +40 -16
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +194 -1
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +160 -25
  15. package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
  16. package/mcp/entwurf-bridge/src/index.ts +50 -16
  17. package/mcp/entwurf-bridge/tsconfig.build.json +9 -0
  18. package/package.json +2 -2
  19. package/pi-extensions/entwurf-control.ts +4 -4
  20. package/pi-extensions/lib/codex-caller-seat.ts +204 -0
  21. package/pi-extensions/lib/codex-fresh-preflight.ts +218 -1
  22. package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
  23. package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
  24. package/pi-extensions/lib/mux-fresh-call.ts +196 -37
  25. package/run.sh +127 -3
  26. package/scripts/check-codex-app-server-launch.ts +445 -0
  27. package/scripts/check-entwurf-v2-production.ts +42 -1
  28. package/scripts/check-entwurf-v2-send.ts +26 -7
  29. package/scripts/check-gate-qualification.ts +4 -2
  30. package/scripts/check-mux-launch-tmux.ts +331 -35
  31. package/scripts/codex-app-server-launch.sh +275 -0
  32. package/scripts/codex-socket-path.ts +33 -0
  33. package/scripts/codex-terminal-title-config.py +500 -0
  34. package/scripts/codex_toml_io.py +121 -0
  35. package/scripts/lib/codex-fresh-live-protocol.ts +11 -3
  36. package/scripts/lib/codex-fresh-source-receipts.ts +29 -2
  37. package/scripts/mutants/codex-app-server-launch.json +157 -0
  38. package/scripts/mutants/codex-caller-seat.json +336 -0
  39. package/scripts/mutants/codex-native.json +3 -3
  40. package/scripts/mutants/mux-fresh-call.json +86 -14
  41. package/scripts/mutants/v2-surface.json +22 -0
  42. package/scripts/smoke-codex-config-state.sh +192 -3
  43. package/scripts/smoke-codex-fresh-live.ts +277 -37
  44. package/scripts/smoke-entwurf-chain-live.ts +50 -0
  45. package/scripts/smoke-setup-verdict.sh +13 -11
@@ -111,7 +111,7 @@
111
111
  "claim": "FRESHCALL-CWD-RECEIPT-REQUESTED",
112
112
  "title": "production freshCall stops assembling the requested cwd into its receipt, so the operator-visible answer silently loses the one placement fact the caller asked for",
113
113
  "subject": "pi-extensions/lib/mux-fresh-call.ts",
114
- "find": ["\t\t\t...(cwd === undefined ? {} : { cwd }),\n"],
114
+ "find": ["\t\t\t...(chosenCwd === undefined ? {} : { cwd: chosenCwd.value, cwdSource: chosenCwd.source }),\n"],
115
115
  "replace": [""],
116
116
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
117
117
  "timeoutSeconds": 120,
@@ -123,7 +123,7 @@
123
123
  "title": "one surface drops the optional cwd from its schema, so cross-repo fresh works from pi and silently cannot exist from the MCP bridge",
124
124
  "subject": "mcp/entwurf-bridge/src/index.ts",
125
125
  "find": [
126
- "\t\tcwd: z\n\t\t\t.string()\n\t\t\t.optional()\n\t\t\t.describe(\n\t\t\t\t\"Optional literal ABSOLUTE path of an existing directory to start the sibling in (cross-repo fresh). Omit or pass \\\"\\\" to start in this agent's own cwd. Taken exactly as given — no trim, no realpath, no project-name resolution; '#' is refused (tmux format expansion). The receipt echoes what was REQUESTED, never an observation.\",\n\t\t\t),\n"
126
+ "\t\tcwd: z\n\t\t\t.string()\n\t\t\t.optional()\n\t\t\t.describe(\n\t\t\t\t\"Optional literal ABSOLUTE path of an existing directory to start the sibling in (cross-repo fresh). Taken exactly as given — no trim, no realpath, no project-name resolution; '#' is refused (tmux format expansion). Omit or pass \\\"\\\" to start where the CALLER is: its own directory, or for a Codex caller its own record directory, because this bridge is the app-server's child and that process's directory is not the caller's. The receipt echoes the directory that was REQUESTED or the caller record it came from, never an observation of where the pane landed.\",\n\t\t\t),\n"
127
127
  ],
128
128
  "replace": [""],
129
129
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
@@ -439,17 +439,6 @@
439
439
  "signature": "[QK:FRESHCALL-BACKEND-SET-SURFACE-PARITY-PI]",
440
440
  "signatureSource": "test/fresh-call-surfaces.contract.test.ts"
441
441
  },
442
- {
443
- "claim": "FRESHCALL-CODEX-HOME-DEFAULT",
444
- "title": "an omitted Codex seat falls back to the caller's session instead of the one supported `codex` home topology",
445
- "subject": "pi-extensions/lib/mux-fresh-call.ts",
446
- "find": ["\tif (backend === \"codex\") return { tmuxSession: CODEX_HOME_TMUX_SESSION, source: \"codex-home\" };"],
447
- "replace": ["\tif (backend === \"pi\") return { tmuxSession: CODEX_HOME_TMUX_SESSION, source: \"codex-home\" };"],
448
- "gate": ["bash", "run.sh", "check-mux-fresh-call"],
449
- "timeoutSeconds": 180,
450
- "signature": "[QK:FRESHCALL-CODEX-HOME-DEFAULT]",
451
- "signatureSource": "test/mux-fresh-call.test.ts"
452
- },
453
442
  {
454
443
  "claim": "TMUXSESSION-LOOKUP-ENGINE",
455
444
  "title": "the seat lookup goes back to the `-f` filter engine, which was measured to match EVERY session when the name contains a '}' — a fresh sibling then opens in whichever session tmux listed first",
@@ -523,13 +512,96 @@
523
512
  "title": "one surface drops the optional seat from its schema, so opening a sibling in the operator's project session works from pi and silently cannot exist from the MCP bridge",
524
513
  "subject": "mcp/entwurf-bridge/src/index.ts",
525
514
  "find": [
526
- "\t\tplacement: z\n\t\t\t.object({\n\t\t\t\ttmuxSession: z\n\t\t\t\t\t.string()\n\t\t\t\t\t.describe(\n\t\t\t\t\t\t\"EXACT name of an EXISTING session on this agent's own tmux server. Nothing is created: an absent session is refused as tmux-session-missing, and a name outside [A-Za-z0-9][A-Za-z0-9_-]* as tmux-session-name-invalid.\",\n\t\t\t\t\t),\n\t\t\t})\n\t\t\t.optional()\n\t\t\t.describe(\n\t\t\t\t\"Optional expert seat override: open the sibling in ONE EXISTING tmux session of this agent's own server. When omitted, Codex selects the exact existing `codex` home session; other backends use the caller's session. Nothing is ever created. Independent of cwd; neither is inferred from the other. The receipt reports the selected name, its source, and resolved target session id.\",\n\t\t\t),\n"
515
+ "\t\tplacement: z\n\t\t\t.object({\n\t\t\t\ttmuxSession: z\n\t\t\t\t\t.string()\n\t\t\t\t\t.describe(\n\t\t\t\t\t\t\"EXACT name of an EXISTING session on this agent's own tmux server. Nothing is created: an absent session is refused as tmux-session-missing, and a name outside [A-Za-z0-9][A-Za-z0-9_-]* as tmux-session-name-invalid.\",\n\t\t\t\t\t),\n\t\t\t})\n\t\t\t.optional()\n\t\t\t.describe(\n\t\t\t\t\"Optional expert seat override: open the sibling in ONE EXISTING tmux session of this agent's own server, and it always wins. When omitted the seat follows the CALLER, never the backend being opened: a Codex CALLER opens beside its own TUI pane, matched by thread-id in that pane's terminal title (0 or 2+ matching panes refuse, never fall back); every other caller opens in its own session. A pane title is a placement input only — never an address, liveness or delivery fact. Nothing is ever created. Independent of cwd; neither is inferred from the other. The receipt reports the selected name (absent for the caller-pane rule, which observed a session rather than requesting a name), its source, and resolved target session id.\",\n\t\t\t),\n"
527
516
  ],
528
517
  "replace": [""],
529
518
  "gate": ["bash", "run.sh", "check-mux-fresh-call"],
530
519
  "timeoutSeconds": 180,
531
520
  "signature": "[QK:FRESHCALL-PLACEMENT-SURFACE-PARITY]",
532
521
  "signatureSource": "test/fresh-call-surfaces.contract.test.ts"
522
+ },
523
+ {
524
+ "claim": "FRESHCALL-CODEX-THREAD-CWD",
525
+ "title": "codex loses the `-C <dir>` token, so an attached thread opens in the APP-SERVER's directory while its pane sits somewhere else — the launch, the receipt and the pane all still look right",
526
+ "subject": "pi-extensions/lib/mux-fresh-call.ts",
527
+ "find": ["\t\t\t\t\"-C\",\n\t\t\t\tlaunchCwd,\n"],
528
+ "replace": [""],
529
+ "gate": ["bash", "run.sh", "check-mux-fresh-call"],
530
+ "timeoutSeconds": 120,
531
+ "signature": "[QK:FRESHCALL-CODEX-THREAD-CWD]",
532
+ "signatureSource": "test/mux-fresh-call.test.ts"
533
+ },
534
+ {
535
+ "claim": "FRESHCALL-CWD-CALLER-ONLY",
536
+ "title": "the codex dialect reads THIS process's directory instead of the directory the launch chose, so a cross-repo or caller-record cwd places the pane while the thread opens somewhere else",
537
+ "subject": "pi-extensions/lib/mux-fresh-call.ts",
538
+ "find": ["\t\t\t\tlaunchCwd,\n"],
539
+ "replace": ["\t\t\t\tprocess.cwd(),\n"],
540
+ "gate": ["bash", "run.sh", "check-mux-fresh-call"],
541
+ "timeoutSeconds": 120,
542
+ "signature": "[QK:FRESHCALL-CWD-CALLER-ONLY]",
543
+ "signatureSource": "test/mux-fresh-call.test.ts"
544
+ },
545
+ {
546
+ "claim": "FRESHCALL-CALLER-CWD-CODEX",
547
+ "title": "the composition drops the caller's record directory, so a Codex caller's omitted cwd silently falls back to the app-server's directory again — the #95 lane C defect, restored with the input still on the signature",
548
+ "subject": "pi-extensions/lib/mux-fresh-call.ts",
549
+ "find": [
550
+ "\t\t\t? { value: requestedCwd, source: \"requested\" }\n\t\t\t: callerCwd !== undefined\n\t\t\t\t? { value: callerCwd, source: \"codex-caller-record\" }\n\t\t\t\t: undefined;"
551
+ ],
552
+ "replace": ["\t\t\t? { value: requestedCwd, source: \"requested\" }\n\t\t\t: undefined;"],
553
+ "gate": ["bash", "run.sh", "check-mux-fresh-call"],
554
+ "timeoutSeconds": 120,
555
+ "signature": "[QK:FRESHCALL-CALLER-CWD-CODEX]",
556
+ "signatureSource": "test/mux-fresh-call.test.ts"
557
+ },
558
+ {
559
+ "claim": "FRESHCALL-CALLER-CWD-PI-ARGV-UNCHANGED",
560
+ "title": "an EMPTY caller cwd stops meaning omit, so a surface that supplies \"\" turns into a classified value — a refusal where the pre-#95 argv simply opened",
561
+ "subject": "pi-extensions/lib/mux-fresh-call.ts",
562
+ "find": [
563
+ "\tconst callerCwd = params.callerCwd === undefined || params.callerCwd === \"\" ? undefined : params.callerCwd;"
564
+ ],
565
+ "replace": ["\tconst callerCwd = params.callerCwd;"],
566
+ "gate": ["bash", "run.sh", "check-mux-fresh-call"],
567
+ "timeoutSeconds": 120,
568
+ "signature": "[QK:FRESHCALL-CALLER-CWD-PI-ARGV-UNCHANGED]",
569
+ "signatureSource": "test/mux-fresh-call.test.ts"
570
+ },
571
+ {
572
+ "claim": "FRESHCALL-CWD-TMUX-CARRIER-SOURCE",
573
+ "title": "an absent chosen directory becomes this process's, so every launch carries a `-c` and the argv four backends have had since #73 is no longer the measured one",
574
+ "subject": "pi-extensions/lib/mux-fresh-call.ts",
575
+ "find": ["\tconst cwd = chosenCwd?.value;"],
576
+ "replace": ["\tconst cwd = chosenCwd?.value ?? process.cwd();"],
577
+ "gate": ["bash", "run.sh", "check-mux-fresh-call"],
578
+ "timeoutSeconds": 120,
579
+ "signature": "[QK:FRESHCALL-CWD-TMUX-CARRIER-SOURCE]",
580
+ "signatureSource": "test/mux-fresh-call.test.ts"
581
+ },
582
+ {
583
+ "claim": "FRESHCALL-CWD-RECEIPT-SOURCE",
584
+ "title": "the receipt relabels a caller-record directory as REQUESTED, telling the operator someone asked for a directory nobody named",
585
+ "subject": "pi-extensions/lib/mux-fresh-call.ts",
586
+ "find": [
587
+ "\t\t\t\t: r.cwdSource === \"codex-caller-record\"\n\t\t\t\t\t? ` cwd: ${r.cwd} (the Codex caller's own record directory, used because no cwd was requested — not an observation)\\n`\n\t\t\t\t\t: ` cwd: ${r.cwd} (requested start directory — not an observation)\\n`) +"
588
+ ],
589
+ "replace": ["\t\t\t\t: ` cwd: ${r.cwd} (requested start directory — not an observation)\\n`) +"],
590
+ "gate": ["bash", "run.sh", "check-mux-fresh-call"],
591
+ "timeoutSeconds": 120,
592
+ "signature": "[QK:FRESHCALL-CWD-RECEIPT-SOURCE]",
593
+ "signatureSource": "test/mux-fresh-call.test.ts"
594
+ },
595
+ {
596
+ "claim": "FRESHCALL-CALLER-CWD-SURFACE",
597
+ "title": "the MCP surface stops handing the codex caller's record directory to the composition, so the capability exists in the leaf and is unreachable from the only surface a codex caller can reach it through",
598
+ "subject": "mcp/entwurf-bridge/src/index.ts",
599
+ "find": ["\t\t\tcallerCwd = self.codexThreadId === undefined ? undefined : self.envelope.cwd;\n"],
600
+ "replace": [""],
601
+ "gate": ["bash", "run.sh", "check-mux-fresh-call"],
602
+ "timeoutSeconds": 120,
603
+ "signature": "[QK:FRESHCALL-CALLER-CWD-SURFACE]",
604
+ "signatureSource": "test/fresh-call-surfaces.contract.test.ts"
533
605
  }
534
606
  ]
535
607
  }
@@ -16,6 +16,28 @@
16
16
  "signature": "[QK:V2SURF-MCP-LOCK-DOMAIN]",
17
17
  "signatureSource": "scripts/check-entwurf-v2-surface.ts"
18
18
  },
19
+ {
20
+ "claim": "V2SEND-INBAND-REJECT-REASON",
21
+ "title": "completed control or mailbox refusal drops its receiver-provided error, leaving the sender with bare rejected again",
22
+ "subject": "pi-extensions/lib/entwurf-v2-send.ts",
23
+ "find": ["\treturn { outcome: \"rejected\", rejectReason: result.error === \"\" ? undefined : result.error };"],
24
+ "replace": ["\treturn { outcome: \"rejected\" };"],
25
+ "gate": ["bash", "run.sh", "check-entwurf-v2-send"],
26
+ "timeoutSeconds": 60,
27
+ "signature": "[QK:V2SEND-INBAND-REJECT-REASON]",
28
+ "signatureSource": "scripts/check-entwurf-v2-send.ts"
29
+ },
30
+ {
31
+ "claim": "V2PROD-INBAND-ERROR-WIRED",
32
+ "title": "the production sendOverSocket closure stops copying response.error onto the RpcSendResult, so a receiver's named in-band refusal is stranded one hop before driveSend and the caller sees a reasonless rejected",
33
+ "subject": "pi-extensions/lib/entwurf-v2-production.ts",
34
+ "find": ["\t\t\treturn { success: response.success, error: response.error };"],
35
+ "replace": ["\t\t\treturn { success: response.success };"],
36
+ "gate": ["bash", "run.sh", "check-entwurf-v2-production"],
37
+ "timeoutSeconds": 60,
38
+ "signature": "[QK:V2PROD-INBAND-ERROR-WIRED]",
39
+ "signatureSource": "scripts/check-entwurf-v2-production.ts"
40
+ },
19
41
  {
20
42
  "claim": "V2SURF-PI-MODE-SCOPE",
21
43
  "title": "pi-native mode param reverts to \"Delivery mode for a live send\" (false for native-push)",
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env bash
2
2
  # smoke-codex-config-state — hermetic install/doctor/inverse contract for the
3
- # two Codex config atoms (codex-mcp-config.py + codex-statusline-config.py).
3
+ # three Codex config atoms (codex-mcp-config.py + codex-statusline-config.py +
4
+ # codex-terminal-title-config.py).
4
5
  # No Codex process, no model turn: the vendor's config.toml surface only.
5
6
  set -euo pipefail
6
7
 
7
8
  REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
8
9
  MCP="$REPO_DIR/scripts/codex-mcp-config.py"
9
10
  SL="$REPO_DIR/scripts/codex-statusline-config.py"
11
+ TT="$REPO_DIR/scripts/codex-terminal-title-config.py"
10
12
  pass=0
11
13
  ok() { printf ' ok %s\n' "$1"; pass=$((pass + 1)); }
12
14
  die() { printf 'FAIL: %s\n' "$1" >&2; exit 1; }
@@ -39,6 +41,7 @@ mkdir -p "$CODEX_HOME" "$(dirname "$XDG_DATA_HOME")"
39
41
  CFG="$CODEX_HOME/config.toml"
40
42
  MCP_STATE="$XDG_DATA_HOME/entwurf/codex-mcp/install-state.json"
41
43
  SL_STATE="$XDG_DATA_HOME/entwurf/codex-statusline/install-state.json"
44
+ TT_STATE="$XDG_DATA_HOME/entwurf/codex-terminal-title/install-state.json"
42
45
 
43
46
  seed_config() { # the operator's hand-edited file: quoted header keys, nested
44
47
  # tables, an unrelated MCP server, and NO entwurf atom content.
@@ -464,6 +467,189 @@ sed -i 's/^status_line = .*/status_line = [\n "weather",\n "thread-title",\n]/
464
467
  refuses "restructured list refuses blind take-back" "changed since install" python3 "$SL" uninstall "$SL_STATE"
465
468
  rm -f "$SL_STATE"
466
469
 
470
+
471
+ ###############################################################################
472
+ # Atom 3 — [tui].terminal_title includes thread-id (#95 lane B caller seat)
473
+ #
474
+ # Same writer shape as atom 2 and a DIFFERENT contract in two places, both of
475
+ # which have a wrong answer that still parses: the seeded list must LEAD with
476
+ # `activity` (the herdr Codex detector keys on the spinner/action-required
477
+ # prefix), and an existing operator list is APPENDED to, never prepended — a
478
+ # prepend would put thread-id ahead of an activity the operator already
479
+ # configured. The append position is the third: a trailing comma, a trailing
480
+ # comment or a `]` inside a string item each send a naive `rfind("]")` at the
481
+ # wrong bytes, and one of those emits `,,`.
482
+ ###############################################################################
483
+
484
+ # seeded from nothing: the herdr-safe order, verbatim
485
+ rm -f "$CFG" "$TT_STATE"
486
+ python3 "$TT" install "$CFG" "$TT_STATE" >"$SB/out"
487
+ want "[QK:CODEX-TT-SEED-ACTIVITY-FIRST] seeded terminal_title leads with activity, ends with thread-id" \
488
+ "grep -q '^created-new ' '$SB/out' && grep -q '^terminal_title = \[\"activity\", \"project-name\", \"thread-id\"\]$' '$CFG'"
489
+ python3 "$TT" doctor-static "$CFG" "$TT_STATE" >"$SB/out"
490
+ want "seeded terminal-title doctor is green and owned" "grep -q '^thread-id-present owned$' '$SB/out'"
491
+ python3 "$TT" uninstall "$TT_STATE" >"$SB/out"
492
+ want "seeded file is removed whole by the inverse" "[ ! -e '$CFG' ] && [ ! -e '$TT_STATE' ]"
493
+
494
+ # an operator list is APPENDED to — the direction that is opposite to atom 2
495
+ seed_config
496
+ sed -i '/^theme = /i terminal_title = ["activity", "project-name"]' "$CFG"
497
+ cp "$CFG" "$SB/tt-seed.toml"
498
+ python3 "$TT" install "$CFG" "$TT_STATE" >"$SB/out"
499
+ want "[QK:CODEX-TT-APPENDS-AT-END] thread-id is appended LAST, operator order untouched" \
500
+ "grep -q '^merged-item ' '$SB/out' && grep -q '^terminal_title = \[\"activity\", \"project-name\", \"thread-id\"\]$' '$CFG'"
501
+ toml_ok "$CFG"; ok "merged terminal_title still parses"
502
+ MT1="$(stat -c %Y "$CFG")"; sleep 1.1
503
+ python3 "$TT" install "$CFG" "$TT_STATE" >/dev/null
504
+ want "terminal-title reinstall writes nothing" "[ '$MT1' = '$(stat -c %Y "$CFG")' ]"
505
+ python3 "$TT" uninstall "$TT_STATE" >/dev/null
506
+ cmp -s "$SB/tt-seed.toml" "$CFG" && ok "appended item restored byte-exact" || die "terminal-title inverse mismatch"
507
+
508
+ # an activity-TERMINATED operator list: the item still lands after it
509
+ seed_config
510
+ sed -i '/^theme = /i terminal_title = ["project-name", "activity"]' "$CFG"
511
+ python3 "$TT" install "$CFG" "$TT_STATE" >/dev/null
512
+ want "an activity-terminated list keeps activity in place and appends after it" \
513
+ "grep -q '^terminal_title = \[\"project-name\", \"activity\", \"thread-id\"\]$' '$CFG'"
514
+ python3 "$TT" uninstall "$TT_STATE" >/dev/null
515
+
516
+ # multi-line array with a TRAILING COMMA — the shape a naive append breaks
517
+ seed_config
518
+ python3 - "$CFG" <<'PY'
519
+ import sys
520
+ p = sys.argv[1]
521
+ t = open(p).read()
522
+ t = t.replace('theme = "zenburn"',
523
+ 'terminal_title = [\n "activity", # operator note\n "project-name",\n]\ntheme = "zenburn"')
524
+ open(p, 'w').write(t)
525
+ PY
526
+ cp "$CFG" "$SB/tt-multiline.toml"
527
+ python3 "$TT" install "$CFG" "$TT_STATE" >/dev/null
528
+ toml_ok "$CFG"
529
+ want "[QK:CODEX-TT-TRAILING-COMMA] a trailing-comma array is appended to without emitting a double comma" \
530
+ "! grep -q ',[[:space:]]*,' '$CFG' && python3 -c \"import tomllib,sys; d=tomllib.load(open(sys.argv[1],'rb')); assert d['tui']['terminal_title']==['activity','project-name','thread-id'], d\" '$CFG'"
531
+ python3 "$TT" uninstall "$TT_STATE" >/dev/null
532
+ cmp -s "$SB/tt-multiline.toml" "$CFG" && ok "trailing-comma array restored byte-exact" || die "trailing-comma inverse mismatch"
533
+
534
+ # a trailing COMMENT that itself contains `]` — rfind("]") would splice into it
535
+ seed_config
536
+ sed -i '/^theme = /i terminal_title = ["activity"] # keep ] this' "$CFG"
537
+ cp "$CFG" "$SB/tt-comment.toml"
538
+ python3 "$TT" install "$CFG" "$TT_STATE" >/dev/null
539
+ want "[QK:CODEX-TT-COMMENT-NOT-SPLICED] the item lands inside the array and the operator's comment survives" \
540
+ "grep -q '^terminal_title = \[\"activity\", \"thread-id\"\] # keep \] this$' '$CFG'"
541
+ python3 "$TT" uninstall "$TT_STATE" >/dev/null
542
+ cmp -s "$SB/tt-comment.toml" "$CFG" && ok "commented line restored byte-exact" || die "comment inverse mismatch"
543
+
544
+ # a `]` inside a string item, and the empty list
545
+ seed_config
546
+ sed -i '/^theme = /i terminal_title = ["a]b", "activity"]' "$CFG"
547
+ python3 "$TT" install "$CFG" "$TT_STATE" >/dev/null
548
+ want "a ] inside a string item does not become the append point" \
549
+ "grep -q '^terminal_title = \[\"a\]b\", \"activity\", \"thread-id\"\]$' '$CFG'"
550
+ python3 "$TT" uninstall "$TT_STATE" >/dev/null
551
+ seed_config
552
+ sed -i '/^theme = /i terminal_title = []' "$CFG"
553
+ cp "$CFG" "$SB/tt-empty.toml"
554
+ python3 "$TT" install "$CFG" "$TT_STATE" >/dev/null
555
+ want "empty terminal_title becomes exactly [\"thread-id\"]" "grep -q '^terminal_title = \[\"thread-id\"\]$' '$CFG'"
556
+ python3 "$TT" uninstall "$TT_STATE" >/dev/null
557
+ cmp -s "$SB/tt-empty.toml" "$CFG" && ok "empty terminal_title restored byte-exact" || die "empty terminal_title inverse mismatch"
558
+
559
+ # an operator-authored item is UNOWNED, and nothing is written over it
560
+ seed_config
561
+ sed -i '/^theme = /i terminal_title = ["activity", "thread-id"]' "$CFG"
562
+ cp "$CFG" "$SB/tt-operator.toml"
563
+ rm -f "$TT_STATE"
564
+ MT1="$(stat -c %Y "$CFG")"; sleep 1.1
565
+ python3 "$TT" install "$CFG" "$TT_STATE" >"$SB/out"
566
+ want "[QK:CODEX-TT-OPERATOR-ITEM-UNOWNED] an operator-authored thread-id writes NO receipt and no config byte" \
567
+ "grep -q '^already-present ' '$SB/out' && [ ! -e '$TT_STATE' ] && [ '$MT1' = '$(stat -c %Y "$CFG")' ]"
568
+ python3 "$TT" doctor-static "$CFG" "$TT_STATE" >"$SB/out"
569
+ want "the doctor reports it UNOWNED — we claim no edit we did not make" \
570
+ "grep -q '^thread-id-present unowned$' '$SB/out'"
571
+ python3 "$TT" uninstall "$TT_STATE" >"$SB/out" 2>&1 \
572
+ && die "uninstall claimed to take back an operator's own item"
573
+ grep -q 'nothing to undo' "$SB/out" || die "unowned uninstall refusal is unnamed: $(cat "$SB/out")"
574
+ cmp -s "$SB/tt-operator.toml" "$CFG" && ok "the refused inverse left the operator's config byte-identical" \
575
+ || die "unowned uninstall mutated the operator's config"
576
+
577
+ # ...and that is what keeps the INVERSE honest across a hand edit. With a receipt parked over an
578
+ # operator's item, a reinstall after they removed it splices ours in while the stale receipt still
579
+ # says it was already there — and the inverse then reports "nothing to take back" and leaves our
580
+ # bytes behind for good. Writing nothing above makes that second install an ordinary merged-item.
581
+ seed_config
582
+ sed -i '/^theme = /i terminal_title = ["activity"]' "$CFG"
583
+ cp "$CFG" "$SB/tt-inverse.toml"
584
+ rm -f "$TT_STATE"
585
+ python3 "$TT" install "$CFG" "$TT_STATE" >/dev/null
586
+ sed -i 's/, "thread-id"\]/]/' "$CFG" # the operator takes our item back by hand
587
+ python3 "$TT" install "$CFG" "$TT_STATE" >"$SB/out"
588
+ want "a reinstall after a hand removal records what it ACTUALLY did" \
589
+ "grep -q '^merged-item ' '$SB/out' && grep -q '\"detectMode\": \"merged-item\"' '$TT_STATE'"
590
+ python3 "$TT" uninstall "$TT_STATE" >/dev/null
591
+ cmp -s "$SB/tt-inverse.toml" "$CFG" && ok "the inverse takes our re-added item back, leaving nothing behind" \
592
+ || { printf 'left behind:\n%s\n' "$(cat "$CFG")"; die "reinstall inverse left our item in the operator's config"; }
593
+ rm -f "$TT_STATE"
594
+
595
+ # the doctor's subject is the REQUIRED caller-seat axis, receipt or no receipt
596
+ seed_config
597
+ sed -i '/^theme = /i terminal_title = ["activity", "thread-id"]' "$CFG"
598
+ rm -f "$TT_STATE"
599
+ python3 "$TT" doctor-static "$CFG" "$TT_STATE" >"$SB/out"
600
+ want "unowned thread-id is green (state-independent)" "grep -q '^thread-id-present unowned$' '$SB/out'"
601
+ sed -i 's/, "thread-id"\]/]/' "$CFG"
602
+ python3 "$TT" doctor-static "$CFG" "$TT_STATE" >"$SB/out" 2>&1 \
603
+ && die "[QK:CODEX-TT-DOCTOR-REQUIRES-SEAT-AXIS] unowned config WITHOUT thread-id reported green: $(cat "$SB/out")"
604
+ want "[QK:CODEX-TT-DOCTOR-REQUIRES-SEAT-AXIS] a missing caller-seat axis is RED even unowned" \
605
+ "grep -q '^thread-id-absent unowned$' '$SB/out'"
606
+ python3 "$TT" install "$CFG" "$TT_STATE" >/dev/null
607
+ sed -i 's/, "thread-id"\]/]/' "$CFG"
608
+ python3 "$TT" doctor-static "$CFG" "$TT_STATE" >"$SB/out" 2>&1 || true
609
+ want "our receipt over a vanished item reports drift, not green" "grep -q '^thread-id-absent owned drift$' '$SB/out'"
610
+ rm -f "$TT_STATE"
611
+
612
+ # the two [tui] atoms are independent: neither sees the other's key
613
+ seed_config
614
+ python3 "$SL" install "$CFG" "$SL_STATE" >/dev/null
615
+ python3 "$TT" install "$CFG" "$TT_STATE" >/dev/null
616
+ want "[QK:CODEX-TT-ATOM-INDEPENDENCE] both [tui] atoms live side by side" \
617
+ "python3 -c \"import tomllib,sys; d=tomllib.load(open(sys.argv[1],'rb'))['tui']; assert d['status_line']==['thread-title','model-with-reasoning','current-dir'], d; assert d['terminal_title']==['activity','project-name','thread-id'], d\" '$CFG'"
618
+ python3 "$TT" uninstall "$TT_STATE" >/dev/null
619
+ want "[QK:CODEX-TT-ATOM-INDEPENDENCE] taking back terminal_title leaves status_line exactly as it was" \
620
+ "grep -q '^status_line = \[\"thread-title\", \"model-with-reasoning\", \"current-dir\"\]$' '$CFG' && ! grep -q 'terminal_title' '$CFG'"
621
+ python3 "$SL" uninstall "$SL_STATE" >/dev/null
622
+
623
+ # refusals — symlink, malformed, contrary value by name, foreign/retargeted state
624
+ seed_config
625
+ ln -sf "$SB/foreign.toml" "$CFG"
626
+ refuses "symlink config refused (terminal-title)" "symlink" python3 "$TT" install "$CFG" "$TT_STATE"
627
+ rm -f "$CFG"
628
+ printf 'garbage [[[\n' > "$CFG"
629
+ refuses "malformed TOML refused (terminal-title)" "not valid TOML" python3 "$TT" install "$CFG" "$TT_STATE"
630
+ seed_config
631
+ sed -i '/^theme = /i terminal_title = "thread-id"' "$CFG"
632
+ refuses "non-array terminal_title refused BY NAME" "terminal_title.*explicitly" python3 "$TT" install "$CFG" "$TT_STATE"
633
+ want "terminal-title refusals wrote no state" "[ ! -e '$TT_STATE' ]"
634
+ seed_config
635
+ python3 "$SL" install "$CFG" "$SL_STATE" >/dev/null
636
+ cp "$SL_STATE" "$TT_STATE" # atom 2's receipt, on our path
637
+ refuses "foreign-atom state refused (terminal-title)" "belongs to atom" python3 "$TT" install "$CFG" "$TT_STATE"
638
+ python3 "$SL" uninstall "$SL_STATE" >/dev/null
639
+ rm -f "$TT_STATE"
640
+ seed_config
641
+ python3 "$TT" install "$CFG" "$TT_STATE" >/dev/null
642
+ sed -i "s|$(json_field "$TT_STATE" managedConfigPath)|$SB/elsewhere.toml|" "$TT_STATE"
643
+ refuses "retargeted state refused (terminal-title)" "refusing to retarget" python3 "$TT" install "$CFG" "$TT_STATE"
644
+ rm -f "$TT_STATE"
645
+
646
+ # operator restructures the list after install: the take-back refuses, by name
647
+ seed_config
648
+ python3 "$TT" install "$CFG" "$TT_STATE" >/dev/null
649
+ sed -i 's/^terminal_title = .*/terminal_title = [\n "weather",\n "thread-id",\n]/' "$CFG"
650
+ refuses "restructured terminal_title refuses blind take-back" "changed since install" python3 "$TT" uninstall "$TT_STATE"
651
+ rm -f "$TT_STATE"
652
+
467
653
  ###############################################################################
468
654
  # Unrelated operator config survives the whole cycle, byte for byte
469
655
  ###############################################################################
@@ -471,6 +657,7 @@ seed_config
471
657
  cp "$CFG" "$SB/final-seed.toml"
472
658
  python3 "$MCP" install "$CFG" entwurf-bridge "$MCP_STATE" >/dev/null
473
659
  python3 "$SL" install "$CFG" "$SL_STATE" >/dev/null
660
+ python3 "$TT" install "$CFG" "$TT_STATE" >/dev/null
474
661
  toml_ok "$CFG"; ok "fully installed config parses as TOML"
475
662
  python3 - "$CFG" <<'PY'
476
663
  import tomllib, sys
@@ -494,17 +681,19 @@ assert d["mcp_servers"]["entwurf-bridge"] == {
494
681
  "env": {"ENTWURF_BRIDGE_NATIVE_HOST": "codex"},
495
682
  }, d
496
683
  assert d["tui"]["status_line"] == ["thread-title", "model-with-reasoning", "current-dir"], d
684
+ assert d["tui"]["terminal_title"] == ["activity", "project-name", "thread-id"], d
497
685
  assert d["tui"]["theme"] == "zenburn" and d["tui"]["model_availability_nux"] == {"gpt-5.5": 4}, d
498
686
  assert d["plugins"]["github@openai-curated"] == {"enabled": False}, d
499
687
  PY
500
- ok "unrelated tables intact and both atoms semantically live"
688
+ ok "unrelated tables intact and all three atoms semantically live"
501
689
  # the vendor writes trust state back between install and uninstall (M1)
502
690
  printf '[hooks.state."~/c.toml:stop:0:0"]\ntrusted_hash = "sha256:def"\n' >> "$CFG"
503
691
  python3 "$MCP" uninstall "$MCP_STATE" >/dev/null
504
692
  python3 "$SL" uninstall "$SL_STATE" >/dev/null
693
+ python3 "$TT" uninstall "$TT_STATE" >/dev/null
505
694
  printf '[hooks.state."~/c.toml:stop:0:0"]\ntrusted_hash = "sha256:def"\n' >> "$SB/final-seed.toml"
506
695
  cmp -s "$SB/final-seed.toml" "$CFG" && ok "full cycle leaves only vendor-added bytes (unrelated config survives)" || die "final byte mismatch"
507
- want "both states cleared" "[ ! -e '$MCP_STATE' ] && [ ! -e '$SL_STATE' ]"
696
+ want "all three states cleared" "[ ! -e '$MCP_STATE' ] && [ ! -e '$SL_STATE' ] && [ ! -e '$TT_STATE' ]"
508
697
 
509
698
  REPO_AFTER="$(cd "$REPO_DIR" && git status --porcelain)"
510
699
  [ "$REPO_BEFORE" = "$REPO_AFTER" ] || die "smoke changed the checkout"