@junghanacs/entwurf 0.20.1 → 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 +85 -190
- package/BASELINE.md +6 -3
- package/CHANGELOG.md +473 -14
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +332 -60
- package/README.md +100 -22
- package/VERIFY.md +93 -7
- package/docs/acp-backend-rail.md +0 -1
- package/docs/external-mcp-host.md +64 -33
- package/docs/setup-clean-host.md +151 -17
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +130 -114
- 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 +519 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -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/entwurf-v2-surface.js +7 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +189 -19
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
- package/mcp/entwurf-bridge/src/index.ts +154 -116
- package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
- package/package.json +13 -4
- package/pi-extensions/entwurf-control.ts +71 -19
- package/pi-extensions/lib/codex-caller-seat.ts +204 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +580 -0
- package/pi-extensions/lib/compaction-send-guard.ts +80 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
- package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
- package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
- package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
- package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
- package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
- package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
- package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
- package/pi-extensions/lib/meta-sender-identity.ts +305 -0
- package/pi-extensions/lib/mux-fresh-call.ts +233 -29
- package/pi-extensions/lib/native-push/adapter.ts +21 -24
- package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
- package/pi-extensions/lib/native-push/register.ts +7 -9
- package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
- package/run.sh +342 -28
- package/scripts/check-agy-sender-identity.ts +1 -1
- package/scripts/check-codex-app-server-launch.ts +445 -0
- package/scripts/check-codex-birth-hook.ts +264 -0
- package/scripts/check-codex-bridge-identity.ts +179 -0
- package/scripts/check-codex-native-push.ts +386 -0
- package/scripts/check-codex-sender-identity.ts +495 -0
- package/scripts/check-compaction-send-guard.ts +130 -0
- package/scripts/check-copilot-receive-arm.ts +4 -1
- package/scripts/check-entwurf-fact-provider.ts +38 -0
- package/scripts/check-entwurf-peers-surface.ts +13 -1
- package/scripts/check-entwurf-self-address.ts +15 -16
- package/scripts/check-entwurf-v2-contract.ts +4 -3
- package/scripts/check-entwurf-v2-decider.ts +7 -5
- package/scripts/check-entwurf-v2-native-push.ts +35 -7
- package/scripts/check-entwurf-v2-production.ts +245 -12
- package/scripts/check-entwurf-v2-runner.ts +1 -1
- package/scripts/check-entwurf-v2-send.ts +26 -7
- package/scripts/check-entwurf-v2-surface.ts +1 -1
- package/scripts/check-gate-qualification.ts +8 -3
- package/scripts/check-harness-admission-parity.ts +0 -1
- package/scripts/check-mux-launch-tmux.ts +345 -4
- package/scripts/check-native-push-adapter.ts +20 -16
- package/scripts/check-native-push-register.ts +5 -1
- package/scripts/check-release-gate-outcomes.ts +47 -1
- package/scripts/check-setup-qualification.sh +3 -1
- package/scripts/codex-app-server-launch.sh +275 -0
- package/scripts/codex-birth-doctor.sh +276 -0
- package/scripts/codex-birth-install.sh +414 -0
- package/scripts/codex-birth-uninstall.sh +170 -0
- package/scripts/codex-mcp-config.py +435 -0
- package/scripts/codex-socket-path.ts +33 -0
- package/scripts/codex-statusline-config.py +434 -0
- package/scripts/codex-terminal-title-config.py +500 -0
- package/scripts/codex_toml_io.py +653 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +121 -0
- package/scripts/lib/codex-fresh-source-receipts.ts +426 -0
- package/scripts/lib/launch-receipt-windows.ts +46 -0
- package/scripts/lib/tmux-coordinate-row.ts +58 -0
- package/scripts/meta-bridge-fresh-cut.ts +6 -1
- 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 +838 -0
- package/scripts/mutants/compaction-send-guard.json +103 -0
- package/scripts/mutants/entwurf-peers.json +19 -0
- package/scripts/mutants/mux-fresh-call.json +93 -10
- package/scripts/mutants/omp-fresh.json +6 -4
- package/scripts/mutants/release-gate.json +13 -0
- package/scripts/mutants/v2-surface.json +75 -1
- package/scripts/raw-async-delivery/README.md +2 -1
- package/scripts/raw-codex-measure/README.md +114 -46
- package/scripts/smoke-agy-native-push-live.ts +3 -1
- package/scripts/smoke-codex-birth.sh +347 -0
- package/scripts/smoke-codex-config-state.sh +700 -0
- package/scripts/smoke-codex-fresh-live.ts +1426 -0
- package/scripts/smoke-codex-native-push-live.ts +75 -0
- package/scripts/smoke-entwurf-chain-live.ts +50 -0
- package/scripts/smoke-setup-verdict.sh +125 -10
- package/scripts/tsconfig.json +1 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""codex-statusline-config — stateful `[tui].status_line` adapter; stdlib only.
|
|
3
|
+
|
|
4
|
+
One reversible atom in `$CODEX_HOME/config.toml`: ensure the TUI's closed-enum
|
|
5
|
+
status line INCLUDES `thread-title` — the only carrier that can hold a visible
|
|
6
|
+
garden id (measured S1b-B in scripts/raw-codex-measure/README.md: an explicitly
|
|
7
|
+
set name survives auto-titling in both orderings). The atom owns exactly the
|
|
8
|
+
`thread-title` MEMBERSHIP, never the list: operator items, their order and
|
|
9
|
+
their bytes survive untouched (a text insert right after the opening bracket,
|
|
10
|
+
not a re-serialization), and the inverse removes exactly the snippet that was
|
|
11
|
+
added. `status_line` is snake_case in TOML — the vendor's own key on this host.
|
|
12
|
+
|
|
13
|
+
Refuses, with the offending name in the message: a symlinked config (someone
|
|
14
|
+
else's SSOT), malformed TOML, a foreign install-state, a state retargeted at
|
|
15
|
+
another config path, and an explicit contrary operator value by name — a
|
|
16
|
+
`status_line` that is not an array of items is the operator's decision, not
|
|
17
|
+
drift to overwrite.
|
|
18
|
+
|
|
19
|
+
The doctor judges EFFECTIVE thread-title from the config alone, independent of
|
|
20
|
+
the state receipt (the operator may set it themselves — that is green, not
|
|
21
|
+
drift). Its subject is that REQUIRED visible-identity axis, so a present item is
|
|
22
|
+
the only green and every other effective token is red; the state never rescues
|
|
23
|
+
one, it only annotates ownership and adds `drift` when our receipt binds the
|
|
24
|
+
file whose item is gone.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import sys
|
|
32
|
+
sys.dont_write_bytecode = True
|
|
33
|
+
|
|
34
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
35
|
+
from codex_toml_io import ( # noqa: E402
|
|
36
|
+
append_block,
|
|
37
|
+
assert_blast_radius,
|
|
38
|
+
atomic_write,
|
|
39
|
+
atomic_write_state,
|
|
40
|
+
deep_get,
|
|
41
|
+
die,
|
|
42
|
+
find_direct_key,
|
|
43
|
+
find_table_family,
|
|
44
|
+
now,
|
|
45
|
+
parse_toml,
|
|
46
|
+
read_text,
|
|
47
|
+
render_file,
|
|
48
|
+
render_string_array,
|
|
49
|
+
scan_lines,
|
|
50
|
+
span_text,
|
|
51
|
+
span_value,
|
|
52
|
+
splice,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
STATE_SCHEMA_VERSION = 1
|
|
56
|
+
ATOM = "codex-statusline"
|
|
57
|
+
TUI_PATH = ("tui",)
|
|
58
|
+
STATUSLINE_KEY = "status_line"
|
|
59
|
+
REQUIRED_ITEM = "thread-title"
|
|
60
|
+
STATE_REQUIRED = (
|
|
61
|
+
"managedConfigPath",
|
|
62
|
+
"atom",
|
|
63
|
+
"detectMode",
|
|
64
|
+
"configExistedBefore",
|
|
65
|
+
"tuiTableExisted",
|
|
66
|
+
"statusLineExisted",
|
|
67
|
+
"statusLinePreimage",
|
|
68
|
+
"postimageSpan",
|
|
69
|
+
"snippet",
|
|
70
|
+
"installedAt",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# detectMode → what the inverse takes back:
|
|
74
|
+
# created-new the whole file (we made it; only if nothing else remains)
|
|
75
|
+
# appended-table the [tui] block we appended (only if it is still only ours)
|
|
76
|
+
# inserted-key the `status_line = [...]` key lines we inserted
|
|
77
|
+
# merged-item the exact `"thread-title", ` snippet spliced after `[`
|
|
78
|
+
# already-present nothing — the operator had it before we ever ran
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_state(state_path: str) -> dict | None:
|
|
82
|
+
raw = read_text(state_path, "install-state")
|
|
83
|
+
if raw is None:
|
|
84
|
+
return None
|
|
85
|
+
if not raw.strip():
|
|
86
|
+
die(4, f"codex-statusline: install-state {state_path} is empty — refusing to guess")
|
|
87
|
+
try:
|
|
88
|
+
state = json.loads(raw)
|
|
89
|
+
except ValueError as error:
|
|
90
|
+
die(4, f"codex-statusline: install-state {state_path} is not valid JSON: {error}")
|
|
91
|
+
if not isinstance(state, dict):
|
|
92
|
+
die(4, f"codex-statusline: install-state {state_path} top-level must be a JSON object")
|
|
93
|
+
return state
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def checked_state(state_path: str) -> dict:
|
|
97
|
+
state = load_state(state_path)
|
|
98
|
+
if state is None:
|
|
99
|
+
die(2, f"codex-statusline: no install-state at {state_path} — nothing to undo")
|
|
100
|
+
if state.get("schemaVersion") != STATE_SCHEMA_VERSION:
|
|
101
|
+
die(4, f"codex-statusline: install-state {state_path} is not schemaVersion {STATE_SCHEMA_VERSION} — refusing")
|
|
102
|
+
atom = state.get("atom")
|
|
103
|
+
if atom != ATOM:
|
|
104
|
+
die(4, f"codex-statusline: install-state {state_path} belongs to atom {atom!r}, not {ATOM!r} — refusing to touch it")
|
|
105
|
+
if any(key not in state for key in STATE_REQUIRED):
|
|
106
|
+
die(4, f"codex-statusline: install-state {state_path} has an unsupported shape — refusing")
|
|
107
|
+
path = state.get("managedConfigPath")
|
|
108
|
+
if not isinstance(path, str) or not os.path.isabs(path):
|
|
109
|
+
die(4, f"codex-statusline: install-state {state_path} has no absolute managedConfigPath")
|
|
110
|
+
return state
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def prior_state(state_path: str, config_path: str) -> dict | None:
|
|
114
|
+
state = load_state(state_path)
|
|
115
|
+
if state is None:
|
|
116
|
+
return None
|
|
117
|
+
if state.get("schemaVersion") != STATE_SCHEMA_VERSION or state.get("atom") != ATOM:
|
|
118
|
+
checked_state(state_path) # the honest, named refusal
|
|
119
|
+
if state.get("managedConfigPath") != os.path.abspath(config_path):
|
|
120
|
+
die(
|
|
121
|
+
3,
|
|
122
|
+
f"codex-statusline: install-state {state_path} is bound to {state.get('managedConfigPath')}, "
|
|
123
|
+
f"not {os.path.abspath(config_path)} — refusing to retarget an install onto another config",
|
|
124
|
+
)
|
|
125
|
+
return checked_state(state_path)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def classify(parsed: dict) -> tuple[str, object]:
|
|
129
|
+
"""`(tui-shape, status_line-value)`; tui-shape is `table`, `refuse:<name>`,
|
|
130
|
+
or `not-line-shaped` only in combination with the span search."""
|
|
131
|
+
tui = deep_get(parsed, TUI_PATH)
|
|
132
|
+
if tui is None:
|
|
133
|
+
return "absent", None
|
|
134
|
+
if not isinstance(tui, dict):
|
|
135
|
+
return "refuse:tui", tui
|
|
136
|
+
value = tui.get(STATUSLINE_KEY)
|
|
137
|
+
if value is None:
|
|
138
|
+
return "no-key", None
|
|
139
|
+
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
|
140
|
+
return "refuse:status_line", value
|
|
141
|
+
return "has-key", value
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def install(config_path: str, state_path: str) -> None:
|
|
145
|
+
if os.path.islink(config_path):
|
|
146
|
+
die(3, f"codex-statusline: refusing to adopt {config_path} — symlink to {os.readlink(config_path)} (someone else's SSOT)")
|
|
147
|
+
if os.path.exists(config_path) and not os.path.isfile(config_path):
|
|
148
|
+
die(4, f"codex-statusline: refusing: {config_path} exists and is not a regular file")
|
|
149
|
+
|
|
150
|
+
text = read_text(config_path, "config")
|
|
151
|
+
parsed = parse_toml(text if text is not None else "", f"config {config_path}") if text is not None else {}
|
|
152
|
+
shape, value = classify(parsed)
|
|
153
|
+
if shape.startswith("refuse:"):
|
|
154
|
+
name = shape.split(":", 1)[1]
|
|
155
|
+
die(
|
|
156
|
+
3,
|
|
157
|
+
f"codex-statusline: refusing: {'.'.join(TUI_PATH)}{'.' + STATUSLINE_KEY if name == STATUSLINE_KEY else ''} in {config_path} "
|
|
158
|
+
f"is explicitly a {type(value).__name__}, not {'an array of status-line items' if name == STATUSLINE_KEY else 'a table'}. "
|
|
159
|
+
"That is the operator's decision, not drift — entwurf will not overwrite it by name.",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
lines = (text if text is not None else "").split("\n")
|
|
163
|
+
infos = scan_lines(lines)
|
|
164
|
+
tui_family = find_table_family(lines, infos, TUI_PATH)
|
|
165
|
+
key_span = find_direct_key(lines, infos, tui_family[0], tui_family[1], STATUSLINE_KEY) if tui_family is not None else None
|
|
166
|
+
if shape in ("has-key", "no-key") and tui_family is None:
|
|
167
|
+
die(
|
|
168
|
+
4,
|
|
169
|
+
f"codex-statusline: [tui] in {config_path} exists but not as a literal table block "
|
|
170
|
+
"(inline/dotted form) — this writer owns only line-shaped tables and refuses to touch it",
|
|
171
|
+
)
|
|
172
|
+
if shape == "has-key" and key_span is None:
|
|
173
|
+
die(
|
|
174
|
+
4,
|
|
175
|
+
f"codex-statusline: `tui.{STATUSLINE_KEY}` in {config_path} exists but not as a literal `key = value` line "
|
|
176
|
+
"(dotted/inline form) — this writer owns only line-shaped keys and refuses to touch it",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
canonical_line = f"{STATUSLINE_KEY} = {render_string_array([REQUIRED_ITEM])}"
|
|
180
|
+
prior = prior_state(state_path, config_path)
|
|
181
|
+
|
|
182
|
+
if shape == "has-key":
|
|
183
|
+
if REQUIRED_ITEM in value:
|
|
184
|
+
mode = "already-present"
|
|
185
|
+
new_lines = lines # untouched: the strongest idempotence is no write at all
|
|
186
|
+
preimage = span_text(lines, key_span[0], key_span[1] + 1)
|
|
187
|
+
postimage_span = preimage
|
|
188
|
+
snippet = None
|
|
189
|
+
else:
|
|
190
|
+
mode = "merged-item"
|
|
191
|
+
preimage = span_text(lines, key_span[0], key_span[1] + 1)
|
|
192
|
+
snippet = f'"{REQUIRED_ITEM}", ' if value else f'"{REQUIRED_ITEM}"'
|
|
193
|
+
old_span = preimage
|
|
194
|
+
bracket = old_span.index("[", old_span.index("="))
|
|
195
|
+
new_span = old_span[: bracket + 1] + snippet + old_span[bracket + 1:]
|
|
196
|
+
# The scanner's own receipt: the merged span must parse to exactly
|
|
197
|
+
# the old list with our item prepended, or we refuse to splice it.
|
|
198
|
+
merged_value = span_value(new_span, "merged status_line").get(STATUSLINE_KEY)
|
|
199
|
+
if merged_value != [REQUIRED_ITEM] + list(value):
|
|
200
|
+
die(7, "codex-statusline: internal refusal: merged status_line span does not re-parse to old items + thread-title")
|
|
201
|
+
postimage_span = new_span
|
|
202
|
+
new_lines = splice(lines, key_span[0], key_span[1] + 1, new_span.split("\n"))
|
|
203
|
+
elif shape == "no-key":
|
|
204
|
+
mode = "inserted-key"
|
|
205
|
+
preimage = None
|
|
206
|
+
postimage_span = canonical_line
|
|
207
|
+
snippet = None
|
|
208
|
+
new_lines = lines[: tui_family[0] + 1] + [canonical_line] + lines[tui_family[0] + 1:]
|
|
209
|
+
elif shape == "absent":
|
|
210
|
+
block = [f"[{TUI_PATH[0]}]", canonical_line]
|
|
211
|
+
if text is None:
|
|
212
|
+
mode = "created-new"
|
|
213
|
+
new_lines = list(block)
|
|
214
|
+
else:
|
|
215
|
+
mode = "appended-table"
|
|
216
|
+
new_lines = append_block(lines, block)
|
|
217
|
+
preimage = None
|
|
218
|
+
postimage_span = canonical_line
|
|
219
|
+
snippet = None
|
|
220
|
+
else: # pragma: no cover — classify() exhausted above
|
|
221
|
+
die(7, f"codex-statusline: internal: unhandled shape {shape!r}")
|
|
222
|
+
|
|
223
|
+
if prior is None:
|
|
224
|
+
state = {
|
|
225
|
+
"schemaVersion": STATE_SCHEMA_VERSION,
|
|
226
|
+
"managedConfigPath": os.path.abspath(config_path),
|
|
227
|
+
"atom": ATOM,
|
|
228
|
+
"detectMode": mode,
|
|
229
|
+
"configExistedBefore": text is not None,
|
|
230
|
+
"tuiTableExisted": shape in ("has-key", "no-key"),
|
|
231
|
+
"statusLineExisted": shape == "has-key",
|
|
232
|
+
# The FIRST preimage, captured once; a reinstall never re-captures.
|
|
233
|
+
"statusLinePreimage": preimage,
|
|
234
|
+
"postimageSpan": postimage_span,
|
|
235
|
+
"snippet": snippet,
|
|
236
|
+
"installedAt": now(),
|
|
237
|
+
}
|
|
238
|
+
else:
|
|
239
|
+
state = prior
|
|
240
|
+
|
|
241
|
+
candidate = render_file(new_lines)
|
|
242
|
+
_, after = assert_blast_radius(text, candidate, TUI_PATH + (STATUSLINE_KEY,), f"config {config_path}")
|
|
243
|
+
effective = deep_get(after, TUI_PATH + (STATUSLINE_KEY,))
|
|
244
|
+
if shape == "has-key":
|
|
245
|
+
expected = list(value) if mode == "already-present" else [REQUIRED_ITEM] + list(value)
|
|
246
|
+
else:
|
|
247
|
+
expected = [REQUIRED_ITEM]
|
|
248
|
+
if effective != expected:
|
|
249
|
+
die(7, f"codex-statusline: internal refusal: candidate status_line is not the operator's items plus thread-title")
|
|
250
|
+
|
|
251
|
+
# The receipt is durable BEFORE the operator's config is ever touched: it
|
|
252
|
+
# already carries the exact preimage/snippet of the bytes about to be
|
|
253
|
+
# replaced (or a prior receipt's, on a re-run). A crash or write failure
|
|
254
|
+
# between here and the config write below leaves the operator's config
|
|
255
|
+
# untouched and the receipt correct — a retry resumes from it instead of
|
|
256
|
+
# mistaking our own canonical postimage for a fresh operator preimage.
|
|
257
|
+
atomic_write_state(state_path, state)
|
|
258
|
+
if candidate != (text if text is not None else ""):
|
|
259
|
+
atomic_write(config_path, candidate, prefix=".codex-sl-")
|
|
260
|
+
sys.stdout.write(f"{mode} {os.path.abspath(config_path)}\n")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _current_key_span(config_path: str, text: str):
|
|
264
|
+
"""Locate the CURRENT status_line key span (fresh bytes, never cached) and
|
|
265
|
+
its parsed value; `not-line-shaped` is refused here too."""
|
|
266
|
+
parsed = parse_toml(text, f"config {config_path}")
|
|
267
|
+
shape, value = classify(parsed)
|
|
268
|
+
if shape.startswith("refuse:"):
|
|
269
|
+
die(4, f"codex-statusline: {config_path} now carries an explicit contrary value under `tui`/`{STATUSLINE_KEY}` — refusing to guess; fix it yourself")
|
|
270
|
+
lines = text.split("\n")
|
|
271
|
+
infos = scan_lines(lines)
|
|
272
|
+
tui_family = find_table_family(lines, infos, TUI_PATH)
|
|
273
|
+
key_span = find_direct_key(lines, infos, tui_family[0], tui_family[1], STATUSLINE_KEY) if tui_family is not None else None
|
|
274
|
+
return parsed, shape, value, lines, infos, tui_family, key_span
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def uninstall(state_path: str) -> None:
|
|
278
|
+
state = checked_state(state_path)
|
|
279
|
+
config_path = state["managedConfigPath"]
|
|
280
|
+
mode = state["detectMode"]
|
|
281
|
+
if os.path.islink(config_path):
|
|
282
|
+
die(3, f"codex-statusline: refusing to uninstall — {config_path} became a symlink (someone else's SSOT)")
|
|
283
|
+
|
|
284
|
+
text = read_text(config_path, "config")
|
|
285
|
+
if text is None:
|
|
286
|
+
os.remove(state_path)
|
|
287
|
+
sys.stdout.write(f"uninstalled {config_path} (config already absent)\n")
|
|
288
|
+
return
|
|
289
|
+
parsed, shape, value, lines, infos, tui_family, key_span = _current_key_span(config_path, text)
|
|
290
|
+
|
|
291
|
+
if mode == "already-present":
|
|
292
|
+
os.remove(state_path) # nothing was taken; nothing is given back
|
|
293
|
+
sys.stdout.write(f"uninstalled {config_path} (already present before install; nothing to take back)\n")
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
if key_span is None:
|
|
297
|
+
if shape == "has-key" and REQUIRED_ITEM in value:
|
|
298
|
+
# `tui`/`status_line` is gone by NAME but the path still holds
|
|
299
|
+
# thread-title — reshaped into inline/dotted TOML. This writer
|
|
300
|
+
# cannot span-edit that shape; treating it as absent would
|
|
301
|
+
# silently drop the receipt over an item we never actually took
|
|
302
|
+
# back. Refuse and keep the receipt so the operator can repair it.
|
|
303
|
+
die(
|
|
304
|
+
3,
|
|
305
|
+
f"codex-statusline: refusing to uninstall — `tui.{STATUSLINE_KEY}` in {config_path} exists but not "
|
|
306
|
+
"as a literal `key = value` line (dotted/inline form) — this writer owns only line-shaped keys "
|
|
307
|
+
"and refuses to guess; the receipt is retained",
|
|
308
|
+
)
|
|
309
|
+
# else: thread-title is genuinely absent — our membership was taken
|
|
310
|
+
# back by hand, or vanished along with the key/table itself.
|
|
311
|
+
os.remove(state_path)
|
|
312
|
+
sys.stdout.write(f"uninstalled {config_path} (status_line already absent)\n")
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
current_span = span_text(lines, key_span[0], key_span[1] + 1)
|
|
316
|
+
current_value = value if shape == "has-key" else None
|
|
317
|
+
|
|
318
|
+
if mode == "merged-item":
|
|
319
|
+
if current_span != state["postimageSpan"]:
|
|
320
|
+
if isinstance(current_value, list) and REQUIRED_ITEM not in current_value:
|
|
321
|
+
os.remove(state_path) # operator removed our item themselves
|
|
322
|
+
sys.stdout.write(f"uninstalled {config_path} (thread-title already removed)\n")
|
|
323
|
+
return
|
|
324
|
+
die(
|
|
325
|
+
6,
|
|
326
|
+
f"codex-statusline: refusing: `tui.{STATUSLINE_KEY}` in {config_path} changed since install "
|
|
327
|
+
"(operator edits present). Remove 'thread-title' yourself if you want it gone.",
|
|
328
|
+
)
|
|
329
|
+
snippet = state["snippet"]
|
|
330
|
+
if not isinstance(snippet, str) or snippet not in current_span:
|
|
331
|
+
die(7, "codex-statusline: internal refusal: recorded snippet is not in the recorded span")
|
|
332
|
+
restored = current_span.replace(snippet, "", 1)
|
|
333
|
+
restored_value = span_value(restored, "restored status_line").get(STATUSLINE_KEY)
|
|
334
|
+
preimage = state["statusLinePreimage"]
|
|
335
|
+
preimage_value = span_value(preimage, "preimage status_line").get(STATUSLINE_KEY) if preimage else None
|
|
336
|
+
if restored_value != preimage_value:
|
|
337
|
+
die(7, "codex-statusline: internal refusal: snippet removal does not reproduce the preimage value")
|
|
338
|
+
candidate_lines = splice(lines, key_span[0], key_span[1] + 1, restored.split("\n"))
|
|
339
|
+
elif mode in ("inserted-key", "appended-table", "created-new"):
|
|
340
|
+
if current_span != state["postimageSpan"]:
|
|
341
|
+
if isinstance(current_value, list) and REQUIRED_ITEM not in current_value:
|
|
342
|
+
os.remove(state_path)
|
|
343
|
+
sys.stdout.write(f"uninstalled {config_path} (thread-title already removed)\n")
|
|
344
|
+
return
|
|
345
|
+
die(
|
|
346
|
+
6,
|
|
347
|
+
f"codex-statusline: refusing: `tui.{STATUSLINE_KEY}` in {config_path} changed since install "
|
|
348
|
+
"(operator edits present). Remove it yourself if you want it gone.",
|
|
349
|
+
)
|
|
350
|
+
candidate_lines = splice(lines, key_span[0], key_span[1] + 1, [])
|
|
351
|
+
# A [tui] block that WE appended and that now holds nothing else goes
|
|
352
|
+
# away whole; an operator's [tui] header always stays.
|
|
353
|
+
if not state["tuiTableExisted"] and tui_family is not None:
|
|
354
|
+
rescanned = scan_lines(candidate_lines)
|
|
355
|
+
family = find_table_family(candidate_lines, rescanned, TUI_PATH)
|
|
356
|
+
if family is not None:
|
|
357
|
+
family_body = span_text(candidate_lines, family[0] + 1, family[1])
|
|
358
|
+
if not [line for line in family_body.split("\n") if line.strip()]:
|
|
359
|
+
candidate_lines = splice(candidate_lines, family[0], family[1], [])
|
|
360
|
+
else:
|
|
361
|
+
die(4, f"codex-statusline: install-state {state_path} has unknown detectMode {mode!r}")
|
|
362
|
+
|
|
363
|
+
candidate = render_file(candidate_lines)
|
|
364
|
+
assert_blast_radius(text, candidate, TUI_PATH + (STATUSLINE_KEY,), f"config {config_path}")
|
|
365
|
+
if state["detectMode"] == "created-new" and state["configExistedBefore"] is False and not [l for l in candidate_lines if l.strip()]:
|
|
366
|
+
os.remove(config_path)
|
|
367
|
+
os.remove(state_path)
|
|
368
|
+
sys.stdout.write(f"uninstalled {config_path} (file removed)\n")
|
|
369
|
+
return
|
|
370
|
+
atomic_write(config_path, candidate, prefix=".codex-sl-")
|
|
371
|
+
os.remove(state_path) # the receipt is deleted LAST
|
|
372
|
+
sys.stdout.write(f"uninstalled {config_path}\n")
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def effective_token(config_path: str) -> str:
|
|
376
|
+
"""The EFFECTIVE verdict, judged from the config alone — never the state."""
|
|
377
|
+
if os.path.islink(config_path):
|
|
378
|
+
return "symlink"
|
|
379
|
+
text = read_text(config_path, "config")
|
|
380
|
+
if text is None or not text.strip():
|
|
381
|
+
return "file-absent"
|
|
382
|
+
try:
|
|
383
|
+
parsed = parse_toml(text, f"config {config_path}")
|
|
384
|
+
except SystemExit:
|
|
385
|
+
return "invalid-toml"
|
|
386
|
+
shape, value = classify(parsed)
|
|
387
|
+
if shape in ("absent", "no-key"):
|
|
388
|
+
return "status-line-absent"
|
|
389
|
+
if shape.startswith("refuse:"):
|
|
390
|
+
return "malformed-value"
|
|
391
|
+
return "thread-title-present" if REQUIRED_ITEM in value else "thread-title-absent"
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def doctor_static(config_path: str, state_path: str) -> None:
|
|
395
|
+
"""One line, two axes: `<effective> <ownership>[ drift]`. The effective
|
|
396
|
+
token is computed without ever reading the receipt; ownership annotates and
|
|
397
|
+
never rescues. GREEN (exit 0) is exactly `thread-title-present`, owned or
|
|
398
|
+
unowned — an unowned file that already carries the item is green, and every
|
|
399
|
+
other effective token is RED because this doctor's subject is the REQUIRED
|
|
400
|
+
visible-identity axis, not our bookkeeping. Ownership still distinguishes
|
|
401
|
+
the two reds an operator repairs differently: `drift` means our receipt
|
|
402
|
+
binds this file and our item is gone (repair in place), while a bare
|
|
403
|
+
`<effective> unowned` means the axis was never established here."""
|
|
404
|
+
effective = effective_token(config_path)
|
|
405
|
+
state = load_state(state_path)
|
|
406
|
+
owned = (
|
|
407
|
+
state is not None
|
|
408
|
+
and state.get("schemaVersion") == STATE_SCHEMA_VERSION
|
|
409
|
+
and state.get("atom") == ATOM
|
|
410
|
+
and state.get("managedConfigPath") == os.path.abspath(config_path)
|
|
411
|
+
)
|
|
412
|
+
ownership = "owned" if owned else "unowned"
|
|
413
|
+
if effective != "thread-title-present":
|
|
414
|
+
sys.stdout.write(f"{effective} {ownership}{' drift' if owned else ''}\n")
|
|
415
|
+
raise SystemExit(1)
|
|
416
|
+
sys.stdout.write(f"{effective} {ownership}\n")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def main(argv: list[str]) -> None:
|
|
420
|
+
if len(argv) < 2:
|
|
421
|
+
die(5, "usage: codex-statusline-config.py <install config state|uninstall state|doctor-static config state>")
|
|
422
|
+
verb = argv[1]
|
|
423
|
+
if verb == "install" and len(argv) == 4:
|
|
424
|
+
install(argv[2], argv[3])
|
|
425
|
+
elif verb == "uninstall" and len(argv) == 3:
|
|
426
|
+
uninstall(argv[2])
|
|
427
|
+
elif verb == "doctor-static" and len(argv) == 4:
|
|
428
|
+
doctor_static(argv[2], argv[3])
|
|
429
|
+
else:
|
|
430
|
+
die(5, "usage: codex-statusline-config.py <install config state|uninstall state|doctor-static config state>")
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
if __name__ == "__main__":
|
|
434
|
+
main(sys.argv)
|