@junghanacs/entwurf 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/AGENTS.md +3 -2
  2. package/BASELINE.md +3 -1
  3. package/CHANGELOG.md +658 -0
  4. package/DELIVERY.md +167 -29
  5. package/README.md +104 -14
  6. package/VERIFY.md +73 -11
  7. package/docs/external-mcp-host.md +16 -6
  8. package/docs/setup-clean-host.md +73 -22
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +54 -31
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +289 -57
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +171 -263
  26. package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
  27. package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
  28. package/mcp/entwurf-bridge/src/index.ts +65 -31
  29. package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
  30. package/package.json +4 -4
  31. package/pi-extensions/entwurf-control.ts +91 -52
  32. package/pi-extensions/lib/codex-caller-seat.ts +204 -0
  33. package/pi-extensions/lib/codex-declaration.js +612 -0
  34. package/pi-extensions/lib/codex-fresh-preflight.ts +325 -52
  35. package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
  36. package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
  37. package/pi-extensions/lib/entwurf-facts.ts +14 -1
  38. package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
  39. package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
  40. package/pi-extensions/lib/entwurf-self-address.ts +33 -0
  41. package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
  42. package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
  43. package/pi-extensions/lib/fresh-call-composition.ts +493 -0
  44. package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
  45. package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
  46. package/pi-extensions/lib/herdr-placement.ts +276 -0
  47. package/pi-extensions/lib/mux-fresh-call.ts +257 -304
  48. package/run.sh +522 -10
  49. package/scripts/check-codex-app-server-launch.ts +445 -0
  50. package/scripts/check-codex-birth-hook.ts +187 -0
  51. package/scripts/check-entwurf-control-rpc.ts +138 -0
  52. package/scripts/check-entwurf-fact-provider.ts +4 -2
  53. package/scripts/check-entwurf-facts.ts +12 -6
  54. package/scripts/check-entwurf-peers-surface.ts +3 -1
  55. package/scripts/check-entwurf-self-address.ts +68 -0
  56. package/scripts/check-entwurf-v2-production.ts +42 -1
  57. package/scripts/check-entwurf-v2-send.ts +26 -7
  58. package/scripts/check-fresh-call-dispatch.ts +301 -0
  59. package/scripts/check-gate-qualification.ts +19 -5
  60. package/scripts/check-herdr-activation.ts +1293 -0
  61. package/scripts/check-herdr-fresh-call.ts +1250 -0
  62. package/scripts/check-herdr-placement.ts +383 -0
  63. package/scripts/check-herdr-plugin-build.ts +842 -0
  64. package/scripts/check-herdr-plugin-profile.ts +371 -0
  65. package/scripts/check-herdr-plugin.ts +523 -0
  66. package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
  67. package/scripts/check-herdr-sandbox.ts +456 -0
  68. package/scripts/check-herdr-supply.ts +184 -0
  69. package/scripts/check-mux-launch-tmux.ts +331 -35
  70. package/scripts/check-mux-launcher-fence.ts +138 -0
  71. package/scripts/check-peer-facts.ts +331 -0
  72. package/scripts/check-release-gate-outcomes.ts +12 -0
  73. package/scripts/check-typing-call-fence.ts +296 -0
  74. package/scripts/codex-app-server-launch.sh +275 -0
  75. package/scripts/codex-birth-doctor.sh +159 -16
  76. package/scripts/codex-birth-install.sh +178 -72
  77. package/scripts/codex-birth-uninstall.sh +162 -7
  78. package/scripts/codex-socket-path.ts +33 -0
  79. package/scripts/codex-terminal-title-config.py +500 -0
  80. package/scripts/codex_toml_io.py +121 -0
  81. package/scripts/fixtures/herdr-supply.json +29 -0
  82. package/scripts/herdr-activation.mjs +536 -0
  83. package/scripts/herdr-plugin-activate.mjs +270 -0
  84. package/scripts/herdr-plugin-deactivate.mjs +193 -0
  85. package/scripts/herdr-runtime.mjs +1203 -0
  86. package/scripts/install-herdr-ci.sh +96 -0
  87. package/scripts/lib/codex-fresh-live-protocol.ts +13 -5
  88. package/scripts/lib/codex-fresh-source-receipts.ts +29 -2
  89. package/scripts/lib/tree-digest.ts +97 -0
  90. package/scripts/meta-bridge-install.sh +19 -2
  91. package/scripts/meta-bridge-state.py +167 -11
  92. package/scripts/mutants/codex-app-server-launch.json +157 -0
  93. package/scripts/mutants/codex-caller-seat.json +334 -0
  94. package/scripts/mutants/codex-native.json +195 -29
  95. package/scripts/mutants/control-socket-disconnect.json +57 -0
  96. package/scripts/mutants/fresh-call-dispatch.json +154 -0
  97. package/scripts/mutants/herdr-activation.json +305 -0
  98. package/scripts/mutants/herdr-fresh-call.json +448 -0
  99. package/scripts/mutants/herdr-placement.json +161 -0
  100. package/scripts/mutants/herdr-plugin-build.json +160 -0
  101. package/scripts/mutants/herdr-plugin-profile.json +202 -0
  102. package/scripts/mutants/herdr-plugin.json +133 -0
  103. package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
  104. package/scripts/mutants/herdr-supply.json +133 -0
  105. package/scripts/mutants/meta-hook-session-switch.json +2 -2
  106. package/scripts/mutants/mux-fresh-call.json +184 -25
  107. package/scripts/mutants/mux-launcher-fence.json +13 -0
  108. package/scripts/mutants/omp-fresh.json +4 -4
  109. package/scripts/mutants/peer-facts.json +98 -0
  110. package/scripts/mutants/self-address.json +33 -0
  111. package/scripts/mutants/typing-call-fence.json +17 -0
  112. package/scripts/mutants/v2-surface.json +22 -0
  113. package/scripts/peer-facts.ts +120 -0
  114. package/scripts/register-pi-package.py +8 -0
  115. package/scripts/register-pi-provider.py +88 -12
  116. package/scripts/smoke-codex-birth.sh +281 -12
  117. package/scripts/smoke-codex-config-state.sh +192 -3
  118. package/scripts/smoke-codex-fresh-live.ts +277 -37
  119. package/scripts/smoke-entwurf-chain-live.ts +50 -0
  120. package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
  121. package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
  122. package/scripts/smoke-setup-verdict.sh +13 -11
