@junghanacs/entwurf 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/BASELINE.md +3 -1
- package/CHANGELOG.md +278 -0
- package/DELIVERY.md +156 -26
- package/README.md +64 -13
- package/VERIFY.md +67 -11
- package/docs/external-mcp-host.md +16 -6
- package/docs/setup-clean-host.md +63 -22
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +40 -16
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +194 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +160 -25
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
- package/mcp/entwurf-bridge/src/index.ts +50 -16
- package/mcp/entwurf-bridge/tsconfig.build.json +9 -0
- package/package.json +2 -2
- package/pi-extensions/entwurf-control.ts +4 -4
- package/pi-extensions/lib/codex-caller-seat.ts +204 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +218 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
- package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
- package/pi-extensions/lib/mux-fresh-call.ts +196 -37
- package/run.sh +127 -3
- package/scripts/check-codex-app-server-launch.ts +445 -0
- package/scripts/check-entwurf-v2-production.ts +42 -1
- package/scripts/check-entwurf-v2-send.ts +26 -7
- package/scripts/check-gate-qualification.ts +4 -2
- package/scripts/check-mux-launch-tmux.ts +331 -35
- package/scripts/codex-app-server-launch.sh +275 -0
- package/scripts/codex-socket-path.ts +33 -0
- package/scripts/codex-terminal-title-config.py +500 -0
- package/scripts/codex_toml_io.py +121 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +11 -3
- package/scripts/lib/codex-fresh-source-receipts.ts +29 -2
- package/scripts/mutants/codex-app-server-launch.json +157 -0
- package/scripts/mutants/codex-caller-seat.json +336 -0
- package/scripts/mutants/codex-native.json +3 -3
- package/scripts/mutants/mux-fresh-call.json +86 -14
- package/scripts/mutants/v2-surface.json +22 -0
- package/scripts/smoke-codex-config-state.sh +192 -3
- package/scripts/smoke-codex-fresh-live.ts +277 -37
- package/scripts/smoke-entwurf-chain-live.ts +50 -0
- package/scripts/smoke-setup-verdict.sh +13 -11
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* mux-fresh-call — open ONE visible sibling in the caller's own tmux server (its own session by
|
|
3
3
|
* default, or one named existing session on that server since #105), hand it its first task in
|
|
4
|
-
* the launch argv, and let it name itself back to the caller. Codex is the one measured
|
|
5
|
-
* exception
|
|
4
|
+
* the launch argv, and let it name itself back to the caller. A Codex CALLER is the one measured
|
|
5
|
+
* exception to "the caller's own session": it opens beside its own TUI pane, found by that
|
|
6
|
+
* pane's terminal title (#95 lane B), never by a TUI guess.
|
|
6
7
|
*
|
|
7
8
|
* ── Why this is a third module and not a parameter on the leaf ──
|
|
8
9
|
*
|
|
@@ -39,6 +40,14 @@
|
|
|
39
40
|
* byte-identical to the pre-#73 shape. Anything else is taken LITERALLY — no trim, no
|
|
40
41
|
* realpath, no project-name resolution, no store/peers/record lookup. The caller is the
|
|
41
42
|
* only cwd authority this module knows.
|
|
43
|
+
* - when a CODEX caller omits it, the surface supplies that citizen's own record cwd and it
|
|
44
|
+
* becomes an explicit `-c` (#95 lane C). This is not a second authority: it is the same
|
|
45
|
+
* caller, named more precisely, because a codex caller's PROCESS directory is the
|
|
46
|
+
* operator-owned app-server's rather than its own. Every other caller keeps the inherited
|
|
47
|
+
* directory and an unchanged argv.
|
|
48
|
+
* - the chosen value reaches codex TWICE, and that is one value with two carriers rather than
|
|
49
|
+
* two inputs: tmux `-c` places the pane, and codex `-C` places the THREAD, which a
|
|
50
|
+
* `--remote` attachment would otherwise take from the app-server (see `buildBackendArgs`).
|
|
42
51
|
* - the value is classified by the shared `classify-tmux-cwd.ts` leaf BEFORE any mutation
|
|
43
52
|
* (same four stable reasons as resume; the measured tmux 3.6a facts live on that leaf).
|
|
44
53
|
* This module's hints phrase them as the REQUESTED cwd; resume's say RECORDED.
|
|
@@ -79,7 +88,19 @@
|
|
|
79
88
|
|
|
80
89
|
import { randomBytes } from "node:crypto";
|
|
81
90
|
import { classifyTmuxCwd, type TmuxCwdRejectReason } from "./classify-tmux-cwd.ts";
|
|
82
|
-
import {
|
|
91
|
+
import {
|
|
92
|
+
CODEX_CALLER_SEAT_HINT,
|
|
93
|
+
type CodexCallerSeatRejectReason,
|
|
94
|
+
resolveCodexCallerSeat,
|
|
95
|
+
} from "./codex-caller-seat.ts";
|
|
96
|
+
import {
|
|
97
|
+
CODEX_CALLER_PREFLIGHT_HINT,
|
|
98
|
+
CODEX_LAUNCH_CWD_PREFLIGHT_HINT,
|
|
99
|
+
CODEX_PREFLIGHT_HINT,
|
|
100
|
+
type CodexCallerPreflightRejectReason,
|
|
101
|
+
type CodexPreflightRejectReason,
|
|
102
|
+
codexLaunchCwdFreshPreflight,
|
|
103
|
+
} from "./codex-fresh-preflight.ts";
|
|
83
104
|
import {
|
|
84
105
|
COPILOT_PREFLIGHT_HINT,
|
|
85
106
|
type CopilotPreflightRejectReason,
|
|
@@ -276,12 +297,37 @@ export function isSafeFreshCallModel(model: string): boolean {
|
|
|
276
297
|
* have needed its own quoting, its own lifetime and its own refusal rules; argv already owns
|
|
277
298
|
* all three. This is deliberately NOT a general `--flag value` passthrough — an arbitrary
|
|
278
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.
|
|
279
320
|
*/
|
|
280
321
|
export function buildBackendArgs(
|
|
281
322
|
backend: FreshCallBackend,
|
|
282
323
|
composition: FreshCallComposition,
|
|
283
324
|
model: string,
|
|
284
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(),
|
|
285
331
|
): string[] {
|
|
286
332
|
switch (backend) {
|
|
287
333
|
case "pi":
|
|
@@ -296,6 +342,8 @@ export function buildBackendArgs(
|
|
|
296
342
|
return [
|
|
297
343
|
"--remote",
|
|
298
344
|
`unix://${resolveCodexDefaultSocketPath(env)}`,
|
|
345
|
+
"-C",
|
|
346
|
+
launchCwd,
|
|
299
347
|
"--model",
|
|
300
348
|
model,
|
|
301
349
|
"--dangerously-bypass-approvals-and-sandbox",
|
|
@@ -391,6 +439,8 @@ export type FreshCallRejectReason =
|
|
|
391
439
|
| CopilotPreflightRejectReason
|
|
392
440
|
| OmpPreflightRejectReason
|
|
393
441
|
| CodexPreflightRejectReason
|
|
442
|
+
| CodexCallerPreflightRejectReason
|
|
443
|
+
| CodexCallerSeatRejectReason
|
|
394
444
|
| "caller-identity-unavailable"
|
|
395
445
|
| "model-empty"
|
|
396
446
|
| "model-invalid"
|
|
@@ -405,25 +455,41 @@ export interface FreshCallPlacement {
|
|
|
405
455
|
tmuxSession: string;
|
|
406
456
|
}
|
|
407
457
|
|
|
408
|
-
/**
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
* convention, not an address axis — `threadId` remains the native delivery address.
|
|
458
|
+
/** Which rule chose the start directory. Mirrors `FreshCallSeatSource` deliberately: one axis,
|
|
459
|
+
* one NAMED source, and no third value that means "we worked it out". `codex-caller-record` is
|
|
460
|
+
* the codex caller's own record cwd, consulted only when the caller requested none (#95 lane C).
|
|
412
461
|
*/
|
|
413
|
-
export
|
|
414
|
-
|
|
462
|
+
export type FreshCallCwdSource = "requested" | "codex-caller-record";
|
|
463
|
+
|
|
464
|
+
export type FreshCallSeatSource = "requested" | "codex-title-anchor";
|
|
415
465
|
export interface FreshCallSeat {
|
|
416
466
|
tmuxSession: string;
|
|
417
467
|
source: FreshCallSeatSource;
|
|
418
468
|
}
|
|
419
469
|
|
|
420
|
-
/**
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
470
|
+
/**
|
|
471
|
+
* WHICH RULE PICKS THE TARGET SESSION. Three in strict order, and the order is the contract:
|
|
472
|
+
*
|
|
473
|
+
* 1. an explicit `placement` — the expert override, and it wins over everything below.
|
|
474
|
+
* 2. a CODEX CALLER with placement omitted — the sibling opens beside the caller's own TUI
|
|
475
|
+
* pane, found by the title anchor (#95 lane B). This one is NOT a named seat and is
|
|
476
|
+
* therefore not decided here: it resolves to a native `$id` with no name in between, so
|
|
477
|
+
* `freshCall` owns it directly (see the anchor step below) and this function answers
|
|
478
|
+
* `null` so the name grammar and the name→id lookup stay out of a path that has neither.
|
|
479
|
+
* 3. anything else — the caller's own session, with no named seat at all.
|
|
480
|
+
*
|
|
481
|
+
* THE SEAT FOLLOWS THE CALLER, NEVER THE BACKEND BEING OPENED. #95 first shipped a fourth rule —
|
|
482
|
+
* an omitted-placement Codex TARGET selected a fixed existing session named `codex`, where the
|
|
483
|
+
* operator seated the app-server and their TUIs — and #95 D1 retired it (GLG, 2026-09-16). It
|
|
484
|
+
* was a workaround for a mapping that did not exist yet: nothing could find the pane a Codex
|
|
485
|
+
* caller was sitting in, so the operator was asked to keep every Codex in one known room. Rule 2
|
|
486
|
+
* is that mapping, so the room is no longer load-bearing, and keeping it would have meant Codex
|
|
487
|
+
* alone answering "where does a sibling open?" differently from every other backend. The
|
|
488
|
+
* operator-owned app-server keeps its own seat wherever the operator puts it; Entwurf still
|
|
489
|
+
* never creates, moves or supervises it.
|
|
490
|
+
*/
|
|
491
|
+
export function selectFreshCallSeat(placement: FreshCallPlacement | undefined): FreshCallSeat | null {
|
|
425
492
|
if (placement !== undefined) return { tmuxSession: placement.tmuxSession, source: "requested" };
|
|
426
|
-
if (backend === "codex") return { tmuxSession: CODEX_HOME_TMUX_SESSION, source: "codex-home" };
|
|
427
493
|
return null;
|
|
428
494
|
}
|
|
429
495
|
|
|
@@ -433,14 +499,21 @@ export function selectFreshCallSeat(
|
|
|
433
499
|
export interface FreshCallReceipt extends WindowHandle {
|
|
434
500
|
backend: FreshCallBackend;
|
|
435
501
|
model: string;
|
|
436
|
-
/** The
|
|
437
|
-
*
|
|
438
|
-
*
|
|
502
|
+
/** The start directory this launch CHOSE — present only when one was chosen: the caller's
|
|
503
|
+
* requested cwd, or a codex caller's own record cwd when it requested none. The same kind of
|
|
504
|
+
* fact as `runtimePath`: what tmux was asked for, never an observation of where the pane
|
|
505
|
+
* landed. ABSENT means no directory was named at all and the pane inherits this process's —
|
|
506
|
+
* that inheritance is not invented into a receipt field here. */
|
|
439
507
|
cwd?: string;
|
|
440
|
-
/**
|
|
441
|
-
|
|
508
|
+
/** Which rule chose `cwd`. Present exactly when `cwd` is. */
|
|
509
|
+
cwdSource?: FreshCallCwdSource;
|
|
510
|
+
/** The caller-REQUESTED session name. The RESOLVED target is the inherited `sessionId`, which
|
|
511
|
+
* is the session the window is actually in. Absent for `codex-title-anchor`, which has no
|
|
512
|
+
* name to echo: a caller's own pane was OBSERVED, not requested by name, and inventing one
|
|
513
|
+
* here would report a seat the caller never named. */
|
|
442
514
|
tmuxSession?: string;
|
|
443
|
-
/**
|
|
515
|
+
/** Which rule selected the target session. Absent exactly when the caller's own session was
|
|
516
|
+
* used with no seat rule at all. */
|
|
444
517
|
tmuxSessionSource?: FreshCallSeatSource;
|
|
445
518
|
runtimePath: string;
|
|
446
519
|
nonce: string;
|
|
@@ -531,6 +604,20 @@ export function buildFreshCallArgs(
|
|
|
531
604
|
* `callerGardenId` is supplied by the SURFACE that registered this tool, from its own
|
|
532
605
|
* record-backed context. It is not a tool parameter and this module never derives, validates
|
|
533
606
|
* against a store, or guesses it: an empty value is a named refusal, not a lookup.
|
|
607
|
+
*
|
|
608
|
+
* `callerNativeSessionId` is supplied by the same surface under the same rule, and its PRESENCE
|
|
609
|
+
* is the whole signal: it is set exactly when the reconciled sender is a record-backed codex
|
|
610
|
+
* citizen, and it carries that citizen's `nativeSessionId` (the `_meta.threadId` the vendor put
|
|
611
|
+
* on this very request). This module never resolves it, never reads `_meta`, and never asks a
|
|
612
|
+
* store who is calling — it only turns a thread id into a pane, and only for placement.
|
|
613
|
+
*
|
|
614
|
+
* `callerCwd` rides the same surface rule and the same condition: it is that codex citizen's
|
|
615
|
+
* RECORD cwd, and it exists because a codex caller's process directory is NOT its own (the
|
|
616
|
+
* bridge runs as a child of the operator-owned app-server, so the directory that process
|
|
617
|
+
* reports is the app-server's — #95 lane C §2). It is consulted only when the caller requested
|
|
618
|
+
* no cwd, so an
|
|
619
|
+
* explicit request always wins, and this module never looks a cwd up, resolves it, or infers it
|
|
620
|
+
* from a seat, a workspace map or a project name.
|
|
534
621
|
*/
|
|
535
622
|
export function freshCall(
|
|
536
623
|
params: {
|
|
@@ -540,6 +627,8 @@ export function freshCall(
|
|
|
540
627
|
cwd?: string;
|
|
541
628
|
placement?: FreshCallPlacement;
|
|
542
629
|
callerGardenId: string | null;
|
|
630
|
+
callerNativeSessionId?: string;
|
|
631
|
+
callerCwd?: string;
|
|
543
632
|
},
|
|
544
633
|
env: NodeJS.ProcessEnv = process.env,
|
|
545
634
|
nonce: string = mintNonce(),
|
|
@@ -555,8 +644,27 @@ export function freshCall(
|
|
|
555
644
|
if (task.length > TASK_MAX_CHARS) return { ok: false, reason: "task-too-long" };
|
|
556
645
|
// ONLY `undefined` and the exact empty string mean "no cwd". Everything else is the literal
|
|
557
646
|
// value — deliberately untrimmed, so a whitespace-mangled path is refused loudly by the
|
|
558
|
-
// classification below instead of being silently repaired into a different directory.
|
|
559
|
-
|
|
647
|
+
// classification below instead of being silently repaired into a different directory. The
|
|
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;
|
|
654
|
+
const callerCwd = params.callerCwd === undefined || params.callerCwd === "" ? undefined : params.callerCwd;
|
|
655
|
+
const chosenCwd: { value: string; source: FreshCallCwdSource } | undefined =
|
|
656
|
+
requestedCwd !== undefined
|
|
657
|
+
? { value: requestedCwd, source: "requested" }
|
|
658
|
+
: callerCwd !== undefined
|
|
659
|
+
? { value: callerCwd, source: "codex-caller-record" }
|
|
660
|
+
: undefined;
|
|
661
|
+
const cwd = chosenCwd?.value;
|
|
662
|
+
// SEAM (#95 lane C): a caller-record directory is classified by the SAME shared leaf and
|
|
663
|
+
// answers the same four `cwd-*` reasons, whose hint text says REQUESTED. When a codex
|
|
664
|
+
// caller's recorded directory has since been deleted, the repair that hint points at is
|
|
665
|
+
// still the right one — that directory does not exist — but the noun belongs to the caller
|
|
666
|
+
// rather than to the request. Kept shared on purpose: doubling the reason set for a wording
|
|
667
|
+
// difference would double the refusal contract two surfaces and one leaf already agree on.
|
|
560
668
|
if (cwd !== undefined) {
|
|
561
669
|
const badCwd = classifyTmuxCwd(cwd);
|
|
562
670
|
if (badCwd) return { ok: false, reason: badCwd };
|
|
@@ -565,9 +673,10 @@ export function freshCall(
|
|
|
565
673
|
// without tmux, so an unresolvable name is answered before anything else runs. Whether that
|
|
566
674
|
// session EXISTS is a tmux question and is asked below, after the caller's own context is
|
|
567
675
|
// proven — a name check that needed a live server would refuse for the wrong reason on a
|
|
568
|
-
// host with no tmux at all.
|
|
569
|
-
//
|
|
570
|
-
|
|
676
|
+
// host with no tmux at all. An explicit seat is an expert override and is reported as such; a
|
|
677
|
+
// CODEX CALLER's own pane is deliberately not a name at all, so it is absent here and
|
|
678
|
+
// resolved after the context proof below — see `selectFreshCallSeat` for the three-rule order.
|
|
679
|
+
const selectedSeat = selectFreshCallSeat(params.placement);
|
|
571
680
|
const seat = selectedSeat?.tmuxSession;
|
|
572
681
|
if (seat !== undefined) {
|
|
573
682
|
const badSeat = classifyTmuxSessionName(seat);
|
|
@@ -606,10 +715,24 @@ export function freshCall(
|
|
|
606
715
|
// native id continues; the name does not travel past this line. STILL PRE-MUTATION: an
|
|
607
716
|
// absent seat refuses with no window anywhere.
|
|
608
717
|
let targetSessionId = placement.sessionId;
|
|
718
|
+
let anchoredSeat = false;
|
|
609
719
|
if (seat !== undefined) {
|
|
610
720
|
const resolved = resolveTmuxSessionId(seat, (args) => runTmux(args, env));
|
|
611
721
|
if (!resolved.ok) return { ok: false, reason: resolved.reason };
|
|
612
722
|
targetSessionId = resolved.sessionId;
|
|
723
|
+
} else if (params.placement === undefined && params.callerNativeSessionId !== undefined) {
|
|
724
|
+
// Rule 2: the caller is a codex citizen and named no seat, so the sibling belongs beside
|
|
725
|
+
// the caller's own TUI. The pane is found by the title anchor and ONLY its `$session`
|
|
726
|
+
// continues — a pane title is forgeable, so it may never become an address, a liveness
|
|
727
|
+
// claim or a delivery input (Hard Rule 16). `params.placement` is re-read here rather
|
|
728
|
+
// than inferred from `seat === undefined`: "an explicit seat always wins" is the one
|
|
729
|
+
// invariant a later edit must not be able to lose by accident.
|
|
730
|
+
// STILL PRE-MUTATION: 0 or 2+ matching panes refuse with no window anywhere and no
|
|
731
|
+
// fallback to any other session.
|
|
732
|
+
const anchor = resolveCodexCallerSeat(params.callerNativeSessionId, (args) => runTmux(args, env));
|
|
733
|
+
if (!anchor.ok) return { ok: false, reason: anchor.reason };
|
|
734
|
+
targetSessionId = anchor.seat.sessionId;
|
|
735
|
+
anchoredSeat = true;
|
|
613
736
|
}
|
|
614
737
|
|
|
615
738
|
const composition: FreshCallComposition = {
|
|
@@ -621,10 +744,38 @@ export function freshCall(
|
|
|
621
744
|
}),
|
|
622
745
|
bootstrapPayload: buildOmpBootstrapPayload({ callerGardenId: params.callerGardenId, nonce, task }),
|
|
623
746
|
};
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
747
|
+
const backendArgs = buildBackendArgs(params.backend, composition, model, env, cwd);
|
|
748
|
+
// THE LAUNCH-DIRECTORY NOTE, AND IT IS A DIAGNOSTIC RATHER THAN A GATE. `[측정 2026-09-16]` a
|
|
749
|
+
// Codex sibling opened into a directory this Codex has no answer for stops on the vendor's
|
|
750
|
+
// folder-consent screen: no first turn, no rollout, no callback. It is tempting to refuse
|
|
751
|
+
// that, and refusing is the wrong product. The consent screen is SELF-REPAIRING when a human
|
|
752
|
+
// is there — one answer and the vendor records the directory, so every later launch runs —
|
|
753
|
+
// and an operator at the keyboard is exactly who a visible-first rail is built for. A refusal
|
|
754
|
+
// would replace that one answer with "no window, go run codex yourself, then call again", and
|
|
755
|
+
// it would have to be right about a decision this process cannot fully see (the vendor merges
|
|
756
|
+
// system, managed and cloud layers around the file this leaf reads). So the launch proceeds
|
|
757
|
+
// and says what it saw.
|
|
758
|
+
//
|
|
759
|
+
// The UNATTENDED case is not answered here and must not be: a gate with nobody at the keyboard
|
|
760
|
+
// needs its precondition named before it spends a model turn, which is its own oracle's job —
|
|
761
|
+
// `smoke-codex-fresh-live` asserts this same leaf up front, so a missing answer reads as a
|
|
762
|
+
// named precondition instead of a callback timeout.
|
|
763
|
+
//
|
|
764
|
+
// The directory asked about is READ BACK off codex's own `-C` token rather than recomputed:
|
|
765
|
+
// one resolution, one authority, and no way for the note to name a directory the thread will
|
|
766
|
+
// not start in (this module is deliberately not allowed to resolve the inherited default a
|
|
767
|
+
// second time — `FRESHCALL-CWD-CALLER-ONLY`).
|
|
768
|
+
if (params.backend === "codex") {
|
|
769
|
+
const at = backendArgs.indexOf("-C");
|
|
770
|
+
const launchCwd = backendArgs[at + 1] ?? "";
|
|
771
|
+
const unanswered = codexLaunchCwdFreshPreflight(env, launchCwd);
|
|
772
|
+
if (unanswered) {
|
|
773
|
+
console.error(
|
|
774
|
+
`[fresh-call] ${unanswered}: ${launchCwd}\n` + ` ${CODEX_LAUNCH_CWD_PREFLIGHT_HINT[unanswered]}`,
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const run = runTmux(buildFreshCallArgs(targetSessionId, runtimePath, backendArgs, cwd), env);
|
|
628
779
|
assertTmuxOk("new-window", run);
|
|
629
780
|
|
|
630
781
|
let fields: ReturnType<typeof parseWindowFields>;
|
|
@@ -649,9 +800,11 @@ export function freshCall(
|
|
|
649
800
|
...fields,
|
|
650
801
|
backend: params.backend,
|
|
651
802
|
model,
|
|
652
|
-
...(
|
|
803
|
+
...(chosenCwd === undefined ? {} : { cwd: chosenCwd.value, cwdSource: chosenCwd.source }),
|
|
653
804
|
...(selectedSeat === null
|
|
654
|
-
?
|
|
805
|
+
? anchoredSeat
|
|
806
|
+
? { tmuxSessionSource: "codex-title-anchor" as const }
|
|
807
|
+
: {}
|
|
655
808
|
: { tmuxSession: selectedSeat.tmuxSession, tmuxSessionSource: selectedSeat.source }),
|
|
656
809
|
runtimePath,
|
|
657
810
|
nonce,
|
|
@@ -667,6 +820,8 @@ const REJECT_HINT: Record<FreshCallRejectReason, string> = {
|
|
|
667
820
|
...COPILOT_PREFLIGHT_HINT,
|
|
668
821
|
...OMP_PREFLIGHT_HINT,
|
|
669
822
|
...CODEX_PREFLIGHT_HINT,
|
|
823
|
+
...CODEX_CALLER_PREFLIGHT_HINT,
|
|
824
|
+
...CODEX_CALLER_SEAT_HINT,
|
|
670
825
|
"no-tmux-context": "this agent is not running inside tmux, so there is no session to open a sibling beside",
|
|
671
826
|
"anchor-malformed": "TMUX_PANE is not a native pane id",
|
|
672
827
|
"anchor-unresolved": "tmux resolved no pane for this agent's anchor",
|
|
@@ -683,7 +838,7 @@ const REJECT_HINT: Record<FreshCallRejectReason, string> = {
|
|
|
683
838
|
"tmux-session-name-invalid":
|
|
684
839
|
"the requested tmux session name is outside the shape this rail addresses (start with a letter or digit, then letters, digits, '_' or '-') — some other shapes tmux cannot resolve at all ('#' is expanded when the name is stored; '.' and ':' are its own pane/window separators inside a target; a name like '$0' loses to the session id '$0'), and the rest are declined to keep one narrow grammar, so rename the session or open one whose name fits",
|
|
685
840
|
"tmux-session-missing":
|
|
686
|
-
"no session with that exact name answers on this agent's tmux server (or that server stopped answering) — nothing was created, so open the session yourself and call again
|
|
841
|
+
"no session with that exact name answers on this agent's tmux server (or that server stopped answering) — nothing was created, so open the session yourself and call again",
|
|
687
842
|
"model-empty": "model is empty after trimming; fresh calls require an explicit model",
|
|
688
843
|
"model-invalid": `model must be one ${MODEL_MAX_CHARS}-character argv-safe id/alias without whitespace or tmux syntax`,
|
|
689
844
|
"task-empty": "task is empty after trimming",
|
|
@@ -718,11 +873,15 @@ export function renderFreshCall(result: FreshCallResult): { text: string; isErro
|
|
|
718
873
|
`[entwurf fresh call →]\n` +
|
|
719
874
|
` backend: ${r.backend} (${r.runtimePath})\n` +
|
|
720
875
|
` model: ${r.model} (requested on the runtime CLI)\n` +
|
|
721
|
-
(r.cwd === undefined
|
|
722
|
-
|
|
876
|
+
(r.cwd === undefined
|
|
877
|
+
? ""
|
|
878
|
+
: r.cwdSource === "codex-caller-record"
|
|
879
|
+
? ` cwd: ${r.cwd} (the Codex caller's own record directory, used because no cwd was requested — not an observation)\n`
|
|
880
|
+
: ` cwd: ${r.cwd} (requested start directory — not an observation)\n`) +
|
|
881
|
+
(r.tmuxSessionSource === undefined
|
|
723
882
|
? ""
|
|
724
|
-
: r.tmuxSessionSource === "codex-
|
|
725
|
-
? ` seat: ${r.
|
|
883
|
+
: r.tmuxSessionSource === "codex-title-anchor"
|
|
884
|
+
? ` seat: ${r.sessionId} (the Codex caller's own pane, found by its thread-id terminal title — an OBSERVED session, not a requested name)\n`
|
|
726
885
|
: ` seat: ${r.tmuxSession} (requested tmux session, resolved to ${r.sessionId})\n`) +
|
|
727
886
|
` window: ${r.windowId} (index ${r.windowIndex}) in session ${r.sessionId}\n` +
|
|
728
887
|
` pane: ${r.paneId} pid ${r.panePid}\n` +
|
package/run.sh
CHANGED
|
@@ -120,7 +120,7 @@ usage() {
|
|
|
120
120
|
cat <<'EOF'
|
|
121
121
|
Usage:
|
|
122
122
|
./run.sh setup [project-dir] # ONE presence-driven composition (#86): per-component PASS/SKIP/FAIL for pi/claude/agy/copilot/omp/codex + stable dev bins + v2 install smoke. Codex birth/MCP/status-line atoms are composed at user scope; setup stays non-green until the operator trusts the birth declaration once in visible Codex. Absent harness = zero-state SKIP, detected-incomplete = named FAIL + nonzero exit. Never installs a harness, starts a daemon, touches credentials, or writes the vendor trust receipt
|
|
123
|
-
./run.sh release-gate [project-dir] [--cut] [--allow-skip-gemini] # SINGLE release gate: full static (pnpm run check:full) + the v2-native live gates (v2 matrix-live, check-bridge, doctor-pi-provider, RGG) + the ACP plugin acceptance floor (12 LIVE smokes: socket-citizen/raw-turn/overlay/provider/session-reuse/carrier-augment/memory-containment/rgg/mcp/skill/bundled-mcp/v2-send) + the one surviving axis the aggregate used to omit silently (claude-native-resume; Cortex stays a documented on-demand direct call) + the cross-harness delivery chain (smoke-entwurf-chain-live) + the Codex
|
|
123
|
+
./run.sh release-gate [project-dir] [--cut] [--allow-skip-gemini] # SINGLE release gate: full static (pnpm run check:full) + the v2-native live gates (v2 matrix-live, check-bridge, doctor-pi-provider, RGG) + the ACP plugin acceptance floor (12 LIVE smokes: socket-citizen/raw-turn/overlay/provider/session-reuse/carrier-augment/memory-containment/rgg/mcp/skill/bundled-mcp/v2-send) + the one surviving axis the aggregate used to omit silently (claude-native-resume; Cortex stays a documented on-demand direct call) + the cross-harness delivery chain (smoke-entwurf-chain-live) + the Codex Pi -> visible Codex -> visible Pi receipt chain (smoke-codex-fresh-live; initial Pi and the Codex it opens in one session S, explicit operator-owned app-server PID in a DIFFERENT session — that separation is the lane B claim). TWO-TIER summary: MUST (release-blocking, owns the exit code — "green" applies here) + BEHAVIOR (advisory, non-blocking: RGG positives model-in-loop turn). STEP OUTCOME protocol: every step is INVOKED and reports its own PASS / SKIP (exit 97, a prerequisite it does not have) / FAIL — a skip is never counted as a pass. Without --cut this is the unattended diagnostic (SKIPs reported, exit 0). WITH --cut it is read as release acceptance and ANY MUST SKIP is red, which is what makes "a CUT needs LIVE=1, SKIP=0" executable instead of prose. --allow-skip-gemini accepted-but-ignored (back-compat). final cut authorization is GLG's.
|
|
124
124
|
./run.sh check-bridge # entwurf-bridge direct MCP smoke + protocol/negative-path test.sh (live substrate = v2 live smokes)
|
|
125
125
|
./run.sh check-entwurf-bridge-boot # deterministic gate (5d-5-pre, G1a/G1b/G1e/G1f, IN pnpm run check:full): boot start.sh under strip-types + assert v2 fence graph loads + entwurf_v2 and entwurf_resume_call registered/schema + the tools/list surface is EXACTLY the seven shipped garden verbs; tools/list only, no auth/side-effect
|
|
126
126
|
./run.sh check-entwurf-bridge-pi-free # deterministic gate (0.12.1 A, IN pnpm check): static — bridge index eager value-import closure must carry no @earendil-works/pi-* (type-only + dynamic import excluded); proves the meta-bridge boots pi-free
|
|
@@ -134,6 +134,7 @@ Usage:
|
|
|
134
134
|
./run.sh check-omp-birth-hook # #87 gate: drives the real OMP assembler into a temp dir, imports the ASSEMBLED index.ts into a MOCK omp host and fires session_start/session_switch. tui mints one backend:"omp" record + a sender marker keyed to the host's OWN pid (the one-process join) + the garden id on the status line; print/rpc/json mint NOTHING (hasUI is true on the rpc rows, as in the vendor); switch attaches on the same native id and mints the replacement on a new one; the CERTIFIED receiver reader still finds no marker. Also the four-root joint binding: extension and omp-labeled bridge child resolve the same sessions/mailbox/senders/receivers bundle against test-built literals (never the production resolver as its own oracle), under a poisoned PI_CODING_AGENT_DIR and under four distinct ENTWURF_META_* overrides; the override grammar is absolute-or-~ and both halves plus the doctor refuse anything else by name; and a drifted provenance label selects no root policy at all. Hermetic; no omp, no model turn
|
|
135
135
|
./run.sh check-copilot-receive-arm # #82 RAIL 5 gate: the REAL receiver installer + the REAL extension.mjs forked with a stubbed SDK. Arms only after birth, marker owned by the WATCHER pid, self-fetch dispatch answer, doorbell carries the garden id and NOT the body, id-drift/foreign-parent refusals. Hermetic; no Copilot, no model turn
|
|
136
136
|
./run.sh check-copilot-launch # #82 RAIL 7 gate: the MANAGED launch `entwurf copilot`, driven through its public address against a FAKE VENDOR on a sandbox PATH. Receiver precondition refusals, EXTENSIONS token + operator token preservation, injected defaults before the `--` terminator, byte-identical argv, the 11 explicit permission/surface policy overrides that suppress `--yolo`, exec (not fork) pid identity, exit passthrough, recursion refusal. Hermetic; no Copilot, no model turn
|
|
137
|
+
./run.sh check-codex-app-server-launch # #95 gate: the MANAGED app-server launch `entwurf codex-app-server`, driven through its public address against a FAKE VENDOR on a sandbox PATH. The injected `app-server --listen unix://<socket>` required to equal the product's own resolveCodexDefaultSocketPath over an env matrix that includes the inputs a bash transcription was MEASURED to diverge on (BOM-only CODEX_HOME, trailing slash, `..`), non-absolute and control-character addresses refused before any write, byte-identical operator argv, exec (not fork) parent identity, exit passthrough, live-socket and indeterminate-socket and second---listen and recursion refusals, dead-socket launch-over, the tmux fact line, and both identity carriers stripped. Hermetic; no Codex, no app-server, no model turn
|
|
137
138
|
./run.sh check-copilot-statusline # #82 Copilot custom-footer renderer. session_id → ready/?/gid + rail `cop`; exit 0. IN pnpm check. No Copilot, no model turn
|
|
138
139
|
./run.sh check-entwurf-capabilities # deterministic gate (0.11 Stage 0 step 3C): backend capability registry (pi/entwurf-capabilities.json) — coverage==META_CITIZEN_BACKENDS + agrees with live META_BACKEND_DESCRIPTORS + strict keyset, no API
|
|
139
140
|
./run.sh check-omp-fresh-preflight # #87 C: the OMP fresh preflight reproduces omp_agent_dir and the tools.xdev read in TS (it runs from two emit depths and cannot call a sibling script). This drives the SHIPPED shell/python leaves over the same inputs — refusals included — and requires the TS half to agree, so the reproduction cannot silently drift from the installer's own oracle
|
|
@@ -175,7 +176,7 @@ Usage:
|
|
|
175
176
|
./run.sh smoke-entwurf-v2-matrix-live # LIVE sentinel (0.11 Stage 0 step 5d-5, D4-b) — OUT of pnpm check, needs LIVE=1. Drives REAL production runEntwurfV2 deps over REAL OS objects, 4 cells: C1 control-socket (real pi --entwurf-control resident → RPC send → lock acquire→release ×1), C1b record-less socket (#50 C4: live record-less pi → EVERY intent rejected pre-probe record-less-socket, no lock, rendered hint names record authority + fresh-cut), C2 meta-mailbox deliverable (armed self-fetch citizen → real .msg enqueue, lock-free), C3 meta-mailbox guard (no armed receiver → reject, no garbage). Model-in-loop OUT (transport/lock/enqueue gate, GPT Q2); negative/timeout stay deterministic. Model: ENTWURF_LIVE_TARGET=<provider>/<model> (default openai-codex/gpt-5.6-luna). LIVE=1 ./run.sh smoke-entwurf-v2-matrix-live
|
|
176
177
|
./run.sh smoke-agy-native-push-live # 봉인 8 LIVE acceptance for the native-push (agy) rail — OUT of pnpm check, needs LIVE=1 + AGY_CONVERSATION_ID (a live agy conversation). Drives the REAL antigravity adapter + register core + runEntwurfV2 (production deps): doctor-static preflight (dangling→FAIL, the ③ gate), probe route, register create/attach idempotency, fire→native-push delivered, post-send re-probe (D7 partial), bogus-conv→native-push-probe-indeterminate. Meta-store isolated to a temp dir (only the agy round-trip is real; no real-store residue). COST FENCE: open that agy conversation on gemini-3.6-flash (free account) — never a Pro tier; entwurf never selects the agy model and no assertion reads it. LIVE=1 AGY_CONVERSATION_ID=<convId> ./run.sh smoke-agy-native-push-live
|
|
177
178
|
./run.sh smoke-codex-native-push-live # on-demand LIVE acceptance for Codex native-push — needs LIVE=1 + CODEX_LIVE_THREAD_ID loaded by a visible TUI attached to the default app-server; probes the real UDS, routes through production runEntwurfV2, queues one exact-token turn without retry, re-probes, and leaves the visible TUI to show the model result
|
|
178
|
-
./run.sh smoke-codex-fresh-live # #95
|
|
179
|
+
./run.sh smoke-codex-fresh-live # #95 RELEASE MUST: record-backed receipt fixture -> PUBLIC initial Pi fresh/callback in a session S that is NOT the app-server's -> Pi PUBLIC omitted-placement Codex fresh/exact callback, also in S -> Pi-to-Codex addressed native-push v2 -> Codex PUBLIC omitted-placement AND omitted-cwd fresh Pi/exact callback IN S beside its own TUI (lane B claim: the app-server env names another session, so S can only come from the caller's pane title; lane C claim: no cwd was asked for, so the directory can only come from the Codex caller's own record — and the Codex thread's own rollout session_meta.cwd, its pane and its record must all be the requested scratch, not the app-server's repo) -> Codex final v2 evidence. Needs LIVE=1 + ENTWURF_CODEX_APP_SERVER_PID + ENTWURF_CODEX_FRESH_MODEL + ENTWURF_CODEX_FRESH_PI_MODEL, and its ONE stable launch directory answered 'Trust' once in a plain codex -C there: codex records its direct consent per exact directory, so a directory with no answer at all opens a consent screen instead of a first turn. Only THIS gate treats that as a precondition and fails up front as codex-launch-cwd-undecided rather than timing out on a callback — an ordinary entwurf_fresh_call only prints the note and opens the window, because a human can answer the screen. The smoke PRINTS that directory as 'launch-cwd <dir>' and repeats it in the failure as a ready-to-run repair command — do not retype it from memory, and do not write it as \$TMPDIR/... (os.tmpdir() falls back to /tmp when TMPDIR is unset, so that spelling names a different directory in the filesystem root). The PID names the operator-owned app-server, which must sit in a DIFFERENT session from S; the smoke checks/prints all four coordinates, never starts/stops/guesses the app-server/session, never reads screen text, interrupts only its exact still-running Codex turn, and cleans only receipt-named window ids
|
|
179
180
|
./run.sh smoke-mux-lifecycle-live # RELEASE MUST integrated LIVE lifecycle acceptance for mux, through the REAL MCP surface — OUT of pnpm check, needs LIVE=1 and spends model turns (two pi siblings: native + recorded-ACP provider, each resumed once; one Claude Code sibling). tools/call fresh_call -> nonce callback sender envelope -> v2 control send landing in the sibling's own transcript -> resume_call REFUSED while live (window count unchanged) -> stable-handle close (pane gone, socket dead, record kept) -> dormant delivery refused honestly -> public entwurf_resume_call with LAUNCH and OBSERVATION receipts kept apart, same-gid socket alive, zero new citizens, zero lock residue, resumed pane_start_path == RECORD cwd (separate tmux query), transcript byte-identical across the resume -> v2 recall of the pre-close fact. claude-code resume refused target-not-pi, no window opened and no lock residue. LIVE=1 ./run.sh smoke-mux-lifecycle-live
|
|
180
181
|
./run.sh check-entwurf-facts # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 1+2): PURE PeerFact core + resolveFactList union — R1 out-of-domain→unsupported, R3b socket-domain 4-value, facts-only keyset; union: PeerFact + RecordLessSocketFact by gardenId (#50 C4: record-less socket = diagnostic subject, gid+liveness only), dormant→dead, F3 indeterminate preserved, out-of-socket-domain+socket fail-loud; pure, no IO
|
|
181
182
|
./run.sh check-socket-discovery # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 3): SOCKET-axis scanSocketProbes — probes (dir sockets) ∪ (in-domain citizen canonical paths) 3-valued; dormant citizen no-file → dead (resumable, not unprobed), stall → indeterminate (F3), dir hygiene/dedup/missing-dir + e2e → resolveFactList; readdir/probe injected, no IO
|
|
@@ -259,6 +260,11 @@ Usage:
|
|
|
259
260
|
./run.sh install-codex-statusline # add thread-title to tui.status_line for visible garden ids
|
|
260
261
|
./run.sh uninstall-codex-statusline # remove only the recorded status-line atom
|
|
261
262
|
./run.sh doctor-codex-statusline # effective visible-identity config + ownership verdict
|
|
263
|
+
./run.sh install-codex-terminal-title # add thread-id to tui.terminal_title so the multiplexer reports the caller's thread in #{pane_title} (#95 placement input, never an address)
|
|
264
|
+
./run.sh uninstall-codex-terminal-title # remove only the recorded terminal-title atom
|
|
265
|
+
./run.sh doctor-codex-terminal-title # effective caller-seat config + ownership verdict
|
|
266
|
+
./run.sh codex-socket-path # #95 INTERNAL (the launcher asks this; you do not need it): print the Codex app-server default control-socket path for this environment, from the SAME resolver delivery and preflight read
|
|
267
|
+
./run.sh codex-app-server [args...] # #95: the ONE COMMAND that starts the operator's Codex app-server. exec()s `codex app-server --listen "unix://$CODEX_HOME/app-server-control/app-server-control.sock"` in THIS terminal (cwd/pid/exit are the vendor's, Ctrl-C is yours) — entwurf owns the SPELLING of that address, never the process's lifecycle: no supervisor, no restart, no daemon, no pid file. It does no path arithmetic: the address comes from `codex-socket-path`, the one leaf every other Codex surface reads, and a resolved address that is not absolute or carries a control character is refused before anything is created. Your arguments are forwarded byte-identical after it; a second --listen is refused by name. Refuses on a live socket (names the /proc owner it can read) or an indeterminate one, and reports which tmux server this seat gives caller-seat lookups. Run it in a DETACHED tmux session other than the one you work in
|
|
262
268
|
./run.sh install-agy-statusline # own the agy statusLine subtree with bare entwurf-agy-statusline; preserve unrelated settings
|
|
263
269
|
./run.sh uninstall-agy-statusline # honest inverse from statusline install-state
|
|
264
270
|
./run.sh doctor-agy-statusline # fail-loud statusLine config/bin/state doctor + honest live SKIP
|
|
@@ -604,6 +610,7 @@ _codex_home() { echo "${CODEX_HOME:-$HOME/.codex}"; }
|
|
|
604
610
|
_codex_config() { echo "$(_codex_home)/config.toml"; }
|
|
605
611
|
_codex_mcp_state() { echo "${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/codex-mcp/install-state.json"; }
|
|
606
612
|
_codex_statusline_state() { echo "${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/codex-statusline/install-state.json"; }
|
|
613
|
+
_codex_terminal_title_state() { echo "${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/codex-terminal-title/install-state.json"; }
|
|
607
614
|
|
|
608
615
|
codex_mcp() {
|
|
609
616
|
local verb="$1" config state
|
|
@@ -632,6 +639,23 @@ codex_statusline() {
|
|
|
632
639
|
esac
|
|
633
640
|
}
|
|
634
641
|
|
|
642
|
+
# #95 lane B: the SECOND visible-identity atom, and a different axis from the
|
|
643
|
+
# status line. `status_line`'s thread-title is what a HUMAN reads in the TUI;
|
|
644
|
+
# `terminal_title`'s thread-id is what the MULTIPLEXER reports back as
|
|
645
|
+
# `#{pane_title}`, which is the only value a Codex caller's `_meta.threadId`
|
|
646
|
+
# can be matched against to find the pane it is sitting in. Placement input
|
|
647
|
+
# only — never an address, delivery or liveness fact.
|
|
648
|
+
codex_terminal_title() {
|
|
649
|
+
local verb="$1" config state
|
|
650
|
+
config="$(_codex_config)"
|
|
651
|
+
state="$(_codex_terminal_title_state)"
|
|
652
|
+
case "$verb" in
|
|
653
|
+
install) python3 "$REPO_DIR/scripts/codex-terminal-title-config.py" install "$config" "$state" ;;
|
|
654
|
+
uninstall) python3 "$REPO_DIR/scripts/codex-terminal-title-config.py" uninstall "$state" ;;
|
|
655
|
+
doctor) python3 "$REPO_DIR/scripts/codex-terminal-title-config.py" doctor-static "$config" "$state" ;;
|
|
656
|
+
esac
|
|
657
|
+
}
|
|
658
|
+
|
|
635
659
|
register_user_scope_citizen() {
|
|
636
660
|
local agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
|
|
637
661
|
# Shared idempotent implementation (also driven by smoke-user-scope-citizen).
|
|
@@ -1026,6 +1050,14 @@ check_copilot_launch() {
|
|
|
1026
1050
|
run_ts scripts/check-copilot-launch.ts
|
|
1027
1051
|
}
|
|
1028
1052
|
|
|
1053
|
+
check_codex_app_server_launch() {
|
|
1054
|
+
# #95: the managed app-server SPELLING, proved against a fake vendor. There is no second
|
|
1055
|
+
# spelling of the address to compare any more — the launcher asks `codex-socket-path` — so
|
|
1056
|
+
# these cells are a WIRING oracle, and they carry the hostile inputs a transcription was
|
|
1057
|
+
# measured to diverge on so that re-deriving the path here goes red rather than passing.
|
|
1058
|
+
run_ts scripts/check-codex-app-server-launch.ts
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1029
1061
|
check_copilot_receive_arm() {
|
|
1030
1062
|
# #82 RAIL 5 gate: drives the REAL receiver installer into a temp extensions dir, then
|
|
1031
1063
|
# forks the REAL extension.mjs with the SDK specifier resolved by a loader hook (the
|
|
@@ -1414,7 +1446,10 @@ check_mux_fresh_call() {
|
|
|
1414
1446
|
# tmux-coordinate-row rides here because it is the parse every tmux COORDINATE in this lane's
|
|
1415
1447
|
# LIVE siblings depends on, and because it is the only half of a measured LIVE failure that a
|
|
1416
1448
|
# deterministic gate can own: the row is the input, so no tmux and no model turn is needed.
|
|
1417
|
-
|
|
1449
|
+
# codex-caller-seat rides here for the same reason as the two preflights: the leaf exists
|
|
1450
|
+
# only as freshCall's Codex placement input (#95 lane B), so certifying it apart from the
|
|
1451
|
+
# composition it feeds would let the two halves of one seat decision drift.
|
|
1452
|
+
run_vitest test/mux-fresh-call.test.ts test/copilot-fresh-preflight.test.ts test/codex-fresh-preflight.test.ts test/fresh-call-surfaces.contract.test.ts test/fresh-call-provider.contract.test.ts test/omp-fresh-bootstrap.contract.test.ts test/tmux-coordinate-row.test.ts test/codex-fresh-live-protocol.test.ts test/codex-caller-seat.test.ts
|
|
1418
1453
|
}
|
|
1419
1454
|
|
|
1420
1455
|
smoke_mux_fresh_call_live() {
|
|
@@ -4498,6 +4533,57 @@ JS
|
|
|
4498
4533
|
fi
|
|
4499
4534
|
echo "[check-pack-install] installed 'entwurf copilot' reached the vendor with the scan flag and the managed argv"
|
|
4500
4535
|
|
|
4536
|
+
# #95 — the managed app-server SPELLING, from the INSTALLED package. Same packaging risk as
|
|
4537
|
+
# the copilot cell above and the same reason a file-list assertion cannot stand in for it:
|
|
4538
|
+
# `scripts/codex-app-server-launch.sh` has to be in the tarball AND resolve its siblings
|
|
4539
|
+
# correctly once it lives under node_modules. What makes this cell worth its seconds is the
|
|
4540
|
+
# ADDRESS: this verb exists so a consumer never types that socket path, so the one thing an
|
|
4541
|
+
# installed run must prove is that the path it hands the vendor is the one the product's own
|
|
4542
|
+
# resolver computes — read here from the INSTALLED tree, never from this checkout.
|
|
4543
|
+
local cx_launch_home="$npm_tmp/codex-appserver-home" cx_launch_bin="$npm_tmp/codex-appserver-bin"
|
|
4544
|
+
mkdir -p "$cx_launch_home" "$cx_launch_bin"
|
|
4545
|
+
{
|
|
4546
|
+
printf '#!/usr/bin/env bash\n'
|
|
4547
|
+
printf 'for a in "$@"; do printf "ARG<%%s>\\n" "$a"; done\n'
|
|
4548
|
+
} > "$cx_launch_bin/codex"
|
|
4549
|
+
chmod +x "$cx_launch_bin/codex"
|
|
4550
|
+
# The expectation is read from the INSTALLED COMPILED resolver, not from this checkout and
|
|
4551
|
+
# not transcribed here: compiled JS is what an installed consumer surface reaches (Hard Rule
|
|
4552
|
+
# 11), so this asks the packed tree itself what address it believes in.
|
|
4553
|
+
local cx_resolver="$npm_pkg/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js" cx_want
|
|
4554
|
+
if [ ! -f "$cx_resolver" ]; then
|
|
4555
|
+
fail "[check-pack-install] the tarball carries no compiled Codex socket resolver at $cx_resolver"
|
|
4556
|
+
return 1
|
|
4557
|
+
fi
|
|
4558
|
+
if ! cx_want=$(CODEX_HOME="$cx_launch_home/.codex" node --input-type=module -e "
|
|
4559
|
+
import { resolveCodexDefaultSocketPath } from '$cx_resolver';
|
|
4560
|
+
process.stdout.write(resolveCodexDefaultSocketPath(process.env));
|
|
4561
|
+
" 2>&1); then
|
|
4562
|
+
fail "[check-pack-install] the INSTALLED compiled socket resolver would not load:"
|
|
4563
|
+
echo "$cx_want" | tail -4 | sed 's/^/ /' >&2
|
|
4564
|
+
return 1
|
|
4565
|
+
fi
|
|
4566
|
+
if ! op_out=$(env -u ENTWURF_CODEX_APP_SERVER_ACTIVE -u TMUX \
|
|
4567
|
+
HOME="$cx_launch_home" CODEX_HOME="$cx_launch_home/.codex" PATH="$cx_launch_bin:$PATH" \
|
|
4568
|
+
"$installed_entwurf" codex-app-server --config probe=1 2>&1); then
|
|
4569
|
+
fail "[check-pack-install] installed 'entwurf codex-app-server' FAILED to reach the vendor:"
|
|
4570
|
+
echo "$op_out" | tail -8 | sed 's/^/ /' >&2
|
|
4571
|
+
return 1
|
|
4572
|
+
fi
|
|
4573
|
+
for want in "ARG<app-server>" "ARG<--listen>" "ARG<unix://$cx_want>" "ARG<--config>" "ARG<probe=1>"; do
|
|
4574
|
+
if ! printf '%s' "$op_out" | grep -qF "$want"; then
|
|
4575
|
+
fail "[check-pack-install] installed 'entwurf codex-app-server' did not hand the vendor the resolved address and argv (missing $want):"
|
|
4576
|
+
echo "$op_out" | tail -8 | sed 's/^/ /' >&2
|
|
4577
|
+
return 1
|
|
4578
|
+
fi
|
|
4579
|
+
done
|
|
4580
|
+
if printf '%s' "$op_out" | grep -qF 'ARG<codex-app-server>'; then
|
|
4581
|
+
fail "[check-pack-install] installed 'entwurf codex-app-server' leaked the dispatcher verb into the vendor's argv:"
|
|
4582
|
+
echo "$op_out" | tail -8 | sed 's/^/ /' >&2
|
|
4583
|
+
return 1
|
|
4584
|
+
fi
|
|
4585
|
+
echo "[check-pack-install] installed 'entwurf codex-app-server' reached the vendor at the INSTALLED resolver's address ($cx_want)"
|
|
4586
|
+
|
|
4501
4587
|
# The generation verb must reach its verdict from under node_modules. On the
|
|
4502
4588
|
# sandbox 0-record agent dir it archives nothing and opens a fresh generation.
|
|
4503
4589
|
# The exit code alone can't tell a verdict from a fence crash, so read the line.
|
|
@@ -5313,6 +5399,12 @@ setup_all() {
|
|
|
5313
5399
|
else
|
|
5314
5400
|
setup_result codex-statusline FAIL "detected codex, but the visible-identity setting did not complete (see above)"
|
|
5315
5401
|
fi
|
|
5402
|
+
codex_rc=0; codex_terminal_title install || codex_rc=$?
|
|
5403
|
+
if [ "$codex_rc" -eq 0 ]; then
|
|
5404
|
+
setup_harness_result codex-terminal-title "thread-id terminal title enabled (caller-seat placement input)" "./run.sh doctor-codex-terminal-title"
|
|
5405
|
+
else
|
|
5406
|
+
setup_result codex-terminal-title FAIL "detected codex, but the terminal-title setting did not complete (see above)"
|
|
5407
|
+
fi
|
|
5316
5408
|
fi
|
|
5317
5409
|
# ── core bridge boundary ── deterministic preflight lives in `pnpm run
|
|
5318
5410
|
# check:full`; live substrate acceptance lives in `LIVE=1 ./run.sh
|
|
@@ -5992,6 +6084,9 @@ case "$cmd" in
|
|
|
5992
6084
|
check-copilot-statusline)
|
|
5993
6085
|
check_copilot_statusline
|
|
5994
6086
|
;;
|
|
6087
|
+
check-codex-app-server-launch)
|
|
6088
|
+
check_codex_app_server_launch
|
|
6089
|
+
;;
|
|
5995
6090
|
check-copilot-launch)
|
|
5996
6091
|
check_copilot_launch
|
|
5997
6092
|
;;
|
|
@@ -6616,6 +6711,35 @@ case "$cmd" in
|
|
|
6616
6711
|
doctor-codex-statusline)
|
|
6617
6712
|
codex_statusline doctor
|
|
6618
6713
|
;;
|
|
6714
|
+
install-codex-terminal-title)
|
|
6715
|
+
codex_terminal_title install
|
|
6716
|
+
;;
|
|
6717
|
+
uninstall-codex-terminal-title)
|
|
6718
|
+
codex_terminal_title uninstall
|
|
6719
|
+
;;
|
|
6720
|
+
doctor-codex-terminal-title)
|
|
6721
|
+
codex_terminal_title doctor
|
|
6722
|
+
;;
|
|
6723
|
+
codex-socket-path)
|
|
6724
|
+
# #95 INTERNAL: print the Codex app-server default control-socket path for this
|
|
6725
|
+
# environment. It exists because `entwurf codex-app-server` is bash and cannot import
|
|
6726
|
+
# `resolveCodexDefaultSocketPath`; re-deriving that path in bash was MEASURED to diverge
|
|
6727
|
+
# (BOM-only CODEX_HOME, path normalization), so the launcher asks instead of transcribing.
|
|
6728
|
+
# `run_ts` is the crossing (compiled twin when installed, strip-types in a clone), which is
|
|
6729
|
+
# the whole reason this sits in the dispatcher rather than inside the leaf.
|
|
6730
|
+
shift || true
|
|
6731
|
+
run_ts scripts/codex-socket-path.ts "$@"
|
|
6732
|
+
;;
|
|
6733
|
+
codex-app-server)
|
|
6734
|
+
# #95: the managed app-server SPELLING. Same shape and same reasons as the `copilot`
|
|
6735
|
+
# branch below — `exec`, no subshell and no cd, because this operates on the operator's
|
|
6736
|
+
# own session rather than on the repo, and the `shift` keeps the dispatcher verb out of
|
|
6737
|
+
# the vendor's argv. It is NOT the same KIND of thing: `entwurf copilot` becomes a
|
|
6738
|
+
# sibling-bearing CLI, while this becomes the long-lived server those siblings' MCP
|
|
6739
|
+
# children hang off. Neither one supervises what it becomes.
|
|
6740
|
+
shift || true
|
|
6741
|
+
exec bash "$REPO_DIR/scripts/codex-app-server-launch.sh" "$@"
|
|
6742
|
+
;;
|
|
6619
6743
|
copilot)
|
|
6620
6744
|
# #82 RAIL 7: the managed launch. `exec` and NO subshell/cd on purpose — the vendor
|
|
6621
6745
|
# must inherit this terminal exactly: the caller's cwd, this pid, this tty, and its own
|