@junghanacs/entwurf 0.22.0 → 0.23.1
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 +4 -1
- package/BASELINE.md +1 -0
- package/CHANGELOG.md +514 -0
- package/DELIVERY.md +10 -2
- package/README.md +43 -4
- package/VERIFY.md +47 -0
- package/docs/acp-backend-rail.md +96 -17
- package/docs/setup-clean-host.md +11 -1
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +39 -40
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +95 -56
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +46 -273
- package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
- package/mcp/entwurf-bridge/src/index.ts +41 -41
- package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
- package/package.json +9 -8
- package/pi-extensions/entwurf-control.ts +91 -52
- package/pi-extensions/lib/acp/acp-client.ts +3 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
- package/pi-extensions/lib/acp/backend.ts +4 -4
- package/pi-extensions/lib/acp/event-mapper.ts +5 -5
- package/pi-extensions/lib/acp/tool-surface.ts +2 -1
- package/pi-extensions/lib/codex-declaration.js +612 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +107 -51
- package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
- package/pi-extensions/lib/entwurf-facts.ts +14 -1
- package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
- package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
- package/pi-extensions/lib/entwurf-self-address.ts +33 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
- package/pi-extensions/lib/fresh-call-composition.ts +493 -0
- package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
- package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
- package/pi-extensions/lib/herdr-placement.ts +276 -0
- package/pi-extensions/lib/mux-fresh-call.ts +95 -301
- package/run.sh +588 -204
- package/scripts/check-acp-usage-accounting.ts +9 -9
- package/scripts/check-codex-birth-hook.ts +187 -0
- package/scripts/check-entwurf-control-rpc.ts +138 -0
- package/scripts/check-entwurf-fact-provider.ts +4 -2
- package/scripts/check-entwurf-peers-surface.ts +3 -1
- package/scripts/check-entwurf-self-address.ts +68 -0
- package/scripts/check-fresh-call-dispatch.ts +301 -0
- package/scripts/check-gate-qualification.ts +17 -4
- package/scripts/check-herdr-activation.ts +1293 -0
- package/scripts/check-herdr-fresh-call.ts +1250 -0
- package/scripts/check-herdr-placement.ts +383 -0
- package/scripts/check-herdr-plugin-build.ts +842 -0
- package/scripts/check-herdr-plugin-profile.ts +371 -0
- package/scripts/check-herdr-plugin.ts +523 -0
- package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
- package/scripts/check-herdr-sandbox.ts +456 -0
- package/scripts/check-herdr-supply.ts +184 -0
- package/scripts/check-mux-launcher-fence.ts +138 -0
- package/scripts/check-peer-facts.ts +331 -0
- package/scripts/check-pi-launch.ts +358 -0
- package/scripts/check-release-gate-outcomes.ts +19 -0
- package/scripts/check-typing-call-fence.ts +296 -0
- package/scripts/codex-birth-doctor.sh +159 -16
- package/scripts/codex-birth-install.sh +178 -72
- package/scripts/codex-birth-uninstall.sh +162 -7
- package/scripts/fixtures/herdr-supply.json +29 -0
- package/scripts/herdr-activation.mjs +536 -0
- package/scripts/herdr-plugin-activate.mjs +270 -0
- package/scripts/herdr-plugin-deactivate.mjs +193 -0
- package/scripts/herdr-runtime.mjs +1203 -0
- package/scripts/install-herdr-ci.sh +96 -0
- package/scripts/inventory-verification-surface.ts +86 -10
- package/scripts/lib/codex-fresh-live-protocol.ts +2 -2
- package/scripts/lib/tree-digest.ts +97 -0
- package/scripts/meta-bridge-install.sh +19 -2
- package/scripts/meta-bridge-state.py +167 -11
- package/scripts/mutants/codex-caller-seat.json +6 -8
- package/scripts/mutants/codex-native.json +195 -29
- package/scripts/mutants/control-socket-disconnect.json +57 -0
- package/scripts/mutants/fresh-call-dispatch.json +154 -0
- package/scripts/mutants/herdr-activation.json +305 -0
- package/scripts/mutants/herdr-fresh-call.json +448 -0
- package/scripts/mutants/herdr-placement.json +161 -0
- package/scripts/mutants/herdr-plugin-build.json +160 -0
- package/scripts/mutants/herdr-plugin-profile.json +202 -0
- package/scripts/mutants/herdr-plugin.json +133 -0
- package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
- package/scripts/mutants/herdr-supply.json +133 -0
- package/scripts/mutants/meta-hook-session-switch.json +2 -2
- package/scripts/mutants/mux-fresh-call.json +104 -17
- package/scripts/mutants/mux-launcher-fence.json +13 -0
- package/scripts/mutants/omp-fresh.json +4 -4
- package/scripts/mutants/peer-facts.json +98 -0
- package/scripts/mutants/pi-launch.json +113 -0
- package/scripts/mutants/resume-args.json +6 -6
- package/scripts/mutants/self-address.json +33 -0
- package/scripts/mutants/typing-call-fence.json +17 -0
- package/scripts/mutants/v2-surface.json +2 -2
- package/scripts/mutants/v2-visible-resume.json +17 -17
- package/scripts/peer-facts.ts +120 -0
- package/scripts/register-pi-package.py +8 -0
- package/scripts/register-pi-provider.py +88 -12
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-codex-birth.sh +281 -12
- package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
- package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
- package/scripts/smoke-herdr-raw-install-live.sh +849 -0
- package/scripts/tsconfig.json +5 -0
- package/scripts/check-entwurf-facts.ts +0 -385
- package/scripts/check-entwurf-resume-args.ts +0 -122
- package/scripts/check-entwurf-v2-decider.ts +0 -657
- package/scripts/check-entwurf-v2-lock.ts +0 -371
- package/scripts/check-entwurf-v2-mailbox.ts +0 -261
- package/scripts/check-entwurf-v2-matrix.ts +0 -401
- package/scripts/check-entwurf-v2-native-push.ts +0 -221
- package/scripts/check-entwurf-v2-release.ts +0 -151
- package/scripts/check-entwurf-v2-runner.ts +0 -448
- package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
- package/scripts/check-entwurf-v2-send.ts +0 -458
- package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
- package/scripts/check-socket-discovery.ts +0 -401
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* herdr-placement — the PURE half of the placement evidence axis (#116 S1).
|
|
3
|
+
*
|
|
4
|
+
* WHAT THIS IS FOR. A citizen opened inside herdr is visible in one of herdr's panes.
|
|
5
|
+
* That pane is an EPHEMERAL VIEW, never an address and never a liveness receipt
|
|
6
|
+
* (AGENTS Hard Rule 16, `docs/mux-launch-rail.md` §7). It earns a column in
|
|
7
|
+
* `entwurf_peers` for one reason: on a herdr host the operator's own question is
|
|
8
|
+
* "which pane is that one in", and answering it by hand means reading two listings
|
|
9
|
+
* side by side.
|
|
10
|
+
*
|
|
11
|
+
* WHY THIS JOIN IS EXACT EVIDENCE AND NOT A GUESS. §7 admitted exactly two forms of
|
|
12
|
+
* exact placement evidence (our own launch receipt, and a peer's verifiable
|
|
13
|
+
* self-report) and named the guesses it refuses: window title, cwd match, and time
|
|
14
|
+
* proximity. This is a THIRD form and it belongs to neither the receipts nor the
|
|
15
|
+
* guesses: herdr reports the native session id its OWN OFFICIAL integration told it,
|
|
16
|
+
* and `nativeSessionId` is unique across the whole meta-store (Hard Rule 7). Two
|
|
17
|
+
* independently-owned facts meet on one unique key. Nothing here narrows candidates
|
|
18
|
+
* by cwd, reads a title, or prefers the nearest timestamp. §7 was amended in the same
|
|
19
|
+
* change that added this module; if the two ever disagree, §7 is the owning document.
|
|
20
|
+
*
|
|
21
|
+
* THE TWO AXES ARE NOT EQUALLY EXACT, AND THE DIFFERENCE IS NAMED. On the claude axis
|
|
22
|
+
* the reported value IS the key, byte for byte. On the pi axis it is a session FILE
|
|
23
|
+
* PATH, and the key is recovered from that filename by a strict conversion measured on
|
|
24
|
+
* pi 0.85.1 — a VENDOR FLOOR, not a key equality. Calling both "a unique-key join"
|
|
25
|
+
* would hide which one can drift when a vendor renames a file. The conversion is
|
|
26
|
+
* deliberately strict at both ends so that drift lands as a missed join, never a wrong
|
|
27
|
+
* one, and a listing that declined to read anything says so (see `declinedReports`).
|
|
28
|
+
*
|
|
29
|
+
* ONE READ, NO WATCHER. The observer performs exactly ONE `pane list` read for a
|
|
30
|
+
* whole listing and never retries. herdr publishes no event that says "this pane's
|
|
31
|
+
* `agent_session` is now settled" — measured on herdr 0.9.0: `pane.updated` is the
|
|
32
|
+
* only event whose payload carries `agent_session` and plugin `[[events]]` refuses
|
|
33
|
+
* that name, `events.subscribe` offers three pane kinds and none of them carry it,
|
|
34
|
+
* and `pane.agent_detected` arrives BEFORE the session reference exists. A retry loop
|
|
35
|
+
* around that gap is a discovery watcher, which §7 forbids by name. So a citizen
|
|
36
|
+
* whose pane has not reported yet reads `none` on this pass and is simply asked again
|
|
37
|
+
* next time.
|
|
38
|
+
*
|
|
39
|
+
* FACTS, NOT VERBS. Nothing here returns a transport, a target, or a suggestion. A
|
|
40
|
+
* pane id must never reach dispatch: `entwurf_v2` resolves its rail from the record
|
|
41
|
+
* and a live probe, and a placement that is one server restart from being false has
|
|
42
|
+
* no business in that decision.
|
|
43
|
+
*/
|
|
44
|
+
export const UNOBSERVED_PLACEMENT = { kind: "unobserved" };
|
|
45
|
+
export const NO_PLACEMENT = { kind: "none" };
|
|
46
|
+
export const AMBIGUOUS_PLACEMENT = { kind: "ambiguous" };
|
|
47
|
+
/**
|
|
48
|
+
* The OFFICIAL integration reports this join accepts, and nothing else.
|
|
49
|
+
*
|
|
50
|
+
* WHY THIS TABLE EXISTS. herdr accepts a session reference from any integration,
|
|
51
|
+
* including one a user wrote (`integrations.mdx:65-90 @ c77af189`), and the row
|
|
52
|
+
* carries who reported it in `agent_session.agent` / `.source`. Reading only
|
|
53
|
+
* `kind`/`value` would have let a third party's report stand in for the placement
|
|
54
|
+
* owner's own — and "the placement owner reported it" is the entire reason
|
|
55
|
+
* `docs/mux-launch-rail.md` §7 admits this as exact evidence. So the triple is
|
|
56
|
+
* pinned: source, agent, AND the shape that source is measured to emit. A row that
|
|
57
|
+
* misses any leg carries no key.
|
|
58
|
+
*
|
|
59
|
+
* `backend` is the meta-record backend this report may speak for. It is checked at
|
|
60
|
+
* resolve time so a claude-reported pane can never be handed to a pi citizen whose
|
|
61
|
+
* native id happens to collide.
|
|
62
|
+
*/
|
|
63
|
+
const OFFICIAL_REPORTS = {
|
|
64
|
+
"herdr:claude": { agent: "claude", kind: "id", backend: "claude-code" },
|
|
65
|
+
"herdr:pi": { agent: "pi", kind: "path", backend: "pi" },
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* A pi session filename, measured 2026-09-14 on pi 0.85.1:
|
|
69
|
+
*
|
|
70
|
+
* 2026-09-14T05-17-03-979Z_01a09e58-f06a-70e8-b14a-1f0f0c7f7c7d.jsonl
|
|
71
|
+
* └────────── start stamp ─────────┘ └──────── nativeSessionId ────────┘
|
|
72
|
+
*
|
|
73
|
+
* WHY THIS IS PINNED HERE AND NOT INFERRED. The uuid is the join key and it lives in
|
|
74
|
+
* a VENDOR filename, so this rule depends on pi's naming and would break silently if
|
|
75
|
+
* pi changed it. **Vendor floor: pi 0.85.1, measured 2026-09-14** — whether that layout
|
|
76
|
+
* is a vendor contract or a convention is NOT measured, so this is the one place in the
|
|
77
|
+
* axis that can drift under us. It is therefore strict on both ends — the name must end
|
|
78
|
+
* in `.jsonl` and the tail after the last `_` must be a well-formed uuid — and a name
|
|
79
|
+
* that fails either test yields no key at all rather than a substring that happens to
|
|
80
|
+
* look plausible. Drift then lands as a DECLINED report (the citizen reads `unobserved`,
|
|
81
|
+
* not `none`), never as a wrong join onto someone else's pane.
|
|
82
|
+
*/
|
|
83
|
+
const PI_SESSION_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
84
|
+
const PI_SESSION_SUFFIX = ".jsonl";
|
|
85
|
+
export function piNativeSessionIdFromPath(value) {
|
|
86
|
+
if (!value.endsWith(PI_SESSION_SUFFIX))
|
|
87
|
+
return null;
|
|
88
|
+
const stem = value.slice(0, -PI_SESSION_SUFFIX.length);
|
|
89
|
+
const base = stem.slice(stem.lastIndexOf("/") + 1);
|
|
90
|
+
const cut = base.lastIndexOf("_");
|
|
91
|
+
if (cut < 0)
|
|
92
|
+
return null;
|
|
93
|
+
const candidate = base.slice(cut + 1);
|
|
94
|
+
return PI_SESSION_UUID.test(candidate) ? candidate : null;
|
|
95
|
+
}
|
|
96
|
+
export function joinKeyOf(row) {
|
|
97
|
+
if (row.sessionSource === null)
|
|
98
|
+
return null;
|
|
99
|
+
const official = OFFICIAL_REPORTS[row.sessionSource];
|
|
100
|
+
// An unmeasured source — a herdr version we have not run, or a third-party
|
|
101
|
+
// integration. Declined on purpose: coercing it would invent a rule, and the
|
|
102
|
+
// whole exact-evidence argument rests on WHO reported.
|
|
103
|
+
if (official === undefined)
|
|
104
|
+
return null;
|
|
105
|
+
if (row.agent !== official.agent)
|
|
106
|
+
return null;
|
|
107
|
+
if (row.sessionKind !== official.kind)
|
|
108
|
+
return null;
|
|
109
|
+
if (row.sessionValue === null || row.sessionValue.length === 0)
|
|
110
|
+
return null;
|
|
111
|
+
const nativeSessionId = official.kind === "id" ? row.sessionValue : piNativeSessionIdFromPath(row.sessionValue);
|
|
112
|
+
return nativeSessionId === null ? null : { nativeSessionId, backend: official.backend };
|
|
113
|
+
}
|
|
114
|
+
export function buildPlacementIndex(rows) {
|
|
115
|
+
const byNativeSessionId = new Map();
|
|
116
|
+
const ambiguous = new Set();
|
|
117
|
+
let declinedReports = 0;
|
|
118
|
+
for (const row of rows) {
|
|
119
|
+
const entry = joinKeyOf(row);
|
|
120
|
+
if (entry === null) {
|
|
121
|
+
// Only a row that actually CARRIED a report counts as declined.
|
|
122
|
+
if (row.sessionSource !== null || row.sessionValue !== null)
|
|
123
|
+
declinedReports++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const key = entry.nativeSessionId;
|
|
127
|
+
if (ambiguous.has(key))
|
|
128
|
+
continue;
|
|
129
|
+
const seen = byNativeSessionId.get(key);
|
|
130
|
+
if (seen !== undefined && seen.paneId !== row.paneId) {
|
|
131
|
+
byNativeSessionId.delete(key);
|
|
132
|
+
ambiguous.add(key);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
byNativeSessionId.set(key, { ...entry, paneId: row.paneId });
|
|
136
|
+
}
|
|
137
|
+
return { byNativeSessionId, ambiguous, declinedReports };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Parse what `herdr pane list` printed. Returns `null` — "nobody looked" — for any
|
|
141
|
+
* payload this does not recognise, because a herdr that answered in a shape we cannot
|
|
142
|
+
* read has told us nothing, and reporting `none` for every citizen on that basis
|
|
143
|
+
* would be a fabricated measurement. A row missing `pane_id` is the one hard error:
|
|
144
|
+
* the whole payload is unreadable rather than partially trusted.
|
|
145
|
+
*/
|
|
146
|
+
export function parseHerdrPaneList(stdout) {
|
|
147
|
+
let raw;
|
|
148
|
+
try {
|
|
149
|
+
raw = JSON.parse(stdout);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
if (typeof raw !== "object" || raw === null)
|
|
155
|
+
return null;
|
|
156
|
+
const result = raw.result;
|
|
157
|
+
if (typeof result !== "object" || result === null)
|
|
158
|
+
return null;
|
|
159
|
+
const panes = result.panes;
|
|
160
|
+
if (!Array.isArray(panes))
|
|
161
|
+
return null;
|
|
162
|
+
const rows = [];
|
|
163
|
+
for (const pane of panes) {
|
|
164
|
+
if (typeof pane !== "object" || pane === null)
|
|
165
|
+
return null;
|
|
166
|
+
const paneId = pane.pane_id;
|
|
167
|
+
if (typeof paneId !== "string" || paneId.length === 0)
|
|
168
|
+
return null;
|
|
169
|
+
const session = pane.agent_session;
|
|
170
|
+
if (typeof session !== "object" || session === null) {
|
|
171
|
+
rows.push({ paneId, agent: null, sessionSource: null, sessionKind: null, sessionValue: null });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const agent = session.agent;
|
|
175
|
+
const source = session.source;
|
|
176
|
+
const kind = session.kind;
|
|
177
|
+
const value = session.value;
|
|
178
|
+
rows.push({
|
|
179
|
+
paneId,
|
|
180
|
+
agent: typeof agent === "string" ? agent : null,
|
|
181
|
+
sessionSource: typeof source === "string" ? source : null,
|
|
182
|
+
sessionKind: kind === "id" || kind === "path" ? kind : null,
|
|
183
|
+
sessionValue: typeof value === "string" ? value : null,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return rows;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The whole decision, for one citizen. A `null` index is the no-herdr host and every
|
|
190
|
+
* citizen on it reads `unobserved` — which is why this takes the index rather than a
|
|
191
|
+
* reader: the read happened once, above, and this stays pure.
|
|
192
|
+
*/
|
|
193
|
+
export function resolvePlacement(index, identity) {
|
|
194
|
+
if (index === null)
|
|
195
|
+
return UNOBSERVED_PLACEMENT;
|
|
196
|
+
if (index.ambiguous.has(identity.nativeSessionId))
|
|
197
|
+
return AMBIGUOUS_PLACEMENT;
|
|
198
|
+
const entry = index.byNativeSessionId.get(identity.nativeSessionId);
|
|
199
|
+
if (entry !== undefined) {
|
|
200
|
+
// The reporting integration must speak for THIS citizen's backend. A pi
|
|
201
|
+
// integration reporting an id that collides with a claude-code record is a
|
|
202
|
+
// contradiction, not a placement, and we decline to claim either way.
|
|
203
|
+
return entry.backend === identity.backend ? { kind: "herdr-pane", paneId: entry.paneId } : UNOBSERVED_PLACEMENT;
|
|
204
|
+
}
|
|
205
|
+
// No join. `none` is only honest when the read was COMPLETE — if any report was
|
|
206
|
+
// declined, the thing we did not read might have been this one.
|
|
207
|
+
return index.declinedReports > 0 ? UNOBSERVED_PLACEMENT : NO_PLACEMENT;
|
|
208
|
+
}
|
|
209
|
+
/** Render one observation as the listing's column value. `herdr <pane>` says WHERE it
|
|
210
|
+
* was seen and by WHOM, so a reader never mistakes the pane id for one of ours. */
|
|
211
|
+
export function renderPlacement(placement) {
|
|
212
|
+
return placement.kind === "herdr-pane" ? `herdr ${placement.paneId}` : placement.kind;
|
|
213
|
+
}
|
|
@@ -85,22 +85,46 @@
|
|
|
85
85
|
* the session the window is actually in. It still reports no `pane_current_path`, and
|
|
86
86
|
* there is no "session created" field because nothing here creates one.
|
|
87
87
|
*/
|
|
88
|
-
import { randomBytes } from "node:crypto";
|
|
89
88
|
import { classifyTmuxCwd } from "./classify-tmux-cwd.js";
|
|
90
89
|
import { CODEX_CALLER_SEAT_HINT, resolveCodexCallerSeat, } from "./codex-caller-seat.js";
|
|
91
90
|
import { CODEX_CALLER_PREFLIGHT_HINT, CODEX_LAUNCH_CWD_PREFLIGHT_HINT, CODEX_PREFLIGHT_HINT, codexLaunchCwdFreshPreflight, } from "./codex-fresh-preflight.js";
|
|
92
91
|
import { COPILOT_PREFLIGHT_HINT, copilotFreshPreflight, } from "./copilot-fresh-preflight.js";
|
|
92
|
+
import { buildOmpBootstrapPayload, composeBackendArgs, composeFreshCallPrompt, FRESH_CALL_BACKENDS, FRESH_CALL_CALLBACK_TOOL, isSafeFreshCallModel, MODEL_MAX_CHARS, mintNonce, normalizeFreshCallInputs, OMP_BOOTSTRAP_FLAG, OMP_BOOTSTRAP_VERSION, TASK_MAX_CHARS, } from "./fresh-call-composition.js";
|
|
93
93
|
import { assertLaunchTarget, LaunchPreconditionError, resolveRuntimeOnPath, } from "./mux-launch.js";
|
|
94
94
|
import { APPEND_FORMAT, assertSelector, assertTmuxOk, inspectPlacement, parseWindowFields, requireSameContext, runTmux, } from "./mux-placement.js";
|
|
95
95
|
import { resolveCodexDefaultSocketPath } from "./native-push/codex-ws-client.js";
|
|
96
96
|
import { OMP_PREFLIGHT_HINT, ompFreshPreflight } from "./omp-fresh-preflight.js";
|
|
97
97
|
import { classifyTmuxSessionName, resolveTmuxSessionId } from "./resolve-tmux-session.js";
|
|
98
|
-
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
|
|
98
|
+
/**
|
|
99
|
+
* THE COMPOSITION MOVED, THE RAIL DID NOT (#116 S2-a). Backend argv, the first-turn framing and
|
|
100
|
+
* the nonce now live in `fresh-call-composition.ts`, which knows nothing about tmux, so the herdr
|
|
101
|
+
* rail can reach them without importing this file and dragging placement behind it. They are
|
|
102
|
+
* re-exported here unchanged: every existing caller, gate and mutant anchor that reads them from
|
|
103
|
+
* this module keeps reading the same names.
|
|
104
|
+
*
|
|
105
|
+
* The two wrappers below are where this rail states what only it can state — the sentence that
|
|
106
|
+
* says a sibling was opened in the operator's tmux session, and the Codex socket path. Both are
|
|
107
|
+
* arguments to the leaf rather than knowledge inside it.
|
|
108
|
+
*/
|
|
109
|
+
export { buildOmpBootstrapPayload, FRESH_CALL_BACKENDS, FRESH_CALL_CALLBACK_TOOL, isSafeFreshCallModel, MODEL_MAX_CHARS, mintNonce, OMP_BOOTSTRAP_FLAG, OMP_BOOTSTRAP_VERSION, TASK_MAX_CHARS, };
|
|
110
|
+
/** What the tmux rail tells a sibling about where it woke up. The resume verb has no equivalent
|
|
111
|
+
* because it composes no first turn at all; a herdr rail will pass its own sentence here. */
|
|
112
|
+
export const TMUX_FRESH_CALL_OPENING_LINE = "You are a fresh visible citizen that entwurf opened in the operator's tmux session.";
|
|
113
|
+
/** Backend argv for THIS rail: the neutral dialect, with the two Codex facts only a rail can
|
|
114
|
+
* state — the socket resolved from the caller's env, and the directory this launch chose.
|
|
115
|
+
*
|
|
116
|
+
* `launchCwd` defaults to THIS process's own directory, which is what tmux gives a window opened
|
|
117
|
+
* with no `-c` (`[측정 ×2]`, module header). The default is that inherited fact rather than a
|
|
118
|
+
* convenience, so a caller that omits it still names the truth to codex. Both facts reach the
|
|
119
|
+
* leaf lazily, because a host with no Codex home must still be able to open pi and claude
|
|
120
|
+
* siblings — see `composeBackendArgs`. */
|
|
121
|
+
export function buildBackendArgs(backend, composition, model, env = process.env, launchCwd = process.cwd()) {
|
|
122
|
+
return composeBackendArgs(backend, composition, model, () => resolveCodexDefaultSocketPath(env), () => launchCwd);
|
|
123
|
+
}
|
|
124
|
+
/** The first turn for THIS rail: the neutral framing under this rail's placement sentence. */
|
|
125
|
+
export function buildFreshCallPrompt(params) {
|
|
126
|
+
return composeFreshCallPrompt({ ...params, openingLine: TMUX_FRESH_CALL_OPENING_LINE });
|
|
127
|
+
}
|
|
104
128
|
/**
|
|
105
129
|
* The fixed runtime each backend resolves on PATH. Same reason `mux-launch` uses PATH rather
|
|
106
130
|
* than a compiled-in location: the official binary is whatever the operator's environment gives
|
|
@@ -130,240 +154,6 @@ export const FRESH_CALL_RUNTIME = {
|
|
|
130
154
|
omp: "omp",
|
|
131
155
|
codex: "codex",
|
|
132
156
|
};
|
|
133
|
-
/**
|
|
134
|
-
* The callback tool NAME differs per backend and that is not cosmetic: native pi exposes the
|
|
135
|
-
* capability directly (`entwurf_v2`), while an MCP-hosted session reaches it under whatever
|
|
136
|
-
* name that harness composes. Naming the wrong one costs the whole first turn.
|
|
137
|
-
*
|
|
138
|
-
* `[측정]` Copilot CLI 1.0.80 composes `<mcpServerName>-<mcpToolName>` — NOT Claude Code's
|
|
139
|
-
* `mcp__<server>__<tool>`. Read from two independent sessions' own event logs
|
|
140
|
-
* (`~/.copilot/session-state/<id>/events.jsonl`): `assistant.message.toolRequests[].name` and
|
|
141
|
-
* `tool.execution_start.toolName` both carry `entwurf-bridge-entwurf_v2`, with
|
|
142
|
-
* `mcpServerName`/`mcpToolName` beside them as the parts. Derive-and-measure, never copy a
|
|
143
|
-
* sibling's spelling (`docs/adding-a-harness.md` step 5).
|
|
144
|
-
*
|
|
145
|
-
* `[측정]` omp 18.0.0 is the sharpest case for that rule: it mints
|
|
146
|
-
* `mcp__${sanitizedServerName}_${normalizedToolName}` with a sanitizer whose charset is
|
|
147
|
-
* `[a-z_]` (`mcp/tool-bridge.ts:351-357`, `:396`), so the DIGIT IN `entwurf_v2` IS EATEN and the
|
|
148
|
-
* hyphen in the server key becomes an underscore — the model-facing name is
|
|
149
|
-
* `mcp__entwurf_bridge_entwurf_v`, not `..._entwurf_v2` and not Claude's double-underscore form.
|
|
150
|
-
* Confirmed against a live tool dump of all seven bridge tools and a real session transcript
|
|
151
|
-
* (`scripts/raw-omp-measure/README.md` "Tool-name dialect"). Unlike Copilot there is no second
|
|
152
|
-
* permission dialect: omp's approval layer consults the same minted string (`source-audit.md`).
|
|
153
|
-
*/
|
|
154
|
-
export const FRESH_CALL_CALLBACK_TOOL = {
|
|
155
|
-
pi: "entwurf_v2",
|
|
156
|
-
"claude-code": "mcp__entwurf-bridge__entwurf_v2",
|
|
157
|
-
copilot: "entwurf-bridge-entwurf_v2",
|
|
158
|
-
omp: "mcp__entwurf_bridge_entwurf_v",
|
|
159
|
-
codex: "mcp__entwurf_bridge__entwurf_v2",
|
|
160
|
-
};
|
|
161
|
-
/** Mirrors the `entwurf_v2` message bound. This is an INTERFACE cap for symmetry with the
|
|
162
|
-
* delivery surface, not a claim that a task of this size was measured through tmux. An argv
|
|
163
|
-
* that the OS refuses is a launch failure and fails loud — it never reads as a delivered task. */
|
|
164
|
-
export const TASK_MAX_CHARS = 16000;
|
|
165
|
-
export const MODEL_MAX_CHARS = 200;
|
|
166
|
-
const MODEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/:[\]-]*$/;
|
|
167
|
-
/** A model is an explicit launch input, not ambient process state. The grammar admits canonical
|
|
168
|
-
* pi provider/model ids, Claude model ids/aliases, and bracketed context variants, while refusing
|
|
169
|
-
* whitespace and tmux control syntax. It is passed without a shell using each runtime's measured
|
|
170
|
-
* CLI dialect: Pi takes `--model`, value; Claude Code takes `--model=value`. */
|
|
171
|
-
export function isSafeFreshCallModel(model) {
|
|
172
|
-
return model.length > 0 && model.length <= MODEL_MAX_CHARS && MODEL_PATTERN.test(model);
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Backend argv AFTER the runtime path. Both orders are MEASURED, and both were measured by
|
|
176
|
-
* getting them wrong first (rail §6-a):
|
|
177
|
-
*
|
|
178
|
-
* pi — prompt BEFORE `--entwurf-control`, then `--model`, value as TWO tokens.
|
|
179
|
-
* Flag-first submitted no message; Pi rejects the equals form for `--model`.
|
|
180
|
-
* claude-code — prompt, then `--allowedTools=` and `--model=` as ONE token each. The space form
|
|
181
|
-
* for allowedTools is variadic and eats the prompt as an option value.
|
|
182
|
-
* copilot — the managed VERB first, then the prompt as the value of `-i/--interactive`,
|
|
183
|
-
* `--model`, value as two tokens, and the policy as the explicit `--yolo`
|
|
184
|
-
* token. Measured from `copilot --help` (1.0.80).
|
|
185
|
-
* omp — NO positional prompt at all. The task rides `--entwurf-bootstrap`, a fixed
|
|
186
|
-
* flag the installed birth extension registers, then `--model`, value as two
|
|
187
|
-
* tokens, then the policy as `--approval-mode`, `yolo`. `-p/--print` remains
|
|
188
|
-
* the flag this argv must never carry — it processes a prompt and EXITS,
|
|
189
|
-
* closing the window on a sibling that has to stay open to be delivered to.
|
|
190
|
-
*
|
|
191
|
-
* Both pi/claude failures looked identical from outside: window open, record and socket minted,
|
|
192
|
-
* no turn.
|
|
193
|
-
*
|
|
194
|
-
* For pi and claude-code the equals form is NOT a permission guarantee — on the measured host
|
|
195
|
-
* the tool was already permitted, so the option's effect was unobservable. What was observed is
|
|
196
|
-
* that it does no harm to the argv. Permission stays a documented host precondition there.
|
|
197
|
-
*
|
|
198
|
-
* Copilot is the backend where the policy IS carried explicitly (step 9 clause 2), and three of
|
|
199
|
-
* its argv facts are load-bearing:
|
|
200
|
-
*
|
|
201
|
-
* - `copilot` is argv[0] of the RUNTIME `entwurf`, i.e. the managed verb — see
|
|
202
|
-
* `FRESH_CALL_RUNTIME`. Everything after it is forwarded byte-identical by
|
|
203
|
-
* `scripts/copilot-launch.sh`.
|
|
204
|
-
* - the prompt rides `--interactive`, never `-p/--prompt`: `-p` runs the prompt and EXITS,
|
|
205
|
-
* which would close the window on a sibling that is supposed to stay open and be delivered
|
|
206
|
-
* to. `--interactive <prompt>` is non-variadic, so the space form is safe here.
|
|
207
|
-
* - the policy token is `--yolo`, STATED HERE rather than left to the launcher: the launcher
|
|
208
|
-
* injects `--yolo` only when the argv names no policy, and step 9 clause 2 requires the
|
|
209
|
-
* fresh composition to state its model and permission policy explicitly, never to rely
|
|
210
|
-
* invisibly on someone else's default.
|
|
211
|
-
*
|
|
212
|
-
* `--yolo` is a GLG operator decision, not a drifted default. The first cut passed a
|
|
213
|
-
* callback-only `--allow-tool=entwurf-bridge(entwurf_v2)` grant, and GLG's 2026-08-25 operator
|
|
214
|
-
* LIVE measured the consequence: the fresh sibling's footer showed no `YOLO`, and every tool
|
|
215
|
-
* its task needed stopped on a confirmation prompt, which made the sibling impractical to work
|
|
216
|
-
* with. GLG then set the policy explicitly: a fresh Copilot sibling carries the same managed
|
|
217
|
-
* `--yolo` profile a human-typed `entwurf copilot` gets. (Copilot 1.0.80 help: `--yolo` = all
|
|
218
|
-
* tools + all paths + all URLs.) The permission GRAMMAR lesson from that first cut — Copilot's
|
|
219
|
-
* `--allow-tool` takes `<mcp-server-name>(tool-name?)`, a different dialect from the
|
|
220
|
-
* model-facing tool name — stays recorded in `docs/adding-a-harness.md` step 9's worked
|
|
221
|
-
* example; it is a measured vendor fact even though this argv no longer uses it.
|
|
222
|
-
*
|
|
223
|
-
* OMP'S POLICY TOKEN IS THE ONE MOST EASILY ARGUED AWAY, SO READ THIS BEFORE DELETING IT.
|
|
224
|
-
* `[측정]` omp 18.0.0's schema default for `tools.approvalMode` IS ALREADY `yolo`
|
|
225
|
-
* (vendor doc `omp://approval-mode.md`; `omp config get tools.approvalMode` → `yolo` on the
|
|
226
|
-
* acceptance host). So dropping `--approval-mode yolo` changes NOTHING observable: the callback
|
|
227
|
-
* still fires, the LIVE smoke still passes, and the argv silently starts depending on a vendor
|
|
228
|
-
* default and on whatever the operator's config happens to say. That is exactly the drift step 9
|
|
229
|
-
* clause 2 forbids — "carry the chosen width as an explicit argv token rather than relying on a
|
|
230
|
-
* launcher's injected default" — and the reason the width is stated here even though the host
|
|
231
|
-
* would have granted it anyway. The width itself (task-wide, not callback-only) is a GLG
|
|
232
|
-
* operator decision of 2026-08-30, taken with the Copilot measurement in hand: a callback-only
|
|
233
|
-
* sibling names itself and then stops at the first tool its TASK needs. omp offers no argv
|
|
234
|
-
* grammar for a narrower grant at all — `tools.approval.<tool>` is a config axis, not a flag —
|
|
235
|
-
* so the honest choice was between `write` and `yolo`, and `yolo` matches what a human-typed
|
|
236
|
-
* `omp` gets on this host. `--approval-mode` takes both the space and equals form (measured);
|
|
237
|
-
* the space form is used for symmetry with `--model`.
|
|
238
|
-
*
|
|
239
|
-
* WHY OMP ALONE CARRIES NO PROMPT, AND WHY THAT IS A MEASUREMENT RATHER THAN A PREFERENCE.
|
|
240
|
-
* `[LIVE 2026-08-30]` the first public fresh call at omp DID pass the full framing as a bare
|
|
241
|
-
* positional. The window opened, the record minted (garden `20260830T181342-452167`), the
|
|
242
|
-
* prompt arrived byte-identical as a user message at `09:13:42.413Z` — and the model answered
|
|
243
|
-
* the literal text `ACK` with ZERO tool calls, because the callback tool did not exist yet.
|
|
244
|
-
* `[source]` the interactive UI defers MCP discovery and only refreshes the tool list once
|
|
245
|
-
* `discoverAndConnect()` settles (`sdk.ts:1847-1855`, `:1881-1905`), while the positional
|
|
246
|
-
* `initialMessage` prompts immediately after `await mode.init()` (`main.ts:540-565`,
|
|
247
|
-
* `595-610`). `[측정]` a `/tmp` observer on the same runtime: `turn_start` at +654ms with the
|
|
248
|
-
* entwurf tools ABSENT, callback tool present only at +1484ms — the turn began ~830ms before
|
|
249
|
-
* the tool it was told to call existed. No argv can close that gap, because the gap is a race
|
|
250
|
-
* inside the host. So the composition hands omp a PAYLOAD instead of a turn, and the
|
|
251
|
-
* in-process birth extension — which can see when the tool becomes callable — owns the first
|
|
252
|
-
* two messages (`pi-extensions/meta-bridge-omp.ts`, "THE TWO-STAGE FRESH BOOTSTRAP").
|
|
253
|
-
*
|
|
254
|
-
* The flag is fixed and one-purpose ON PURPOSE. `[측정 2026-08-30]` a normal discovered
|
|
255
|
-
* extension that registers a flag receives the operator's argv value byte-identical — quotes,
|
|
256
|
-
* `$VAR`, backticks and a semicolon all survived a 137-byte JSON payload — because extensions
|
|
257
|
-
* load before argv classification and the reparse writes the registered map
|
|
258
|
-
* (`main.ts:1799-1810`, `cli/extension-flags.ts:36-43`). An env carrier or a temp file would
|
|
259
|
-
* have needed its own quoting, its own lifetime and its own refusal rules; argv already owns
|
|
260
|
-
* all three. This is deliberately NOT a general `--flag value` passthrough — an arbitrary
|
|
261
|
-
* carrier would hand callers the launch-shaping power this rail exists to refuse.
|
|
262
|
-
*
|
|
263
|
-
* CODEX ALONE CARRIES A DIRECTORY IN ITS ARGV, AND OMITTING IT IS A WRONG ANSWER RATHER THAN A
|
|
264
|
-
* NEUTRAL ONE. `[source rust-v0.153.4]` an explicit `--remote <endpoint>` — which this argv
|
|
265
|
-
* always passes — resolves to `AppServerTarget::Remote` (`codex-rs/tui/src/lib.rs:875-876`;
|
|
266
|
-
* `LocalDaemon` is only the IMPLICIT no-flag reuse path), and a Remote target takes its new
|
|
267
|
-
* thread's directory from `remote_cwd_override` ALONE: `thread_cwd_from_config`
|
|
268
|
-
* (`codex-rs/tui/src/app_server_session.rs:2022-2033`) answers `None` without it, and the
|
|
269
|
-
* app-server then opens the thread in ITS OWN directory. That override is exactly `-C/--cd`,
|
|
270
|
-
* retained only for a remote target (`codex-rs/tui/src/startup_orchestration.rs:191-194`), and
|
|
271
|
-
* the flag lives on the shared interactive options this argv already uses for
|
|
272
|
-
* `--dangerously-bypass-approvals-and-sandbox` (`codex-rs/utils/cli/src/shared_options.rs:53-68`)
|
|
273
|
-
* — not only on the `agents` subcommand that also spells it (`codex-rs/cli/src/main.rs:334`).
|
|
274
|
-
*
|
|
275
|
-
* `[측정 2026-09-16]` with the flag absent, three citizens of one chain (pi → Codex → Claude
|
|
276
|
-
* Code) all recorded the app-server's `~/repos/gh/entwurf` while the panes themselves sat in
|
|
277
|
-
* `~/repos/gh/agent-config`, and the birth hook wrote that vendor-supplied cwd into each record
|
|
278
|
-
* honestly (#95 lane C §1). So the token is ALWAYS present for codex: ONE directory, TWO
|
|
279
|
-
* CARRIERS — tmux `-c` places the PANE, codex `-C` places the THREAD, and `launchCwd` is the
|
|
280
|
-
* single value both receive. It is not a new input axis: the value is chosen by the cwd rules
|
|
281
|
-
* this module already has, one layer up.
|
|
282
|
-
*/
|
|
283
|
-
export function buildBackendArgs(backend, composition, model, env = process.env,
|
|
284
|
-
/** Where the sibling will actually start: the directory `freshCall` chose when it chose one,
|
|
285
|
-
* and otherwise THIS process's own — which is what tmux gives a window opened with no `-c`
|
|
286
|
-
* (`[측정 ×2]`, module header). The default is that inherited fact rather than a
|
|
287
|
-
* convenience, so a caller that omits it still names the truth to codex. */
|
|
288
|
-
launchCwd = process.cwd()) {
|
|
289
|
-
switch (backend) {
|
|
290
|
-
case "pi":
|
|
291
|
-
return [composition.prompt, "--entwurf-control", "--model", model];
|
|
292
|
-
case "claude-code":
|
|
293
|
-
return [composition.prompt, `--allowedTools=${FRESH_CALL_CALLBACK_TOOL["claude-code"]}`, `--model=${model}`];
|
|
294
|
-
case "copilot":
|
|
295
|
-
return ["copilot", "--interactive", composition.prompt, "--model", model, "--yolo"];
|
|
296
|
-
case "omp":
|
|
297
|
-
return [`--${OMP_BOOTSTRAP_FLAG}`, composition.bootstrapPayload, "--model", model, "--approval-mode", "yolo"];
|
|
298
|
-
case "codex":
|
|
299
|
-
return [
|
|
300
|
-
"--remote",
|
|
301
|
-
`unix://${resolveCodexDefaultSocketPath(env)}`,
|
|
302
|
-
"-C",
|
|
303
|
-
launchCwd,
|
|
304
|
-
"--model",
|
|
305
|
-
model,
|
|
306
|
-
"--dangerously-bypass-approvals-and-sandbox",
|
|
307
|
-
composition.prompt,
|
|
308
|
-
];
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* The omp bootstrap flag, spelled WITHOUT dashes — the vendor's flag map is keyed by bare
|
|
313
|
-
* name (`extensions/loader.ts:221-228`) and this composition adds the `--` itself.
|
|
314
|
-
*
|
|
315
|
-
* Held equal to the installed extension's own constant by
|
|
316
|
-
* `test/omp-fresh-bootstrap.contract.test.ts`. The two copies exist because the extension
|
|
317
|
-
* ships INSIDE the omp agent dir carrying only its own small closure and cannot import this
|
|
318
|
-
* module; the gate is what keeps the duplication from becoming drift.
|
|
319
|
-
*/
|
|
320
|
-
export const OMP_BOOTSTRAP_FLAG = "entwurf-bootstrap";
|
|
321
|
-
/** Payload grammar version, matched exactly by the decoder. A bump means a stale installed
|
|
322
|
-
* unit, which is the one thing `doctor-omp-bridge` exists to say out loud. */
|
|
323
|
-
export const OMP_BOOTSTRAP_VERSION = 1;
|
|
324
|
-
/**
|
|
325
|
-
* The whole of what a fresh omp sibling is launched with.
|
|
326
|
-
*
|
|
327
|
-
* THREE FIELDS, CLOSED. The decoder refuses an unknown key, so this object is the entire
|
|
328
|
-
* contract: who to call back, the nonce that proves it is this call, and the task that is
|
|
329
|
-
* released only after that callback succeeds. There is no command here, no path, no env name
|
|
330
|
-
* and no model — the model is already an explicit argv token, and a second copy of it inside
|
|
331
|
-
* a payload would be a second place for it to disagree with the launch.
|
|
332
|
-
*/
|
|
333
|
-
export function buildOmpBootstrapPayload(params) {
|
|
334
|
-
return JSON.stringify({
|
|
335
|
-
v: OMP_BOOTSTRAP_VERSION,
|
|
336
|
-
target: params.callerGardenId,
|
|
337
|
-
nonce: params.nonce,
|
|
338
|
-
task: params.task,
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* The first-turn framing. Order is the contract: the callback is the FIRST action and the task
|
|
343
|
-
* follows it, so a sibling that gets stuck in the task has already told the caller who it is.
|
|
344
|
-
*
|
|
345
|
-
* The three prohibitions are not politeness. Each names a detour that was measured to produce a
|
|
346
|
-
* confidently wrong answer or a wasted turn.
|
|
347
|
-
*/
|
|
348
|
-
export function buildFreshCallPrompt(params) {
|
|
349
|
-
const tool = FRESH_CALL_CALLBACK_TOOL[params.backend];
|
|
350
|
-
return [
|
|
351
|
-
"You are a fresh visible citizen that entwurf opened in the operator's tmux session.",
|
|
352
|
-
"",
|
|
353
|
-
`FIRST ACTION, before reading files or anything else: call ${tool} with ` +
|
|
354
|
-
`target=${params.callerGardenId}, intent=fire-and-forget, wants_reply=false, and ` +
|
|
355
|
-
`message set to exactly ${params.nonce} — that string alone, nothing added.`,
|
|
356
|
-
"That call is how the agent that opened you learns your address. Do not skip it, do not",
|
|
357
|
-
"defer it until the task is done, and do not reword the message.",
|
|
358
|
-
"",
|
|
359
|
-
"Do not inspect environment variables, do not call entwurf_self, and do not start an MCP",
|
|
360
|
-
"server yourself. Your own report of your identity is not the address anyone needs.",
|
|
361
|
-
"",
|
|
362
|
-
"After the tool receipt, carry out this task:",
|
|
363
|
-
"",
|
|
364
|
-
params.task,
|
|
365
|
-
].join("\n");
|
|
366
|
-
}
|
|
367
157
|
/**
|
|
368
158
|
* WHICH RULE PICKS THE TARGET SESSION. Three in strict order, and the order is the contract:
|
|
369
159
|
*
|
|
@@ -390,14 +180,6 @@ export function selectFreshCallSeat(placement) {
|
|
|
390
180
|
return { tmuxSession: placement.tmuxSession, source: "requested" };
|
|
391
181
|
return null;
|
|
392
182
|
}
|
|
393
|
-
/** Correlation tag only. Random, never derived from time, cwd or a peer listing — a nonce that
|
|
394
|
-
* encoded any of those would invite exactly the guessing this rail exists to refuse. */
|
|
395
|
-
export function mintNonce(randomHex = defaultRandomHex) {
|
|
396
|
-
return `mux-fresh-call-${randomHex()}`;
|
|
397
|
-
}
|
|
398
|
-
function defaultRandomHex() {
|
|
399
|
-
return randomBytes(12).toString("hex");
|
|
400
|
-
}
|
|
401
183
|
/**
|
|
402
184
|
* The pi identity carrier, scrubbed at the launch seam for EVERY backend (#87 Bundle C).
|
|
403
185
|
*
|
|
@@ -481,28 +263,19 @@ export function buildFreshCallArgs(targetSessionId, runtimePath, backendArgs, cw
|
|
|
481
263
|
* from a seat, a workspace map or a project name.
|
|
482
264
|
*/
|
|
483
265
|
export function freshCall(params, env = process.env, nonce = mintNonce()) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
// ONLY `undefined` and the exact empty string mean "no cwd". Everything else is the literal
|
|
498
|
-
// value — deliberately untrimmed, so a whitespace-mangled path is refused loudly by the
|
|
499
|
-
// classification below instead of being silently repaired into a different directory. The
|
|
500
|
-
// caller's own record directory answers the SAME two-value emptiness rule and is consulted
|
|
501
|
-
// ONLY second: an explicit request always wins, and a caller that supplies neither leaves the
|
|
502
|
-
// pane to inherit this process's directory exactly as before — a pi caller's argv is
|
|
503
|
-
// byte-identical, because its process directory IS its own and a `-c` token would change
|
|
504
|
-
// nothing about where that window lands.
|
|
505
|
-
const requestedCwd = params.cwd === undefined || params.cwd === "" ? undefined : params.cwd;
|
|
266
|
+
// The caller-facing input contract lives in the composition leaf so BOTH rails answer a
|
|
267
|
+
// mistyped model or an oversized task with the same words. Order, trimming and the
|
|
268
|
+
// cwd-omission rule (`undefined` and the exact empty string, and nothing else, mean "no
|
|
269
|
+
// cwd") are unchanged from when they lived here.
|
|
270
|
+
const normalized = normalizeFreshCallInputs(params);
|
|
271
|
+
if (!normalized.ok)
|
|
272
|
+
return { ok: false, reason: normalized.reason };
|
|
273
|
+
const { callerGardenId, model, task, cwd: requestedCwd } = normalized.inputs;
|
|
274
|
+
// The caller's own record directory answers the SAME two-value emptiness rule the leaf
|
|
275
|
+
// applies to a requested cwd, and is consulted ONLY second: an explicit request always wins,
|
|
276
|
+
// and a caller that supplies neither leaves the pane to inherit this process's directory
|
|
277
|
+
// exactly as before — a pi caller's argv is byte-identical, because its process directory IS
|
|
278
|
+
// its own and a `-c` token would change nothing about where that window lands.
|
|
506
279
|
const callerCwd = params.callerCwd === undefined || params.callerCwd === "" ? undefined : params.callerCwd;
|
|
507
280
|
const chosenCwd = requestedCwd !== undefined
|
|
508
281
|
? { value: requestedCwd, source: "requested" }
|
|
@@ -596,10 +369,10 @@ export function freshCall(params, env = process.env, nonce = mintNonce()) {
|
|
|
596
369
|
prompt: buildFreshCallPrompt({
|
|
597
370
|
backend: params.backend,
|
|
598
371
|
task,
|
|
599
|
-
callerGardenId
|
|
372
|
+
callerGardenId,
|
|
600
373
|
nonce,
|
|
601
374
|
}),
|
|
602
|
-
bootstrapPayload: buildOmpBootstrapPayload({ callerGardenId
|
|
375
|
+
bootstrapPayload: buildOmpBootstrapPayload({ callerGardenId, nonce, task }),
|
|
603
376
|
};
|
|
604
377
|
const backendArgs = buildBackendArgs(params.backend, composition, model, env, cwd);
|
|
605
378
|
// THE LAUNCH-DIRECTORY NOTE, AND IT IS A DIAGNOSTIC RATHER THAN A GATE. `[측정 2026-09-16]` a
|