@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
|
@@ -86,7 +86,6 @@
|
|
|
86
86
|
* there is no "session created" field because nothing here creates one.
|
|
87
87
|
*/
|
|
88
88
|
|
|
89
|
-
import { randomBytes } from "node:crypto";
|
|
90
89
|
import { classifyTmuxCwd, type TmuxCwdRejectReason } from "./classify-tmux-cwd.ts";
|
|
91
90
|
import {
|
|
92
91
|
CODEX_CALLER_SEAT_HINT,
|
|
@@ -106,6 +105,23 @@ import {
|
|
|
106
105
|
type CopilotPreflightRejectReason,
|
|
107
106
|
copilotFreshPreflight,
|
|
108
107
|
} from "./copilot-fresh-preflight.ts";
|
|
108
|
+
import {
|
|
109
|
+
buildOmpBootstrapPayload,
|
|
110
|
+
composeBackendArgs,
|
|
111
|
+
composeFreshCallPrompt,
|
|
112
|
+
FRESH_CALL_BACKENDS,
|
|
113
|
+
FRESH_CALL_CALLBACK_TOOL,
|
|
114
|
+
type FreshCallBackend,
|
|
115
|
+
type FreshCallComposition,
|
|
116
|
+
type FreshCallInputRejectReason,
|
|
117
|
+
isSafeFreshCallModel,
|
|
118
|
+
MODEL_MAX_CHARS,
|
|
119
|
+
mintNonce,
|
|
120
|
+
normalizeFreshCallInputs,
|
|
121
|
+
OMP_BOOTSTRAP_FLAG,
|
|
122
|
+
OMP_BOOTSTRAP_VERSION,
|
|
123
|
+
TASK_MAX_CHARS,
|
|
124
|
+
} from "./fresh-call-composition.ts";
|
|
109
125
|
import {
|
|
110
126
|
assertLaunchTarget,
|
|
111
127
|
LaunchPreconditionError,
|
|
@@ -127,13 +143,69 @@ import { resolveCodexDefaultSocketPath } from "./native-push/codex-ws-client.ts"
|
|
|
127
143
|
import { OMP_PREFLIGHT_HINT, type OmpPreflightRejectReason, ompFreshPreflight } from "./omp-fresh-preflight.ts";
|
|
128
144
|
import { classifyTmuxSessionName, resolveTmuxSessionId, type TmuxSessionRejectReason } from "./resolve-tmux-session.ts";
|
|
129
145
|
|
|
130
|
-
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
/**
|
|
147
|
+
* THE COMPOSITION MOVED, THE RAIL DID NOT (#116 S2-a). Backend argv, the first-turn framing and
|
|
148
|
+
* the nonce now live in `fresh-call-composition.ts`, which knows nothing about tmux, so the herdr
|
|
149
|
+
* rail can reach them without importing this file and dragging placement behind it. They are
|
|
150
|
+
* re-exported here unchanged: every existing caller, gate and mutant anchor that reads them from
|
|
151
|
+
* this module keeps reading the same names.
|
|
152
|
+
*
|
|
153
|
+
* The two wrappers below are where this rail states what only it can state — the sentence that
|
|
154
|
+
* says a sibling was opened in the operator's tmux session, and the Codex socket path. Both are
|
|
155
|
+
* arguments to the leaf rather than knowledge inside it.
|
|
156
|
+
*/
|
|
157
|
+
export {
|
|
158
|
+
buildOmpBootstrapPayload,
|
|
159
|
+
FRESH_CALL_BACKENDS,
|
|
160
|
+
FRESH_CALL_CALLBACK_TOOL,
|
|
161
|
+
type FreshCallBackend,
|
|
162
|
+
type FreshCallComposition,
|
|
163
|
+
isSafeFreshCallModel,
|
|
164
|
+
MODEL_MAX_CHARS,
|
|
165
|
+
mintNonce,
|
|
166
|
+
OMP_BOOTSTRAP_FLAG,
|
|
167
|
+
OMP_BOOTSTRAP_VERSION,
|
|
168
|
+
TASK_MAX_CHARS,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/** What the tmux rail tells a sibling about where it woke up. The resume verb has no equivalent
|
|
172
|
+
* because it composes no first turn at all; a herdr rail will pass its own sentence here. */
|
|
173
|
+
export const TMUX_FRESH_CALL_OPENING_LINE =
|
|
174
|
+
"You are a fresh visible citizen that entwurf opened in the operator's tmux session.";
|
|
175
|
+
|
|
176
|
+
/** Backend argv for THIS rail: the neutral dialect, with the two Codex facts only a rail can
|
|
177
|
+
* state — the socket resolved from the caller's env, and the directory this launch chose.
|
|
178
|
+
*
|
|
179
|
+
* `launchCwd` defaults to THIS process's own directory, which is what tmux gives a window opened
|
|
180
|
+
* with no `-c` (`[측정 ×2]`, module header). The default is that inherited fact rather than a
|
|
181
|
+
* convenience, so a caller that omits it still names the truth to codex. Both facts reach the
|
|
182
|
+
* leaf lazily, because a host with no Codex home must still be able to open pi and claude
|
|
183
|
+
* siblings — see `composeBackendArgs`. */
|
|
184
|
+
export function buildBackendArgs(
|
|
185
|
+
backend: FreshCallBackend,
|
|
186
|
+
composition: FreshCallComposition,
|
|
187
|
+
model: string,
|
|
188
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
189
|
+
launchCwd: string = process.cwd(),
|
|
190
|
+
): string[] {
|
|
191
|
+
return composeBackendArgs(
|
|
192
|
+
backend,
|
|
193
|
+
composition,
|
|
194
|
+
model,
|
|
195
|
+
() => resolveCodexDefaultSocketPath(env),
|
|
196
|
+
() => launchCwd,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The first turn for THIS rail: the neutral framing under this rail's placement sentence. */
|
|
201
|
+
export function buildFreshCallPrompt(params: {
|
|
202
|
+
backend: FreshCallBackend;
|
|
203
|
+
task: string;
|
|
204
|
+
callerGardenId: string;
|
|
205
|
+
nonce: string;
|
|
206
|
+
}): string {
|
|
207
|
+
return composeFreshCallPrompt({ ...params, openingLine: TMUX_FRESH_CALL_OPENING_LINE });
|
|
208
|
+
}
|
|
137
209
|
|
|
138
210
|
/**
|
|
139
211
|
* The fixed runtime each backend resolves on PATH. Same reason `mux-launch` uses PATH rather
|
|
@@ -165,273 +237,11 @@ export const FRESH_CALL_RUNTIME: Record<FreshCallBackend, string> = {
|
|
|
165
237
|
codex: "codex",
|
|
166
238
|
};
|
|
167
239
|
|
|
168
|
-
/**
|
|
169
|
-
* The callback tool NAME differs per backend and that is not cosmetic: native pi exposes the
|
|
170
|
-
* capability directly (`entwurf_v2`), while an MCP-hosted session reaches it under whatever
|
|
171
|
-
* name that harness composes. Naming the wrong one costs the whole first turn.
|
|
172
|
-
*
|
|
173
|
-
* `[측정]` Copilot CLI 1.0.80 composes `<mcpServerName>-<mcpToolName>` — NOT Claude Code's
|
|
174
|
-
* `mcp__<server>__<tool>`. Read from two independent sessions' own event logs
|
|
175
|
-
* (`~/.copilot/session-state/<id>/events.jsonl`): `assistant.message.toolRequests[].name` and
|
|
176
|
-
* `tool.execution_start.toolName` both carry `entwurf-bridge-entwurf_v2`, with
|
|
177
|
-
* `mcpServerName`/`mcpToolName` beside them as the parts. Derive-and-measure, never copy a
|
|
178
|
-
* sibling's spelling (`docs/adding-a-harness.md` step 5).
|
|
179
|
-
*
|
|
180
|
-
* `[측정]` omp 18.0.0 is the sharpest case for that rule: it mints
|
|
181
|
-
* `mcp__${sanitizedServerName}_${normalizedToolName}` with a sanitizer whose charset is
|
|
182
|
-
* `[a-z_]` (`mcp/tool-bridge.ts:351-357`, `:396`), so the DIGIT IN `entwurf_v2` IS EATEN and the
|
|
183
|
-
* hyphen in the server key becomes an underscore — the model-facing name is
|
|
184
|
-
* `mcp__entwurf_bridge_entwurf_v`, not `..._entwurf_v2` and not Claude's double-underscore form.
|
|
185
|
-
* Confirmed against a live tool dump of all seven bridge tools and a real session transcript
|
|
186
|
-
* (`scripts/raw-omp-measure/README.md` "Tool-name dialect"). Unlike Copilot there is no second
|
|
187
|
-
* permission dialect: omp's approval layer consults the same minted string (`source-audit.md`).
|
|
188
|
-
*/
|
|
189
|
-
export const FRESH_CALL_CALLBACK_TOOL: Record<FreshCallBackend, string> = {
|
|
190
|
-
pi: "entwurf_v2",
|
|
191
|
-
"claude-code": "mcp__entwurf-bridge__entwurf_v2",
|
|
192
|
-
copilot: "entwurf-bridge-entwurf_v2",
|
|
193
|
-
omp: "mcp__entwurf_bridge_entwurf_v",
|
|
194
|
-
codex: "mcp__entwurf_bridge__entwurf_v2",
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
/** Mirrors the `entwurf_v2` message bound. This is an INTERFACE cap for symmetry with the
|
|
198
|
-
* delivery surface, not a claim that a task of this size was measured through tmux. An argv
|
|
199
|
-
* that the OS refuses is a launch failure and fails loud — it never reads as a delivered task. */
|
|
200
|
-
export const TASK_MAX_CHARS = 16000;
|
|
201
|
-
export const MODEL_MAX_CHARS = 200;
|
|
202
|
-
const MODEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/:[\]-]*$/;
|
|
203
|
-
|
|
204
|
-
/** A model is an explicit launch input, not ambient process state. The grammar admits canonical
|
|
205
|
-
* pi provider/model ids, Claude model ids/aliases, and bracketed context variants, while refusing
|
|
206
|
-
* whitespace and tmux control syntax. It is passed without a shell using each runtime's measured
|
|
207
|
-
* CLI dialect: Pi takes `--model`, value; Claude Code takes `--model=value`. */
|
|
208
|
-
export function isSafeFreshCallModel(model: string): boolean {
|
|
209
|
-
return model.length > 0 && model.length <= MODEL_MAX_CHARS && MODEL_PATTERN.test(model);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Backend argv AFTER the runtime path. Both orders are MEASURED, and both were measured by
|
|
214
|
-
* getting them wrong first (rail §6-a):
|
|
215
|
-
*
|
|
216
|
-
* pi — prompt BEFORE `--entwurf-control`, then `--model`, value as TWO tokens.
|
|
217
|
-
* Flag-first submitted no message; Pi rejects the equals form for `--model`.
|
|
218
|
-
* claude-code — prompt, then `--allowedTools=` and `--model=` as ONE token each. The space form
|
|
219
|
-
* for allowedTools is variadic and eats the prompt as an option value.
|
|
220
|
-
* copilot — the managed VERB first, then the prompt as the value of `-i/--interactive`,
|
|
221
|
-
* `--model`, value as two tokens, and the policy as the explicit `--yolo`
|
|
222
|
-
* token. Measured from `copilot --help` (1.0.80).
|
|
223
|
-
* omp — NO positional prompt at all. The task rides `--entwurf-bootstrap`, a fixed
|
|
224
|
-
* flag the installed birth extension registers, then `--model`, value as two
|
|
225
|
-
* tokens, then the policy as `--approval-mode`, `yolo`. `-p/--print` remains
|
|
226
|
-
* the flag this argv must never carry — it processes a prompt and EXITS,
|
|
227
|
-
* closing the window on a sibling that has to stay open to be delivered to.
|
|
228
|
-
*
|
|
229
|
-
* Both pi/claude failures looked identical from outside: window open, record and socket minted,
|
|
230
|
-
* no turn.
|
|
231
|
-
*
|
|
232
|
-
* For pi and claude-code the equals form is NOT a permission guarantee — on the measured host
|
|
233
|
-
* the tool was already permitted, so the option's effect was unobservable. What was observed is
|
|
234
|
-
* that it does no harm to the argv. Permission stays a documented host precondition there.
|
|
235
|
-
*
|
|
236
|
-
* Copilot is the backend where the policy IS carried explicitly (step 9 clause 2), and three of
|
|
237
|
-
* its argv facts are load-bearing:
|
|
238
|
-
*
|
|
239
|
-
* - `copilot` is argv[0] of the RUNTIME `entwurf`, i.e. the managed verb — see
|
|
240
|
-
* `FRESH_CALL_RUNTIME`. Everything after it is forwarded byte-identical by
|
|
241
|
-
* `scripts/copilot-launch.sh`.
|
|
242
|
-
* - the prompt rides `--interactive`, never `-p/--prompt`: `-p` runs the prompt and EXITS,
|
|
243
|
-
* which would close the window on a sibling that is supposed to stay open and be delivered
|
|
244
|
-
* to. `--interactive <prompt>` is non-variadic, so the space form is safe here.
|
|
245
|
-
* - the policy token is `--yolo`, STATED HERE rather than left to the launcher: the launcher
|
|
246
|
-
* injects `--yolo` only when the argv names no policy, and step 9 clause 2 requires the
|
|
247
|
-
* fresh composition to state its model and permission policy explicitly, never to rely
|
|
248
|
-
* invisibly on someone else's default.
|
|
249
|
-
*
|
|
250
|
-
* `--yolo` is a GLG operator decision, not a drifted default. The first cut passed a
|
|
251
|
-
* callback-only `--allow-tool=entwurf-bridge(entwurf_v2)` grant, and GLG's 2026-08-25 operator
|
|
252
|
-
* LIVE measured the consequence: the fresh sibling's footer showed no `YOLO`, and every tool
|
|
253
|
-
* its task needed stopped on a confirmation prompt, which made the sibling impractical to work
|
|
254
|
-
* with. GLG then set the policy explicitly: a fresh Copilot sibling carries the same managed
|
|
255
|
-
* `--yolo` profile a human-typed `entwurf copilot` gets. (Copilot 1.0.80 help: `--yolo` = all
|
|
256
|
-
* tools + all paths + all URLs.) The permission GRAMMAR lesson from that first cut — Copilot's
|
|
257
|
-
* `--allow-tool` takes `<mcp-server-name>(tool-name?)`, a different dialect from the
|
|
258
|
-
* model-facing tool name — stays recorded in `docs/adding-a-harness.md` step 9's worked
|
|
259
|
-
* example; it is a measured vendor fact even though this argv no longer uses it.
|
|
260
|
-
*
|
|
261
|
-
* OMP'S POLICY TOKEN IS THE ONE MOST EASILY ARGUED AWAY, SO READ THIS BEFORE DELETING IT.
|
|
262
|
-
* `[측정]` omp 18.0.0's schema default for `tools.approvalMode` IS ALREADY `yolo`
|
|
263
|
-
* (vendor doc `omp://approval-mode.md`; `omp config get tools.approvalMode` → `yolo` on the
|
|
264
|
-
* acceptance host). So dropping `--approval-mode yolo` changes NOTHING observable: the callback
|
|
265
|
-
* still fires, the LIVE smoke still passes, and the argv silently starts depending on a vendor
|
|
266
|
-
* default and on whatever the operator's config happens to say. That is exactly the drift step 9
|
|
267
|
-
* clause 2 forbids — "carry the chosen width as an explicit argv token rather than relying on a
|
|
268
|
-
* launcher's injected default" — and the reason the width is stated here even though the host
|
|
269
|
-
* would have granted it anyway. The width itself (task-wide, not callback-only) is a GLG
|
|
270
|
-
* operator decision of 2026-08-30, taken with the Copilot measurement in hand: a callback-only
|
|
271
|
-
* sibling names itself and then stops at the first tool its TASK needs. omp offers no argv
|
|
272
|
-
* grammar for a narrower grant at all — `tools.approval.<tool>` is a config axis, not a flag —
|
|
273
|
-
* so the honest choice was between `write` and `yolo`, and `yolo` matches what a human-typed
|
|
274
|
-
* `omp` gets on this host. `--approval-mode` takes both the space and equals form (measured);
|
|
275
|
-
* the space form is used for symmetry with `--model`.
|
|
276
|
-
*
|
|
277
|
-
* WHY OMP ALONE CARRIES NO PROMPT, AND WHY THAT IS A MEASUREMENT RATHER THAN A PREFERENCE.
|
|
278
|
-
* `[LIVE 2026-08-30]` the first public fresh call at omp DID pass the full framing as a bare
|
|
279
|
-
* positional. The window opened, the record minted (garden `20260830T181342-452167`), the
|
|
280
|
-
* prompt arrived byte-identical as a user message at `09:13:42.413Z` — and the model answered
|
|
281
|
-
* the literal text `ACK` with ZERO tool calls, because the callback tool did not exist yet.
|
|
282
|
-
* `[source]` the interactive UI defers MCP discovery and only refreshes the tool list once
|
|
283
|
-
* `discoverAndConnect()` settles (`sdk.ts:1847-1855`, `:1881-1905`), while the positional
|
|
284
|
-
* `initialMessage` prompts immediately after `await mode.init()` (`main.ts:540-565`,
|
|
285
|
-
* `595-610`). `[측정]` a `/tmp` observer on the same runtime: `turn_start` at +654ms with the
|
|
286
|
-
* entwurf tools ABSENT, callback tool present only at +1484ms — the turn began ~830ms before
|
|
287
|
-
* the tool it was told to call existed. No argv can close that gap, because the gap is a race
|
|
288
|
-
* inside the host. So the composition hands omp a PAYLOAD instead of a turn, and the
|
|
289
|
-
* in-process birth extension — which can see when the tool becomes callable — owns the first
|
|
290
|
-
* two messages (`pi-extensions/meta-bridge-omp.ts`, "THE TWO-STAGE FRESH BOOTSTRAP").
|
|
291
|
-
*
|
|
292
|
-
* The flag is fixed and one-purpose ON PURPOSE. `[측정 2026-08-30]` a normal discovered
|
|
293
|
-
* extension that registers a flag receives the operator's argv value byte-identical — quotes,
|
|
294
|
-
* `$VAR`, backticks and a semicolon all survived a 137-byte JSON payload — because extensions
|
|
295
|
-
* load before argv classification and the reparse writes the registered map
|
|
296
|
-
* (`main.ts:1799-1810`, `cli/extension-flags.ts:36-43`). An env carrier or a temp file would
|
|
297
|
-
* have needed its own quoting, its own lifetime and its own refusal rules; argv already owns
|
|
298
|
-
* all three. This is deliberately NOT a general `--flag value` passthrough — an arbitrary
|
|
299
|
-
* carrier would hand callers the launch-shaping power this rail exists to refuse.
|
|
300
|
-
*
|
|
301
|
-
* CODEX ALONE CARRIES A DIRECTORY IN ITS ARGV, AND OMITTING IT IS A WRONG ANSWER RATHER THAN A
|
|
302
|
-
* NEUTRAL ONE. `[source rust-v0.153.4]` an explicit `--remote <endpoint>` — which this argv
|
|
303
|
-
* always passes — resolves to `AppServerTarget::Remote` (`codex-rs/tui/src/lib.rs:875-876`;
|
|
304
|
-
* `LocalDaemon` is only the IMPLICIT no-flag reuse path), and a Remote target takes its new
|
|
305
|
-
* thread's directory from `remote_cwd_override` ALONE: `thread_cwd_from_config`
|
|
306
|
-
* (`codex-rs/tui/src/app_server_session.rs:2022-2033`) answers `None` without it, and the
|
|
307
|
-
* app-server then opens the thread in ITS OWN directory. That override is exactly `-C/--cd`,
|
|
308
|
-
* retained only for a remote target (`codex-rs/tui/src/startup_orchestration.rs:191-194`), and
|
|
309
|
-
* the flag lives on the shared interactive options this argv already uses for
|
|
310
|
-
* `--dangerously-bypass-approvals-and-sandbox` (`codex-rs/utils/cli/src/shared_options.rs:53-68`)
|
|
311
|
-
* — not only on the `agents` subcommand that also spells it (`codex-rs/cli/src/main.rs:334`).
|
|
312
|
-
*
|
|
313
|
-
* `[측정 2026-09-16]` with the flag absent, three citizens of one chain (pi → Codex → Claude
|
|
314
|
-
* Code) all recorded the app-server's `~/repos/gh/entwurf` while the panes themselves sat in
|
|
315
|
-
* `~/repos/gh/agent-config`, and the birth hook wrote that vendor-supplied cwd into each record
|
|
316
|
-
* honestly (#95 lane C §1). So the token is ALWAYS present for codex: ONE directory, TWO
|
|
317
|
-
* CARRIERS — tmux `-c` places the PANE, codex `-C` places the THREAD, and `launchCwd` is the
|
|
318
|
-
* single value both receive. It is not a new input axis: the value is chosen by the cwd rules
|
|
319
|
-
* this module already has, one layer up.
|
|
320
|
-
*/
|
|
321
|
-
export function buildBackendArgs(
|
|
322
|
-
backend: FreshCallBackend,
|
|
323
|
-
composition: FreshCallComposition,
|
|
324
|
-
model: string,
|
|
325
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
326
|
-
/** Where the sibling will actually start: the directory `freshCall` chose when it chose one,
|
|
327
|
-
* and otherwise THIS process's own — which is what tmux gives a window opened with no `-c`
|
|
328
|
-
* (`[측정 ×2]`, module header). The default is that inherited fact rather than a
|
|
329
|
-
* convenience, so a caller that omits it still names the truth to codex. */
|
|
330
|
-
launchCwd: string = process.cwd(),
|
|
331
|
-
): string[] {
|
|
332
|
-
switch (backend) {
|
|
333
|
-
case "pi":
|
|
334
|
-
return [composition.prompt, "--entwurf-control", "--model", model];
|
|
335
|
-
case "claude-code":
|
|
336
|
-
return [composition.prompt, `--allowedTools=${FRESH_CALL_CALLBACK_TOOL["claude-code"]}`, `--model=${model}`];
|
|
337
|
-
case "copilot":
|
|
338
|
-
return ["copilot", "--interactive", composition.prompt, "--model", model, "--yolo"];
|
|
339
|
-
case "omp":
|
|
340
|
-
return [`--${OMP_BOOTSTRAP_FLAG}`, composition.bootstrapPayload, "--model", model, "--approval-mode", "yolo"];
|
|
341
|
-
case "codex":
|
|
342
|
-
return [
|
|
343
|
-
"--remote",
|
|
344
|
-
`unix://${resolveCodexDefaultSocketPath(env)}`,
|
|
345
|
-
"-C",
|
|
346
|
-
launchCwd,
|
|
347
|
-
"--model",
|
|
348
|
-
model,
|
|
349
|
-
"--dangerously-bypass-approvals-and-sandbox",
|
|
350
|
-
composition.prompt,
|
|
351
|
-
];
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* What a launch has to say, in the two shapes the five backends need. Four of them are
|
|
357
|
-
* handed a first-turn PROMPT; omp is handed a bootstrap PAYLOAD its own installed extension
|
|
358
|
-
* unpacks. Both are always built, because building one is cheap and a backend switch must
|
|
359
|
-
* never be able to reach a field that was not composed.
|
|
360
|
-
*/
|
|
361
|
-
export interface FreshCallComposition {
|
|
362
|
-
prompt: string;
|
|
363
|
-
bootstrapPayload: string;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* The omp bootstrap flag, spelled WITHOUT dashes — the vendor's flag map is keyed by bare
|
|
368
|
-
* name (`extensions/loader.ts:221-228`) and this composition adds the `--` itself.
|
|
369
|
-
*
|
|
370
|
-
* Held equal to the installed extension's own constant by
|
|
371
|
-
* `test/omp-fresh-bootstrap.contract.test.ts`. The two copies exist because the extension
|
|
372
|
-
* ships INSIDE the omp agent dir carrying only its own small closure and cannot import this
|
|
373
|
-
* module; the gate is what keeps the duplication from becoming drift.
|
|
374
|
-
*/
|
|
375
|
-
export const OMP_BOOTSTRAP_FLAG = "entwurf-bootstrap";
|
|
376
|
-
|
|
377
|
-
/** Payload grammar version, matched exactly by the decoder. A bump means a stale installed
|
|
378
|
-
* unit, which is the one thing `doctor-omp-bridge` exists to say out loud. */
|
|
379
|
-
export const OMP_BOOTSTRAP_VERSION = 1;
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* The whole of what a fresh omp sibling is launched with.
|
|
383
|
-
*
|
|
384
|
-
* THREE FIELDS, CLOSED. The decoder refuses an unknown key, so this object is the entire
|
|
385
|
-
* contract: who to call back, the nonce that proves it is this call, and the task that is
|
|
386
|
-
* released only after that callback succeeds. There is no command here, no path, no env name
|
|
387
|
-
* and no model — the model is already an explicit argv token, and a second copy of it inside
|
|
388
|
-
* a payload would be a second place for it to disagree with the launch.
|
|
389
|
-
*/
|
|
390
|
-
export function buildOmpBootstrapPayload(params: { callerGardenId: string; nonce: string; task: string }): string {
|
|
391
|
-
return JSON.stringify({
|
|
392
|
-
v: OMP_BOOTSTRAP_VERSION,
|
|
393
|
-
target: params.callerGardenId,
|
|
394
|
-
nonce: params.nonce,
|
|
395
|
-
task: params.task,
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* The first-turn framing. Order is the contract: the callback is the FIRST action and the task
|
|
401
|
-
* follows it, so a sibling that gets stuck in the task has already told the caller who it is.
|
|
402
|
-
*
|
|
403
|
-
* The three prohibitions are not politeness. Each names a detour that was measured to produce a
|
|
404
|
-
* confidently wrong answer or a wasted turn.
|
|
405
|
-
*/
|
|
406
|
-
export function buildFreshCallPrompt(params: {
|
|
407
|
-
backend: FreshCallBackend;
|
|
408
|
-
task: string;
|
|
409
|
-
callerGardenId: string;
|
|
410
|
-
nonce: string;
|
|
411
|
-
}): string {
|
|
412
|
-
const tool = FRESH_CALL_CALLBACK_TOOL[params.backend];
|
|
413
|
-
return [
|
|
414
|
-
"You are a fresh visible citizen that entwurf opened in the operator's tmux session.",
|
|
415
|
-
"",
|
|
416
|
-
`FIRST ACTION, before reading files or anything else: call ${tool} with ` +
|
|
417
|
-
`target=${params.callerGardenId}, intent=fire-and-forget, wants_reply=false, and ` +
|
|
418
|
-
`message set to exactly ${params.nonce} — that string alone, nothing added.`,
|
|
419
|
-
"That call is how the agent that opened you learns your address. Do not skip it, do not",
|
|
420
|
-
"defer it until the task is done, and do not reword the message.",
|
|
421
|
-
"",
|
|
422
|
-
"Do not inspect environment variables, do not call entwurf_self, and do not start an MCP",
|
|
423
|
-
"server yourself. Your own report of your identity is not the address anyone needs.",
|
|
424
|
-
"",
|
|
425
|
-
"After the tool receipt, carry out this task:",
|
|
426
|
-
"",
|
|
427
|
-
params.task,
|
|
428
|
-
].join("\n");
|
|
429
|
-
}
|
|
430
|
-
|
|
431
240
|
/** A launch that was refused, or a placement that could not be established. Every value is a
|
|
432
241
|
* NAMED refusal — this module has no fallback launch and no fallback directory. The cwd members
|
|
433
242
|
* come from the shared classification leaf and their string values are stable contract. */
|
|
434
243
|
export type FreshCallRejectReason =
|
|
244
|
+
| FreshCallInputRejectReason
|
|
435
245
|
| PlacementRejectReason
|
|
436
246
|
| LaunchRejectReason
|
|
437
247
|
| TmuxCwdRejectReason
|
|
@@ -521,16 +331,6 @@ export interface FreshCallReceipt extends WindowHandle {
|
|
|
521
331
|
|
|
522
332
|
export type FreshCallResult = { ok: true; receipt: FreshCallReceipt } | { ok: false; reason: FreshCallRejectReason };
|
|
523
333
|
|
|
524
|
-
/** Correlation tag only. Random, never derived from time, cwd or a peer listing — a nonce that
|
|
525
|
-
* encoded any of those would invite exactly the guessing this rail exists to refuse. */
|
|
526
|
-
export function mintNonce(randomHex: () => string = defaultRandomHex): string {
|
|
527
|
-
return `mux-fresh-call-${randomHex()}`;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function defaultRandomHex(): string {
|
|
531
|
-
return randomBytes(12).toString("hex");
|
|
532
|
-
}
|
|
533
|
-
|
|
534
334
|
/**
|
|
535
335
|
* The pi identity carrier, scrubbed at the launch seam for EVERY backend (#87 Bundle C).
|
|
536
336
|
*
|
|
@@ -633,24 +433,18 @@ export function freshCall(
|
|
|
633
433
|
env: NodeJS.ProcessEnv = process.env,
|
|
634
434
|
nonce: string = mintNonce(),
|
|
635
435
|
): FreshCallResult {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if (!
|
|
642
|
-
const task =
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
// caller's own record directory answers the SAME two-value emptiness rule and is consulted
|
|
649
|
-
// ONLY second: an explicit request always wins, and a caller that supplies neither leaves the
|
|
650
|
-
// pane to inherit this process's directory exactly as before — a pi caller's argv is
|
|
651
|
-
// byte-identical, because its process directory IS its own and a `-c` token would change
|
|
652
|
-
// nothing about where that window lands.
|
|
653
|
-
const requestedCwd = params.cwd === undefined || params.cwd === "" ? undefined : params.cwd;
|
|
436
|
+
// The caller-facing input contract lives in the composition leaf so BOTH rails answer a
|
|
437
|
+
// mistyped model or an oversized task with the same words. Order, trimming and the
|
|
438
|
+
// cwd-omission rule (`undefined` and the exact empty string, and nothing else, mean "no
|
|
439
|
+
// cwd") are unchanged from when they lived here.
|
|
440
|
+
const normalized = normalizeFreshCallInputs(params);
|
|
441
|
+
if (!normalized.ok) return { ok: false, reason: normalized.reason };
|
|
442
|
+
const { callerGardenId, model, task, cwd: requestedCwd } = normalized.inputs;
|
|
443
|
+
// The caller's own record directory answers the SAME two-value emptiness rule the leaf
|
|
444
|
+
// applies to a requested cwd, and is consulted ONLY second: an explicit request always wins,
|
|
445
|
+
// and a caller that supplies neither leaves the pane to inherit this process's directory
|
|
446
|
+
// exactly as before — a pi caller's argv is byte-identical, because its process directory IS
|
|
447
|
+
// its own and a `-c` token would change nothing about where that window lands.
|
|
654
448
|
const callerCwd = params.callerCwd === undefined || params.callerCwd === "" ? undefined : params.callerCwd;
|
|
655
449
|
const chosenCwd: { value: string; source: FreshCallCwdSource } | undefined =
|
|
656
450
|
requestedCwd !== undefined
|
|
@@ -739,10 +533,10 @@ export function freshCall(
|
|
|
739
533
|
prompt: buildFreshCallPrompt({
|
|
740
534
|
backend: params.backend,
|
|
741
535
|
task,
|
|
742
|
-
callerGardenId
|
|
536
|
+
callerGardenId,
|
|
743
537
|
nonce,
|
|
744
538
|
}),
|
|
745
|
-
bootstrapPayload: buildOmpBootstrapPayload({ callerGardenId
|
|
539
|
+
bootstrapPayload: buildOmpBootstrapPayload({ callerGardenId, nonce, task }),
|
|
746
540
|
};
|
|
747
541
|
const backendArgs = buildBackendArgs(params.backend, composition, model, env, cwd);
|
|
748
542
|
// THE LAUNCH-DIRECTORY NOTE, AND IT IS A DIAGNOSTIC RATHER THAN A GATE. `[측정 2026-09-16]` a
|