@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.
- package/AGENTS.md +2 -2
- package/BASELINE.md +3 -1
- package/CHANGELOG.md +278 -0
- package/DELIVERY.md +156 -26
- package/README.md +64 -13
- package/VERIFY.md +67 -11
- package/docs/external-mcp-host.md +16 -6
- package/docs/setup-clean-host.md +63 -22
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +40 -16
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +194 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +160 -25
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
- package/mcp/entwurf-bridge/src/index.ts +50 -16
- package/mcp/entwurf-bridge/tsconfig.build.json +9 -0
- package/package.json +2 -2
- package/pi-extensions/entwurf-control.ts +4 -4
- package/pi-extensions/lib/codex-caller-seat.ts +204 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +218 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
- package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
- package/pi-extensions/lib/mux-fresh-call.ts +196 -37
- package/run.sh +127 -3
- package/scripts/check-codex-app-server-launch.ts +445 -0
- package/scripts/check-entwurf-v2-production.ts +42 -1
- package/scripts/check-entwurf-v2-send.ts +26 -7
- package/scripts/check-gate-qualification.ts +4 -2
- package/scripts/check-mux-launch-tmux.ts +331 -35
- package/scripts/codex-app-server-launch.sh +275 -0
- package/scripts/codex-socket-path.ts +33 -0
- package/scripts/codex-terminal-title-config.py +500 -0
- package/scripts/codex_toml_io.py +121 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +11 -3
- package/scripts/lib/codex-fresh-source-receipts.ts +29 -2
- package/scripts/mutants/codex-app-server-launch.json +157 -0
- package/scripts/mutants/codex-caller-seat.json +336 -0
- package/scripts/mutants/codex-native.json +3 -3
- package/scripts/mutants/mux-fresh-call.json +86 -14
- package/scripts/mutants/v2-surface.json +22 -0
- package/scripts/smoke-codex-config-state.sh +192 -3
- package/scripts/smoke-codex-fresh-live.ts +277 -37
- package/scripts/smoke-entwurf-chain-live.ts +50 -0
- 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...(
|
|
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).
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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 "
|
|
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"
|