@@ -0,0 +1,204 @@
1
+ /**
2
+ * codex-caller-seat — the ONE resolution of a Codex caller's `threadId` into the tmux pane its
3
+ * TUI is sitting in. Narrow leaf of the fresh-call composition (#95 lane B); it owns the title
4
+ * anchor and the pane count and NOTHING else — it never runs tmux (the runner is injected),
5
+ * never phrases a placement decision, never reads a record, and has no fallback seat.
6
+ *
7
+ * Same shape and same discipline as `resolve-tmux-session.ts` and `classify-tmux-cwd.ts`: this
8
+ * file imports nothing at all, not even a node builtin, so it stays deletable on its own and
9
+ * cannot acquire an opinion about mux, entwurf, identity or delivery. The injected runner is
10
+ * matched STRUCTURALLY to `mux-placement.TmuxRun` rather than by a type import, for the same
11
+ * reason.
12
+ *
13
+ * ── WHAT THIS IS NOT ──
14
+ *
15
+ * A pane title is OPERATOR-WRITABLE and forgeable: any process in any pane can emit the same
16
+ * OSC 0 string. So this leaf's answer is a PLACEMENT INPUT and nothing else — the `$session`
17
+ * it returns may only reach a `-t` target. Identity, delivery and liveness keep the record +
18
+ * `_meta` join they already have (AGENTS.md Hard Rule 16). Nothing here reads screen text,
19
+ * sends keys, or infers that a citizen is alive.
20
+ *
21
+ * ── THE ANCHOR, AND WHY IT IS NOT THE BARE UUID ──
22
+ *
23
+ * With `thread-id` in `[tui].terminal_title` (the `entwurf install-codex-terminal-title` atom)
24
+ * the vendor renders the thread UUID into the terminal title — but TRUNCATED. `[측정]
25
+ * 2026-09-16, thinkpad, codex-cli 0.153.4: a live TUI's `#{pane_title}` read back
26
+ * `tmp | 01a0a7f9-ed9c-7aa2-a4dd-b1a39...`, not the 36-char id. Source at `rust-v0.153.4`:
27
+ * `codex-rs/tui/src/chatwidget/status_surfaces.rs:892-894` renders `TerminalTitleItem::SessionId`
28
+ * through `truncate_terminal_title_part(value, 32)`, and `:1027-1043` keeps 29 graphemes and
29
+ * appends `...`. So the anchor set is TWO strings: the truncated form the vendor emits today,
30
+ * and the full id — accepted so that a vendor which later stops truncating passes unchanged
31
+ * rather than silently resolving nothing.
32
+ *
33
+ * `[측정]` 29 leading chars of a thread UUID are ASCII (`01a09ec6-e6de-7643-8571-f3261`, 29
34
+ * code points, 29 bytes — terra#1 2026-09-16 item 8), so counting code points here and
35
+ * graphemes there is the same count for every id this rail will ever see.
36
+ *
37
+ * `[측정]` `rust-v0.154.0` is unchanged on all three axes — the `thread-id` item
38
+ * (`title_setup.rs:81-83`), the 32-char truncation (`status_surfaces.rs:930-934`) and the
39
+ * ` | ` separator (`title_setup.rs:187-195`) — so this anchor is not pinned to one release.
40
+ *
41
+ * ── WHY TOKENS AND NOT ` | ` SEGMENTS ──
42
+ *
43
+ * The obvious rule is "one ` | `-separated segment equals the anchor". It has a vendor hole.
44
+ * `title_setup.rs:183-193 separator_from_previous` joins adjacent items with ` | ` EXCEPT when
45
+ * either side is the `activity` item (`Spinner`, `title_setup.rs:47-48`), which gets a plain
46
+ * space — and `status_surfaces.rs:330-343` computes that `previous` from the last RENDERED
47
+ * item, skipping any that resolved to `None`. So on a host whose operator list ends in
48
+ * `activity`, the installed atom appends `thread-id` right after it and a WORKING TUI renders
49
+ * `<spinner text> 01a0a7f9-…`: one segment, two values. Splitting each segment on a space and
50
+ * comparing TOKENS closes that hole and cannot open a new one — a false positive would need
51
+ * some other title item to render a string byte-identical to this thread's own id.
52
+ *
53
+ * ── WHY THE COUNT IS OVER PANES, NOT TOKENS ──
54
+ *
55
+ * One pane can legitimately show the same thread twice: an operator whose `terminal_title` also
56
+ * carries `thread-title` sees the full 36-char id there whenever the thread is unnamed
57
+ * (`status_surfaces.rs:776-786`), beside our truncated one. Two tokens, one pane, one thread —
58
+ * not an ambiguity. Ambiguity is TWO PANES claiming the same thread, and that is refused
59
+ * because picking either would seat a sibling by guess.
60
+ */
61
+
62
+ /** Why a Codex caller's seat could not be resolved. Two stable literals — the consuming
63
+ * composition widens its own reject union with this type, so the strings are contract. */
64
+ export type CodexCallerSeatRejectReason = "codex-caller-seat-unresolved" | "codex-caller-seat-ambiguous";
65
+
66
+ /** The repair text for each refusal, owned by the leaf that decides it so the sentence an
67
+ * operator reads cannot drift away from the predicate that produced it (the same rule the
68
+ * Copilot/OMP/Codex preflight hints follow). */
69
+ export const CODEX_CALLER_SEAT_HINT: Record<CodexCallerSeatRejectReason, string> = {
70
+ "codex-caller-seat-unresolved":
71
+ "no pane on this agent's own tmux server shows this Codex thread in its title, so there is no caller seat to open a sibling beside — the TUI may be on another tmux server or outside tmux entirely, its config may not carry `thread-id` in [tui].terminal_title (run `entwurf install-codex-terminal-title`, then `entwurf doctor-codex-terminal-title`), or that server may have `allow-set-title off`, which replaces every pane title with the hostname and hides the id [측정 2026-09-16]",
72
+ "codex-caller-seat-ambiguous":
73
+ "more than one pane on this agent's own tmux server shows this Codex thread in its title, so which one is the caller cannot be decided — nothing is opened rather than guessing a seat; close the stale duplicate and call again",
74
+ };
75
+
76
+ /** What the injected runner returns. Structurally identical to `mux-placement.TmuxRun`; kept as
77
+ * its own declaration so this leaf imports nothing. */
78
+ export interface CodexCallerSeatRun {
79
+ status: number | null;
80
+ stdout: string;
81
+ stderr: string;
82
+ }
83
+
84
+ /** The pane a Codex caller is sitting in. `sessionId` is the only member a placement may use;
85
+ * `paneId` travels for receipts and diagnosis, never as an address. */
86
+ export interface CodexCallerSeat {
87
+ paneId: string;
88
+ sessionId: string;
89
+ source: "codex-title-anchor";
90
+ }
91
+
92
+ export type CodexCallerSeatResult =
93
+ | { ok: true; seat: CodexCallerSeat }
94
+ | { ok: false; reason: CodexCallerSeatRejectReason };
95
+
96
+ /** The vendor's per-item ceiling for `thread-id` (`status_surfaces.rs:892-894`). */
97
+ export const CODEX_TITLE_ITEM_MAX_CHARS = 32;
98
+
99
+ /** The two separators a rendered title can put between items: ` | ` for an ordinary pair, and a
100
+ * bare space when either neighbour is the `activity` indicator (`title_setup.rs:183-193`). */
101
+ const TITLE_SEGMENT_SEPARATOR = " | ";
102
+ const TITLE_TOKEN_SEPARATOR = " ";
103
+
104
+ /**
105
+ * The vendor's `truncate_terminal_title_part`, reproduced (`status_surfaces.rs:1027-1043`).
106
+ * Code points stand in for graphemes — measured identical for every thread id (see header).
107
+ */
108
+ export function truncateTerminalTitlePart(value: string, maxChars: number): string {
109
+ const chars = [...value];
110
+ if (chars.length <= maxChars || maxChars <= 3) return chars.slice(0, maxChars).join("");
111
+ return `${chars.slice(0, maxChars - 3).join("")}...`;
112
+ }
113
+
114
+ /**
115
+ * Every string a title may legitimately carry for this thread. The truncated form is what the
116
+ * vendor emits today; the full id is accepted so a future vendor that stops truncating keeps
117
+ * working without a code change here.
118
+ */
119
+ export function codexTitleAnchors(threadId: string): string[] {
120
+ if (threadId.length === 0) {
121
+ throw new Error("codex-caller-seat: refusing to build an anchor for an empty threadId");
122
+ }
123
+ const truncated = truncateTerminalTitlePart(threadId, CODEX_TITLE_ITEM_MAX_CHARS);
124
+ return truncated === threadId ? [threadId] : [threadId, truncated];
125
+ }
126
+
127
+ /** Does this ONE pane title name the thread? Segment first, then token — see the header for the
128
+ * `activity`-adjacency hole a segment-only rule leaves open. */
129
+ export function titleNamesThread(title: string, anchors: readonly string[]): boolean {
130
+ for (const segment of title.split(TITLE_SEGMENT_SEPARATOR)) {
131
+ for (const token of segment.split(TITLE_TOKEN_SEPARATOR)) {
132
+ if (anchors.includes(token)) return true;
133
+ }
134
+ }
135
+ return false;
136
+ }
137
+
138
+ /**
139
+ * The lookup argv. `-a` is the whole server because a Codex TUI is not required to be in the
140
+ * caller's own session — that asymmetry is exactly what this leaf exists to remove. The three
141
+ * fields are tab-separated so a title containing spaces (a project name, the spinner text)
142
+ * cannot be read as a new column.
143
+ */
144
+ export function buildCodexCallerSeatArgs(): string[] {
145
+ return ["list-panes", "-a", "-F", "#{pane_id}\t#{session_id}\t#{pane_title}"];
146
+ }
147
+
148
+ /** One `list-panes` line back into its three fields, or `null` when it is not one. The title
149
+ * takes EVERYTHING after the second tab: splitting on every tab would truncate a title that
150
+ * ever carried one. */
151
+ function parsePaneLine(line: string): { paneId: string; sessionId: string; title: string } | null {
152
+ const firstTab = line.indexOf("\t");
153
+ if (firstTab < 0) return null;
154
+ const secondTab = line.indexOf("\t", firstTab + 1);
155
+ if (secondTab < 0) return null;
156
+ const paneId = line.slice(0, firstTab);
157
+ const sessionId = line.slice(firstTab + 1, secondTab);
158
+ if (paneId.length === 0 || sessionId.length === 0) return null;
159
+ return { paneId, sessionId, title: line.slice(secondTab + 1) };
160
+ }
161
+
162
+ /**
163
+ * Resolve the pane a Codex caller's thread is displayed in, on whatever server the runner's
164
+ * environment names.
165
+ *
166
+ * ONE BOUNDED IMPRECISION, STATED RATHER THAN LAUNDERED (the same one
167
+ * `resolve-tmux-session.ts` carries): rc≠0 also covers "no server running on this socket". This
168
+ * leaf reads every rc≠0 as `codex-caller-seat-unresolved`, so a server that died between the
169
+ * caller's context proof and this lookup is reported under the narrower word. That is safe —
170
+ * both readings are refusals that mutate nothing, and the hint above names both — and it is
171
+ * preferred over matching tmux's own stderr text, which would pin this leaf to one vendor
172
+ * version's wording.
173
+ */
174
+ export function resolveCodexCallerSeat(
175
+ threadId: string,
176
+ run: (args: string[]) => CodexCallerSeatRun,
177
+ ): CodexCallerSeatResult {
178
+ const anchors = codexTitleAnchors(threadId);
179
+ const result = run(buildCodexCallerSeatArgs());
180
+ // A signalled call is not tmux answering — it carries no information about any pane at all,
181
+ // so it must never be read as "the caller's TUI is not here".
182
+ if (result.status === null) {
183
+ throw new Error(`codex-caller-seat: the pane listing was killed by a signal: ${result.stderr.trim()}`);
184
+ }
185
+ if (result.status !== 0) return { ok: false, reason: "codex-caller-seat-unresolved" };
186
+
187
+ const matches: CodexCallerSeat[] = [];
188
+ for (const line of result.stdout.split("\n")) {
189
+ if (line.length === 0) continue;
190
+ const pane = parsePaneLine(line);
191
+ // A line this leaf cannot read is NOT a pane it may skip quietly: the listing is the
192
+ // whole evidence base for "exactly one", and a dropped line could be the second match
193
+ // that should have refused.
194
+ if (pane === null) {
195
+ throw new Error(`codex-caller-seat: tmux printed a pane line this leaf cannot read: ${JSON.stringify(line)}`);
196
+ }
197
+ if (titleNamesThread(pane.title, anchors)) {
198
+ matches.push({ paneId: pane.paneId, sessionId: pane.sessionId, source: "codex-title-anchor" });
199
+ }
200
+ }
201
+ if (matches.length === 0) return { ok: false, reason: "codex-caller-seat-unresolved" };
202
+ if (matches.length > 1) return { ok: false, reason: "codex-caller-seat-ambiguous" };
203
+ return { ok: true, seat: matches[0] };
204
+ }