@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
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""codex-terminal-title-config — stateful `[tui].terminal_title` adapter; stdlib only.
|
|
3
|
+
|
|
4
|
+
One reversible atom in `$CODEX_HOME/config.toml`: ensure the TUI's closed-enum
|
|
5
|
+
TERMINAL TITLE list INCLUDES `thread-id`. That item renders the raw thread UUID
|
|
6
|
+
(`tui/src/bottom_pane/title_setup.rs:79-80` at rust-v0.153.4) into the OSC 0/2
|
|
7
|
+
title, which the multiplexer gives back as `#{pane_title}` — the ONE value a
|
|
8
|
+
`_meta.threadId` can be matched against to find the pane a Codex caller is
|
|
9
|
+
sitting in (#95 lane B). This is a PLACEMENT input only: the title is
|
|
10
|
+
operator-writable and forgeable, so identity and delivery keep the record +
|
|
11
|
+
`_meta` join (AGENTS.md Hard Rule 16).
|
|
12
|
+
|
|
13
|
+
Sibling of `codex-statusline-config.py` and deliberately its twin in shape: the
|
|
14
|
+
atom owns exactly the `thread-id` MEMBERSHIP, never the list. Operator items,
|
|
15
|
+
their order and their bytes survive untouched (a text insert right before the
|
|
16
|
+
closing bracket, not a re-serialization), and the inverse removes exactly the
|
|
17
|
+
snippet that was added.
|
|
18
|
+
|
|
19
|
+
TWO DIFFERENCES FROM THE STATUS-LINE ATOM, both measured, neither cosmetic:
|
|
20
|
+
|
|
21
|
+
1. The seed list is `["activity", "project-name", "thread-id"]`, not the bare
|
|
22
|
+
item. `activity` must lead because the herdr Codex detector keys on the
|
|
23
|
+
spinner tokens and the `[ ! ] Action Required` prefix
|
|
24
|
+
(herdr `src/detect/manifests/codex.toml`); seeding a title without it
|
|
25
|
+
would silently retire that detection on this host.
|
|
26
|
+
2. The item is APPENDED at the END of an existing operator list, where the
|
|
27
|
+
status-line atom prepends. Same reason: a prepend would put `thread-id`
|
|
28
|
+
ahead of `activity` on a host that already configured one.
|
|
29
|
+
|
|
30
|
+
Refuses, with the offending name in the message: a symlinked config (someone
|
|
31
|
+
else's SSOT), malformed TOML, a foreign install-state, a state retargeted at
|
|
32
|
+
another config path, and an explicit contrary operator value by name — a
|
|
33
|
+
`terminal_title` that is not an array of items is the operator's decision, not
|
|
34
|
+
drift to overwrite.
|
|
35
|
+
|
|
36
|
+
The doctor judges EFFECTIVE thread-id from the config alone, independent of the
|
|
37
|
+
state receipt (the operator may set it themselves — that is green, not drift).
|
|
38
|
+
Its subject is the REQUIRED caller-seat axis, so a present item is the only
|
|
39
|
+
green and every other effective token is red; the state never rescues one, it
|
|
40
|
+
only annotates ownership and adds `drift` when our receipt binds the file whose
|
|
41
|
+
item is gone.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import json
|
|
47
|
+
import os
|
|
48
|
+
import sys
|
|
49
|
+
sys.dont_write_bytecode = True
|
|
50
|
+
|
|
51
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
52
|
+
from codex_toml_io import ( # noqa: E402
|
|
53
|
+
append_block,
|
|
54
|
+
array_append_point,
|
|
55
|
+
assert_blast_radius,
|
|
56
|
+
atomic_write,
|
|
57
|
+
atomic_write_state,
|
|
58
|
+
deep_get,
|
|
59
|
+
die,
|
|
60
|
+
find_direct_key,
|
|
61
|
+
find_table_family,
|
|
62
|
+
now,
|
|
63
|
+
parse_toml,
|
|
64
|
+
read_text,
|
|
65
|
+
render_file,
|
|
66
|
+
render_string_array,
|
|
67
|
+
scan_lines,
|
|
68
|
+
span_text,
|
|
69
|
+
span_value,
|
|
70
|
+
splice,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
STATE_SCHEMA_VERSION = 1
|
|
74
|
+
ATOM = "codex-terminal-title"
|
|
75
|
+
TUI_PATH = ("tui",)
|
|
76
|
+
TITLE_KEY = "terminal_title"
|
|
77
|
+
REQUIRED_ITEM = "thread-id"
|
|
78
|
+
# The seeded list when the operator has no `terminal_title` at all. `activity`
|
|
79
|
+
# leads for the herdr detector (see the module docstring); `project-name` is
|
|
80
|
+
# what makes a seated title readable to a human at a glance.
|
|
81
|
+
SEED_ITEMS = ("activity", "project-name", REQUIRED_ITEM)
|
|
82
|
+
STATE_REQUIRED = (
|
|
83
|
+
"managedConfigPath",
|
|
84
|
+
"atom",
|
|
85
|
+
"detectMode",
|
|
86
|
+
"configExistedBefore",
|
|
87
|
+
"tuiTableExisted",
|
|
88
|
+
"terminalTitleExisted",
|
|
89
|
+
"terminalTitlePreimage",
|
|
90
|
+
"postimageSpan",
|
|
91
|
+
"snippet",
|
|
92
|
+
"installedAt",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# detectMode → what the inverse takes back:
|
|
96
|
+
# created-new the whole file (we made it; only if nothing else remains)
|
|
97
|
+
# appended-table the [tui] block we appended (only if it is still only ours)
|
|
98
|
+
# inserted-key the `terminal_title = [...]` key lines we inserted
|
|
99
|
+
# merged-item the exact `, "thread-id"` snippet appended to the list
|
|
100
|
+
#
|
|
101
|
+
# There is deliberately NO `already-present` mode on disk. An operator who wrote
|
|
102
|
+
# `thread-id` themselves is UNOWNED, and recording a receipt over their bytes
|
|
103
|
+
# would claim an edit we never made — the doctor would then answer `owned`, and
|
|
104
|
+
# `drift` (its "repair in place" red) would point at an item that was never
|
|
105
|
+
# ours. It also carried a real inverse bug: with such a receipt parked, a later
|
|
106
|
+
# reinstall after the operator removed the item DOES splice ours in while the
|
|
107
|
+
# stale receipt still says `already-present`, so the inverse reports "nothing to
|
|
108
|
+
# take back" and leaves our bytes in their file for good. Writing nothing at all
|
|
109
|
+
# is what makes that second install a normal `merged-item` with an honest
|
|
110
|
+
# inverse. `[측정 2026-09-16]` the same shape is still live in the
|
|
111
|
+
# `codex-statusline` sibling; widening this repair to it is a separate decision.
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def load_state(state_path: str) -> dict | None:
|
|
115
|
+
raw = read_text(state_path, "install-state")
|
|
116
|
+
if raw is None:
|
|
117
|
+
return None
|
|
118
|
+
if not raw.strip():
|
|
119
|
+
die(4, f"codex-terminal-title: install-state {state_path} is empty — refusing to guess")
|
|
120
|
+
try:
|
|
121
|
+
state = json.loads(raw)
|
|
122
|
+
except ValueError as error:
|
|
123
|
+
die(4, f"codex-terminal-title: install-state {state_path} is not valid JSON: {error}")
|
|
124
|
+
if not isinstance(state, dict):
|
|
125
|
+
die(4, f"codex-terminal-title: install-state {state_path} top-level must be a JSON object")
|
|
126
|
+
return state
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def checked_state(state_path: str) -> dict:
|
|
130
|
+
state = load_state(state_path)
|
|
131
|
+
if state is None:
|
|
132
|
+
die(2, f"codex-terminal-title: no install-state at {state_path} — nothing to undo")
|
|
133
|
+
if state.get("schemaVersion") != STATE_SCHEMA_VERSION:
|
|
134
|
+
die(4, f"codex-terminal-title: install-state {state_path} is not schemaVersion {STATE_SCHEMA_VERSION} — refusing")
|
|
135
|
+
atom = state.get("atom")
|
|
136
|
+
if atom != ATOM:
|
|
137
|
+
die(4, f"codex-terminal-title: install-state {state_path} belongs to atom {atom!r}, not {ATOM!r} — refusing to touch it")
|
|
138
|
+
if any(key not in state for key in STATE_REQUIRED):
|
|
139
|
+
die(4, f"codex-terminal-title: install-state {state_path} has an unsupported shape — refusing")
|
|
140
|
+
path = state.get("managedConfigPath")
|
|
141
|
+
if not isinstance(path, str) or not os.path.isabs(path):
|
|
142
|
+
die(4, f"codex-terminal-title: install-state {state_path} has no absolute managedConfigPath")
|
|
143
|
+
return state
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def prior_state(state_path: str, config_path: str) -> dict | None:
|
|
147
|
+
state = load_state(state_path)
|
|
148
|
+
if state is None:
|
|
149
|
+
return None
|
|
150
|
+
if state.get("schemaVersion") != STATE_SCHEMA_VERSION or state.get("atom") != ATOM:
|
|
151
|
+
checked_state(state_path) # the honest, named refusal
|
|
152
|
+
if state.get("managedConfigPath") != os.path.abspath(config_path):
|
|
153
|
+
die(
|
|
154
|
+
3,
|
|
155
|
+
f"codex-terminal-title: install-state {state_path} is bound to {state.get('managedConfigPath')}, "
|
|
156
|
+
f"not {os.path.abspath(config_path)} — refusing to retarget an install onto another config",
|
|
157
|
+
)
|
|
158
|
+
return checked_state(state_path)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def classify(parsed: dict) -> tuple[str, object]:
|
|
162
|
+
"""`(tui-shape, terminal_title-value)`; tui-shape is `table`, `refuse:<name>`,
|
|
163
|
+
or `not-line-shaped` only in combination with the span search."""
|
|
164
|
+
tui = deep_get(parsed, TUI_PATH)
|
|
165
|
+
if tui is None:
|
|
166
|
+
return "absent", None
|
|
167
|
+
if not isinstance(tui, dict):
|
|
168
|
+
return "refuse:tui", tui
|
|
169
|
+
value = tui.get(TITLE_KEY)
|
|
170
|
+
if value is None:
|
|
171
|
+
return "no-key", None
|
|
172
|
+
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
|
173
|
+
return "refuse:terminal_title", value
|
|
174
|
+
return "has-key", value
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _append_item(span: str, config_path: str) -> tuple[str, str]:
|
|
178
|
+
"""`(merged_span, snippet)` — the span with our item appended at the END of
|
|
179
|
+
the operator's list, and the exact bytes that were added.
|
|
180
|
+
|
|
181
|
+
The position and the separator both come from `array_append_point`, not
|
|
182
|
+
from `rfind("]")`: a trailing comment, a `]` inside a string item, and the
|
|
183
|
+
multi-line style whose last item already carries a comma each send a naive
|
|
184
|
+
append to the wrong bytes (the last one emits `,,`). A span whose array
|
|
185
|
+
that helper cannot locate is a SHAPE this writer does not own — a named
|
|
186
|
+
refusal, not an internal error. The caller still re-parses the result and
|
|
187
|
+
compares it to the operator's items plus ours, so the position has an
|
|
188
|
+
independent oracle either way.
|
|
189
|
+
"""
|
|
190
|
+
point = array_append_point(span, span.index("=") + 1)
|
|
191
|
+
if point is None:
|
|
192
|
+
die(
|
|
193
|
+
4,
|
|
194
|
+
f"codex-terminal-title: `tui.{TITLE_KEY}` in {config_path} is a `key = value` line whose array this "
|
|
195
|
+
"writer cannot locate (unterminated, or closed past a comment) — it owns only line-shaped array "
|
|
196
|
+
"values and refuses to guess where the list ends",
|
|
197
|
+
)
|
|
198
|
+
at, tail = point
|
|
199
|
+
# `comma` keeps the operator's trailing-comma style by writing one of our
|
|
200
|
+
# own; the inverse removes this exact string, so the two stay symmetric.
|
|
201
|
+
snippet = {
|
|
202
|
+
"empty": f'"{REQUIRED_ITEM}"',
|
|
203
|
+
"item": f', "{REQUIRED_ITEM}"',
|
|
204
|
+
"comma": f' "{REQUIRED_ITEM}",',
|
|
205
|
+
}[tail]
|
|
206
|
+
return span[:at] + snippet + span[at:], snippet
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def install(config_path: str, state_path: str) -> None:
|
|
210
|
+
if os.path.islink(config_path):
|
|
211
|
+
die(3, f"codex-terminal-title: refusing to adopt {config_path} — symlink to {os.readlink(config_path)} (someone else's SSOT)")
|
|
212
|
+
if os.path.exists(config_path) and not os.path.isfile(config_path):
|
|
213
|
+
die(4, f"codex-terminal-title: refusing: {config_path} exists and is not a regular file")
|
|
214
|
+
|
|
215
|
+
text = read_text(config_path, "config")
|
|
216
|
+
parsed = parse_toml(text if text is not None else "", f"config {config_path}") if text is not None else {}
|
|
217
|
+
shape, value = classify(parsed)
|
|
218
|
+
if shape.startswith("refuse:"):
|
|
219
|
+
name = shape.split(":", 1)[1]
|
|
220
|
+
die(
|
|
221
|
+
3,
|
|
222
|
+
f"codex-terminal-title: refusing: {'.'.join(TUI_PATH)}{'.' + TITLE_KEY if name == TITLE_KEY else ''} in {config_path} "
|
|
223
|
+
f"is explicitly a {type(value).__name__}, not {'an array of terminal-title items' if name == TITLE_KEY else 'a table'}. "
|
|
224
|
+
"That is the operator's decision, not drift — entwurf will not overwrite it by name.",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
lines = (text if text is not None else "").split("\n")
|
|
228
|
+
infos = scan_lines(lines)
|
|
229
|
+
tui_family = find_table_family(lines, infos, TUI_PATH)
|
|
230
|
+
key_span = find_direct_key(lines, infos, tui_family[0], tui_family[1], TITLE_KEY) if tui_family is not None else None
|
|
231
|
+
if shape in ("has-key", "no-key") and tui_family is None:
|
|
232
|
+
die(
|
|
233
|
+
4,
|
|
234
|
+
f"codex-terminal-title: [tui] in {config_path} exists but not as a literal table block "
|
|
235
|
+
"(inline/dotted form) — this writer owns only line-shaped tables and refuses to touch it",
|
|
236
|
+
)
|
|
237
|
+
if shape == "has-key" and key_span is None:
|
|
238
|
+
die(
|
|
239
|
+
4,
|
|
240
|
+
f"codex-terminal-title: `tui.{TITLE_KEY}` in {config_path} exists but not as a literal `key = value` line "
|
|
241
|
+
"(dotted/inline form) — this writer owns only line-shaped keys and refuses to touch it",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
canonical_line = f"{TITLE_KEY} = {render_string_array(SEED_ITEMS)}"
|
|
245
|
+
# Read BEFORE the shape branch: whether a receipt already binds this file is what tells an
|
|
246
|
+
# operator-authored item apart from one of ours, and they get different answers below.
|
|
247
|
+
prior = prior_state(state_path, config_path)
|
|
248
|
+
|
|
249
|
+
if shape == "has-key":
|
|
250
|
+
if REQUIRED_ITEM in value:
|
|
251
|
+
if prior is None:
|
|
252
|
+
# The operator wrote it. Nothing is written ANYWHERE — not the config, not a
|
|
253
|
+
# receipt — so the doctor reads this host as `unowned`, which is what it is.
|
|
254
|
+
sys.stdout.write(f"already-present {os.path.abspath(config_path)}\n")
|
|
255
|
+
return
|
|
256
|
+
# Ours, already installed. The strongest idempotence is to leave both files exactly
|
|
257
|
+
# as they are, carrying the ORIGINAL receipt rather than re-deriving one.
|
|
258
|
+
mode = prior["detectMode"]
|
|
259
|
+
new_lines = lines
|
|
260
|
+
preimage = prior["terminalTitlePreimage"]
|
|
261
|
+
postimage_span = prior["postimageSpan"]
|
|
262
|
+
snippet = prior["snippet"]
|
|
263
|
+
else:
|
|
264
|
+
mode = "merged-item"
|
|
265
|
+
preimage = span_text(lines, key_span[0], key_span[1] + 1)
|
|
266
|
+
new_span, snippet = _append_item(preimage, config_path)
|
|
267
|
+
# The scanner's own receipt: the merged span must parse to exactly
|
|
268
|
+
# the old list with our item APPENDED, or we refuse to splice it.
|
|
269
|
+
merged_value = span_value(new_span, "merged terminal_title").get(TITLE_KEY)
|
|
270
|
+
if merged_value != list(value) + [REQUIRED_ITEM]:
|
|
271
|
+
die(7, "codex-terminal-title: internal refusal: merged terminal_title span does not re-parse to old items + thread-id")
|
|
272
|
+
postimage_span = new_span
|
|
273
|
+
new_lines = splice(lines, key_span[0], key_span[1] + 1, new_span.split("\n"))
|
|
274
|
+
elif shape == "no-key":
|
|
275
|
+
mode = "inserted-key"
|
|
276
|
+
preimage = None
|
|
277
|
+
postimage_span = canonical_line
|
|
278
|
+
snippet = None
|
|
279
|
+
new_lines = lines[: tui_family[0] + 1] + [canonical_line] + lines[tui_family[0] + 1:]
|
|
280
|
+
elif shape == "absent":
|
|
281
|
+
block = [f"[{TUI_PATH[0]}]", canonical_line]
|
|
282
|
+
if text is None:
|
|
283
|
+
mode = "created-new"
|
|
284
|
+
new_lines = list(block)
|
|
285
|
+
else:
|
|
286
|
+
mode = "appended-table"
|
|
287
|
+
new_lines = append_block(lines, block)
|
|
288
|
+
preimage = None
|
|
289
|
+
postimage_span = canonical_line
|
|
290
|
+
snippet = None
|
|
291
|
+
else: # pragma: no cover — classify() exhausted above
|
|
292
|
+
die(7, f"codex-terminal-title: internal: unhandled shape {shape!r}")
|
|
293
|
+
|
|
294
|
+
if prior is None:
|
|
295
|
+
state = {
|
|
296
|
+
"schemaVersion": STATE_SCHEMA_VERSION,
|
|
297
|
+
"managedConfigPath": os.path.abspath(config_path),
|
|
298
|
+
"atom": ATOM,
|
|
299
|
+
"detectMode": mode,
|
|
300
|
+
"configExistedBefore": text is not None,
|
|
301
|
+
"tuiTableExisted": shape in ("has-key", "no-key"),
|
|
302
|
+
"terminalTitleExisted": shape == "has-key",
|
|
303
|
+
# The FIRST preimage, captured once; a reinstall never re-captures.
|
|
304
|
+
"terminalTitlePreimage": preimage,
|
|
305
|
+
"postimageSpan": postimage_span,
|
|
306
|
+
"snippet": snippet,
|
|
307
|
+
"installedAt": now(),
|
|
308
|
+
}
|
|
309
|
+
else:
|
|
310
|
+
state = prior
|
|
311
|
+
|
|
312
|
+
candidate = render_file(new_lines)
|
|
313
|
+
_, after = assert_blast_radius(text, candidate, TUI_PATH + (TITLE_KEY,), f"config {config_path}")
|
|
314
|
+
effective = deep_get(after, TUI_PATH + (TITLE_KEY,))
|
|
315
|
+
if shape == "has-key":
|
|
316
|
+
expected = list(value) if REQUIRED_ITEM in value else list(value) + [REQUIRED_ITEM]
|
|
317
|
+
else:
|
|
318
|
+
expected = list(SEED_ITEMS)
|
|
319
|
+
if effective != expected:
|
|
320
|
+
die(7, "codex-terminal-title: internal refusal: candidate terminal_title is not the operator's items plus thread-id")
|
|
321
|
+
|
|
322
|
+
# The receipt is durable BEFORE the operator's config is ever touched: it
|
|
323
|
+
# already carries the exact preimage/snippet of the bytes about to be
|
|
324
|
+
# replaced (or a prior receipt's, on a re-run). A crash or write failure
|
|
325
|
+
# between here and the config write below leaves the operator's config
|
|
326
|
+
# untouched and the receipt correct — a retry resumes from it instead of
|
|
327
|
+
# mistaking our own canonical postimage for a fresh operator preimage.
|
|
328
|
+
atomic_write_state(state_path, state)
|
|
329
|
+
if candidate != (text if text is not None else ""):
|
|
330
|
+
atomic_write(config_path, candidate, prefix=".codex-tt-")
|
|
331
|
+
sys.stdout.write(f"{mode} {os.path.abspath(config_path)}\n")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _current_key_span(config_path: str, text: str):
|
|
335
|
+
"""Locate the CURRENT terminal_title key span (fresh bytes, never cached)
|
|
336
|
+
and its parsed value; `not-line-shaped` is refused here too."""
|
|
337
|
+
parsed = parse_toml(text, f"config {config_path}")
|
|
338
|
+
shape, value = classify(parsed)
|
|
339
|
+
if shape.startswith("refuse:"):
|
|
340
|
+
die(4, f"codex-terminal-title: {config_path} now carries an explicit contrary value under `tui`/`{TITLE_KEY}` — refusing to guess; fix it yourself")
|
|
341
|
+
lines = text.split("\n")
|
|
342
|
+
infos = scan_lines(lines)
|
|
343
|
+
tui_family = find_table_family(lines, infos, TUI_PATH)
|
|
344
|
+
key_span = find_direct_key(lines, infos, tui_family[0], tui_family[1], TITLE_KEY) if tui_family is not None else None
|
|
345
|
+
return parsed, shape, value, lines, infos, tui_family, key_span
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def uninstall(state_path: str) -> None:
|
|
349
|
+
state = checked_state(state_path)
|
|
350
|
+
config_path = state["managedConfigPath"]
|
|
351
|
+
mode = state["detectMode"]
|
|
352
|
+
if os.path.islink(config_path):
|
|
353
|
+
die(3, f"codex-terminal-title: refusing to uninstall — {config_path} became a symlink (someone else's SSOT)")
|
|
354
|
+
|
|
355
|
+
text = read_text(config_path, "config")
|
|
356
|
+
if text is None:
|
|
357
|
+
os.remove(state_path)
|
|
358
|
+
sys.stdout.write(f"uninstalled {config_path} (config already absent)\n")
|
|
359
|
+
return
|
|
360
|
+
parsed, shape, value, lines, infos, tui_family, key_span = _current_key_span(config_path, text)
|
|
361
|
+
|
|
362
|
+
if key_span is None:
|
|
363
|
+
if shape == "has-key" and REQUIRED_ITEM in value:
|
|
364
|
+
# `tui`/`terminal_title` is gone by NAME but the path still holds
|
|
365
|
+
# thread-id — reshaped into inline/dotted TOML. This writer cannot
|
|
366
|
+
# span-edit that shape; treating it as absent would silently drop
|
|
367
|
+
# the receipt over an item we never actually took back. Refuse and
|
|
368
|
+
# keep the receipt so the operator can repair it.
|
|
369
|
+
die(
|
|
370
|
+
3,
|
|
371
|
+
f"codex-terminal-title: refusing to uninstall — `tui.{TITLE_KEY}` in {config_path} exists but not "
|
|
372
|
+
"as a literal `key = value` line (dotted/inline form) — this writer owns only line-shaped keys "
|
|
373
|
+
"and refuses to guess; the receipt is retained",
|
|
374
|
+
)
|
|
375
|
+
# else: thread-id is genuinely absent — our membership was taken back
|
|
376
|
+
# by hand, or vanished along with the key/table itself.
|
|
377
|
+
os.remove(state_path)
|
|
378
|
+
sys.stdout.write(f"uninstalled {config_path} (terminal_title already absent)\n")
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
current_span = span_text(lines, key_span[0], key_span[1] + 1)
|
|
382
|
+
current_value = value if shape == "has-key" else None
|
|
383
|
+
|
|
384
|
+
if mode == "merged-item":
|
|
385
|
+
if current_span != state["postimageSpan"]:
|
|
386
|
+
if isinstance(current_value, list) and REQUIRED_ITEM not in current_value:
|
|
387
|
+
os.remove(state_path) # operator removed our item themselves
|
|
388
|
+
sys.stdout.write(f"uninstalled {config_path} (thread-id already removed)\n")
|
|
389
|
+
return
|
|
390
|
+
die(
|
|
391
|
+
6,
|
|
392
|
+
f"codex-terminal-title: refusing: `tui.{TITLE_KEY}` in {config_path} changed since install "
|
|
393
|
+
"(operator edits present). Remove 'thread-id' yourself if you want it gone.",
|
|
394
|
+
)
|
|
395
|
+
snippet = state["snippet"]
|
|
396
|
+
if not isinstance(snippet, str) or snippet not in current_span:
|
|
397
|
+
die(7, "codex-terminal-title: internal refusal: recorded snippet is not in the recorded span")
|
|
398
|
+
restored = current_span.replace(snippet, "", 1)
|
|
399
|
+
restored_value = span_value(restored, "restored terminal_title").get(TITLE_KEY)
|
|
400
|
+
preimage = state["terminalTitlePreimage"]
|
|
401
|
+
preimage_value = span_value(preimage, "preimage terminal_title").get(TITLE_KEY) if preimage else None
|
|
402
|
+
if restored_value != preimage_value:
|
|
403
|
+
die(7, "codex-terminal-title: internal refusal: snippet removal does not reproduce the preimage value")
|
|
404
|
+
candidate_lines = splice(lines, key_span[0], key_span[1] + 1, restored.split("\n"))
|
|
405
|
+
elif mode in ("inserted-key", "appended-table", "created-new"):
|
|
406
|
+
if current_span != state["postimageSpan"]:
|
|
407
|
+
if isinstance(current_value, list) and REQUIRED_ITEM not in current_value:
|
|
408
|
+
os.remove(state_path)
|
|
409
|
+
sys.stdout.write(f"uninstalled {config_path} (thread-id already removed)\n")
|
|
410
|
+
return
|
|
411
|
+
die(
|
|
412
|
+
6,
|
|
413
|
+
f"codex-terminal-title: refusing: `tui.{TITLE_KEY}` in {config_path} changed since install "
|
|
414
|
+
"(operator edits present). Remove it yourself if you want it gone.",
|
|
415
|
+
)
|
|
416
|
+
candidate_lines = splice(lines, key_span[0], key_span[1] + 1, [])
|
|
417
|
+
# A [tui] block that WE appended and that now holds nothing else goes
|
|
418
|
+
# away whole; an operator's [tui] header always stays.
|
|
419
|
+
if not state["tuiTableExisted"] and tui_family is not None:
|
|
420
|
+
rescanned = scan_lines(candidate_lines)
|
|
421
|
+
family = find_table_family(candidate_lines, rescanned, TUI_PATH)
|
|
422
|
+
if family is not None:
|
|
423
|
+
family_body = span_text(candidate_lines, family[0] + 1, family[1])
|
|
424
|
+
if not [line for line in family_body.split("\n") if line.strip()]:
|
|
425
|
+
candidate_lines = splice(candidate_lines, family[0], family[1], [])
|
|
426
|
+
else:
|
|
427
|
+
die(4, f"codex-terminal-title: install-state {state_path} has unknown detectMode {mode!r}")
|
|
428
|
+
|
|
429
|
+
candidate = render_file(candidate_lines)
|
|
430
|
+
assert_blast_radius(text, candidate, TUI_PATH + (TITLE_KEY,), f"config {config_path}")
|
|
431
|
+
if state["detectMode"] == "created-new" and state["configExistedBefore"] is False and not [l for l in candidate_lines if l.strip()]:
|
|
432
|
+
os.remove(config_path)
|
|
433
|
+
os.remove(state_path)
|
|
434
|
+
sys.stdout.write(f"uninstalled {config_path} (file removed)\n")
|
|
435
|
+
return
|
|
436
|
+
atomic_write(config_path, candidate, prefix=".codex-tt-")
|
|
437
|
+
os.remove(state_path) # the receipt is deleted LAST
|
|
438
|
+
sys.stdout.write(f"uninstalled {config_path}\n")
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def effective_token(config_path: str) -> str:
|
|
442
|
+
"""The EFFECTIVE verdict, judged from the config alone — never the state."""
|
|
443
|
+
if os.path.islink(config_path):
|
|
444
|
+
return "symlink"
|
|
445
|
+
text = read_text(config_path, "config")
|
|
446
|
+
if text is None or not text.strip():
|
|
447
|
+
return "file-absent"
|
|
448
|
+
try:
|
|
449
|
+
parsed = parse_toml(text, f"config {config_path}")
|
|
450
|
+
except SystemExit:
|
|
451
|
+
return "invalid-toml"
|
|
452
|
+
shape, value = classify(parsed)
|
|
453
|
+
if shape in ("absent", "no-key"):
|
|
454
|
+
return "terminal-title-absent"
|
|
455
|
+
if shape.startswith("refuse:"):
|
|
456
|
+
return "malformed-value"
|
|
457
|
+
return "thread-id-present" if REQUIRED_ITEM in value else "thread-id-absent"
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def doctor_static(config_path: str, state_path: str) -> None:
|
|
461
|
+
"""One line, two axes: `<effective> <ownership>[ drift]`. The effective
|
|
462
|
+
token is computed without ever reading the receipt; ownership annotates and
|
|
463
|
+
never rescues. GREEN (exit 0) is exactly `thread-id-present`, owned or
|
|
464
|
+
unowned — an unowned file that already carries the item is green, and every
|
|
465
|
+
other effective token is RED because this doctor's subject is the REQUIRED
|
|
466
|
+
caller-seat axis, not our bookkeeping. Ownership still distinguishes the two
|
|
467
|
+
reds an operator repairs differently: `drift` means our receipt binds this
|
|
468
|
+
file and our item is gone (repair in place), while a bare
|
|
469
|
+
`<effective> unowned` means the axis was never established here."""
|
|
470
|
+
effective = effective_token(config_path)
|
|
471
|
+
state = load_state(state_path)
|
|
472
|
+
owned = (
|
|
473
|
+
state is not None
|
|
474
|
+
and state.get("schemaVersion") == STATE_SCHEMA_VERSION
|
|
475
|
+
and state.get("atom") == ATOM
|
|
476
|
+
and state.get("managedConfigPath") == os.path.abspath(config_path)
|
|
477
|
+
)
|
|
478
|
+
ownership = "owned" if owned else "unowned"
|
|
479
|
+
if effective != "thread-id-present":
|
|
480
|
+
sys.stdout.write(f"{effective} {ownership}{' drift' if owned else ''}\n")
|
|
481
|
+
raise SystemExit(1)
|
|
482
|
+
sys.stdout.write(f"{effective} {ownership}\n")
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def main(argv: list[str]) -> None:
|
|
486
|
+
if len(argv) < 2:
|
|
487
|
+
die(5, "usage: codex-terminal-title-config.py <install config state|uninstall state|doctor-static config state>")
|
|
488
|
+
verb = argv[1]
|
|
489
|
+
if verb == "install" and len(argv) == 4:
|
|
490
|
+
install(argv[2], argv[3])
|
|
491
|
+
elif verb == "uninstall" and len(argv) == 3:
|
|
492
|
+
uninstall(argv[2])
|
|
493
|
+
elif verb == "doctor-static" and len(argv) == 4:
|
|
494
|
+
doctor_static(argv[2], argv[3])
|
|
495
|
+
else:
|
|
496
|
+
die(5, "usage: codex-terminal-title-config.py <install config state|uninstall state|doctor-static config state>")
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
if __name__ == "__main__":
|
|
500
|
+
main(sys.argv)
|
package/scripts/codex_toml_io.py
CHANGED
|
@@ -522,6 +522,127 @@ def assert_blast_radius(original_text: str | None, candidate_text: str, owned_pa
|
|
|
522
522
|
return before, after
|
|
523
523
|
|
|
524
524
|
|
|
525
|
+
def array_append_point(span: str, search_from: int = 0) -> tuple[int, str] | None:
|
|
526
|
+
"""Where a new item goes at the END of the first array in `span`, as
|
|
527
|
+
`(insert_index, tail)` — or None when this span holds no array this writer
|
|
528
|
+
can own.
|
|
529
|
+
|
|
530
|
+
`tail` says what the last meaningful byte inside the array was, because
|
|
531
|
+
that decides the snippet an atom appends:
|
|
532
|
+
|
|
533
|
+
``empty`` the array has no items — insert ``"x"``
|
|
534
|
+
``item`` it ends with an item — insert ``, "x"``
|
|
535
|
+
``comma`` it ends with a trailing comma (the multi-line style)
|
|
536
|
+
— insert `` "x",`` AFTER that comma
|
|
537
|
+
|
|
538
|
+
The naive locator — append before ``span.rfind("]")`` — is wrong on three
|
|
539
|
+
real operator shapes: a trailing comment (``items = ["a"] # keep ] this``),
|
|
540
|
+
a ``]`` inside a string item, and the multi-line array whose last item
|
|
541
|
+
already carries a comma (appending ``, "x"`` there emits ``,,``). So this
|
|
542
|
+
walks the span with the SAME string/comment rules `_scan_text` applies, but
|
|
543
|
+
reports POSITIONS, which `_scan_text` cannot: it is line-oriented (its
|
|
544
|
+
single-line string and escape flags are per call, so it may not be driven
|
|
545
|
+
character by character) and it returns only a resume index. Hence one
|
|
546
|
+
self-contained walk here rather than a wrapper.
|
|
547
|
+
|
|
548
|
+
Returning None is a SHAPE answer, never an error: a span with no
|
|
549
|
+
line-shaped array is a thing this writer does not own, and the caller
|
|
550
|
+
refuses it by name instead of splicing into someone's comment.
|
|
551
|
+
"""
|
|
552
|
+
i = search_from
|
|
553
|
+
n = len(span)
|
|
554
|
+
depth = 0
|
|
555
|
+
open_at = -1
|
|
556
|
+
last_meaningful = -1 # index AFTER the last non-space byte inside the array
|
|
557
|
+
basic_ml = literal_ml = in_basic = in_literal = escaped = False
|
|
558
|
+
|
|
559
|
+
def mark(end: int) -> None:
|
|
560
|
+
nonlocal last_meaningful
|
|
561
|
+
if depth >= 1:
|
|
562
|
+
last_meaningful = end
|
|
563
|
+
|
|
564
|
+
while i < n:
|
|
565
|
+
ch = span[i]
|
|
566
|
+
if basic_ml or literal_ml:
|
|
567
|
+
quote = '"' if basic_ml else "'"
|
|
568
|
+
if ch == quote:
|
|
569
|
+
run = 1
|
|
570
|
+
while i + run < n and span[i + run] == quote:
|
|
571
|
+
run += 1
|
|
572
|
+
if run >= 3:
|
|
573
|
+
basic_ml = literal_ml = False
|
|
574
|
+
i += 3
|
|
575
|
+
mark(i)
|
|
576
|
+
continue
|
|
577
|
+
i += run
|
|
578
|
+
mark(i)
|
|
579
|
+
continue
|
|
580
|
+
i += 1
|
|
581
|
+
mark(i)
|
|
582
|
+
continue
|
|
583
|
+
if in_basic:
|
|
584
|
+
if escaped:
|
|
585
|
+
escaped = False
|
|
586
|
+
elif ch == "\\":
|
|
587
|
+
escaped = True
|
|
588
|
+
elif ch == '"':
|
|
589
|
+
in_basic = False
|
|
590
|
+
i += 1
|
|
591
|
+
mark(i)
|
|
592
|
+
continue
|
|
593
|
+
if in_literal:
|
|
594
|
+
if ch == "'":
|
|
595
|
+
in_literal = False
|
|
596
|
+
i += 1
|
|
597
|
+
mark(i)
|
|
598
|
+
continue
|
|
599
|
+
if ch in "\"'":
|
|
600
|
+
run = 1
|
|
601
|
+
while i + run < n and span[i + run] == ch:
|
|
602
|
+
run += 1
|
|
603
|
+
if run >= 3:
|
|
604
|
+
basic_ml, literal_ml = ch == '"', ch == "'"
|
|
605
|
+
i += 3
|
|
606
|
+
elif run == 2:
|
|
607
|
+
i += 2 # an empty string opens nothing
|
|
608
|
+
else:
|
|
609
|
+
in_basic, in_literal = ch == '"', ch == "'"
|
|
610
|
+
i += 1
|
|
611
|
+
mark(i)
|
|
612
|
+
continue
|
|
613
|
+
if ch == "#":
|
|
614
|
+
# A comment runs to end of line and is NOT meaningful content.
|
|
615
|
+
# Inside an OPEN array that is legal TOML and the array continues
|
|
616
|
+
# on the next line; outside one there is nothing left to close.
|
|
617
|
+
newline = span.find("\n", i)
|
|
618
|
+
if depth == 0 or newline < 0:
|
|
619
|
+
return None
|
|
620
|
+
i = newline + 1
|
|
621
|
+
continue
|
|
622
|
+
if ch in "[{":
|
|
623
|
+
depth += 1
|
|
624
|
+
if depth == 1 and ch == "[":
|
|
625
|
+
open_at = i
|
|
626
|
+
mark(i + 1)
|
|
627
|
+
i += 1
|
|
628
|
+
continue
|
|
629
|
+
if ch in "]}":
|
|
630
|
+
depth -= 1
|
|
631
|
+
if depth < 0:
|
|
632
|
+
return None
|
|
633
|
+
if depth == 0 and ch == "]" and open_at >= 0:
|
|
634
|
+
if last_meaningful <= open_at + 1:
|
|
635
|
+
return open_at + 1, "empty"
|
|
636
|
+
return last_meaningful, "comma" if span[last_meaningful - 1] == "," else "item"
|
|
637
|
+
mark(i + 1)
|
|
638
|
+
i += 1
|
|
639
|
+
continue
|
|
640
|
+
i += 1
|
|
641
|
+
if not ch.isspace():
|
|
642
|
+
mark(i)
|
|
643
|
+
return None
|
|
644
|
+
|
|
645
|
+
|
|
525
646
|
def toml_string(value: str) -> str:
|
|
526
647
|
# JSON double-quoted strings are valid TOML basic strings for every value
|
|
527
648
|
# json can emit (\", \\, \n, \uXXXX — all legal TOML escapes).
|
|
@@ -39,7 +39,6 @@ export interface CodexInstructionInput {
|
|
|
39
39
|
finalToken: string;
|
|
40
40
|
callerGid: string;
|
|
41
41
|
piModel: string;
|
|
42
|
-
scratch: string;
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
/** The payload the initial Pi forwards verbatim. It names only Codex's own steps. */
|
|
@@ -47,8 +46,14 @@ export function buildCodexInstruction(input: CodexInstructionInput): string {
|
|
|
47
46
|
return (
|
|
48
47
|
`${input.waitToken}\nThis is the addressed instruction from the initial Pi caller. Execute once, with no shell, ` +
|
|
49
48
|
"terminal, screen inspection, or retry:\n" +
|
|
50
|
-
`1. Call public mcp__entwurf_bridge__entwurf_fresh_call once with backend pi, model ${input.piModel},
|
|
51
|
-
|
|
49
|
+
`1. Call public mcp__entwurf_bridge__entwurf_fresh_call once with backend pi, model ${input.piModel}, ` +
|
|
50
|
+
// NO placement AND NO cwd are both CLAIMS rather than conveniences, and they are two
|
|
51
|
+
// different ones. The seat must come from this Codex's own pane title (#95 lane B); the
|
|
52
|
+
// DIRECTORY must come from this Codex's own record (#95 lane C), which is a fact only
|
|
53
|
+
// because its thread now opens where its pane is rather than where the app-server sits.
|
|
54
|
+
// Omitting the parameter is the whole point — passing one would prove neither.
|
|
55
|
+
'NO cwd parameter and NO placement parameter, and task "After your automatic callback succeeds, answer ACK and ' +
|
|
56
|
+
'stop. Do not open another sibling."\n' +
|
|
52
57
|
"2. Stop and wait for the Pi callback delivered into this Codex thread. Accept only a callback whose body " +
|
|
53
58
|
"repeats the exact nonce from your LAUNCH receipt and whose sender envelope names a garden id.\n" +
|
|
54
59
|
`3. Only after that match, call public mcp__entwurf_bridge__entwurf_v2 once with target ${input.callerGid}, ` +
|
|
@@ -71,6 +76,9 @@ export interface InitialPiPhaseOneInput {
|
|
|
71
76
|
export function buildInitialPiPhaseOne(input: InitialPiPhaseOneInput): string {
|
|
72
77
|
return (
|
|
73
78
|
`${input.initialPiWaitToken}\nExecute this public-tool chain exactly once; do not use shell, terminal, screen text, or retries:\n` +
|
|
79
|
+
// NO placement on BOTH legs since #95 D1: an omitted seat is the caller's own session for
|
|
80
|
+
// every backend, so this Codex opens in S beside the Pi that called it — which is what
|
|
81
|
+
// puts its TUI somewhere the app-server's inherited `TMUX` does not point.
|
|
74
82
|
`1. Call entwurf_fresh_call with backend codex, model ${input.codexModel}, cwd ${input.scratch}, NO placement, and task ` +
|
|
75
83
|
`"After your required callback receipt, end the turn and wait passively for the addressed instruction containing ${input.codexWaitToken}. ` +
|
|
76
84
|
`Do not call shell, sleep, terminal, or any tool to wait."\n` +
|