@junghanacs/entwurf 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +248 -0
  3. package/CONTRIBUTING.md +1 -1
  4. package/README.md +8 -6
  5. package/VERIFY.md +3 -3
  6. package/docs/external-mcp-host.md +2 -2
  7. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +18 -7
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +76 -9
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +54 -6
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resolve-tmux-session.js +114 -0
  11. package/mcp/entwurf-bridge/src/index.ts +22 -7
  12. package/package.json +1 -1
  13. package/pi-extensions/entwurf-control.ts +27 -3
  14. package/pi-extensions/lib/mux-fresh-call.ts +98 -11
  15. package/pi-extensions/lib/mux-placement.ts +65 -10
  16. package/pi-extensions/lib/resolve-tmux-session.ts +137 -0
  17. package/scripts/check-fresh-cut-gate.sh +54 -2
  18. package/scripts/check-gate-qualification.ts +5 -5
  19. package/scripts/check-mux-launch-tmux.ts +77 -2
  20. package/scripts/check-mux-launch.ts +17 -0
  21. package/scripts/check-mux-placement-tmux.ts +61 -1
  22. package/scripts/check-mux-placement.ts +33 -0
  23. package/scripts/check-release-gate-outcomes.ts +224 -9
  24. package/scripts/ci-qualify-decide.sh +159 -0
  25. package/scripts/fixtures/qualify-replay.json +149 -0
  26. package/scripts/mutants/fresh-cut.json +26 -0
  27. package/scripts/mutants/mux-boundary.json +24 -0
  28. package/scripts/mutants/mux-fresh-call.json +81 -0
  29. package/scripts/mutants/omp-birth.json +16 -3
  30. package/scripts/mutants/release-gate.json +45 -1
  31. package/scripts/omp-bridge-doctor.sh +11 -1
  32. package/scripts/smoke-mux-fresh-call-live.ts +175 -3
  33. package/scripts/smoke-omp-bridge-state.sh +5 -2
@@ -1,6 +1,7 @@
1
1
  /**
2
- * mux-fresh-call — open ONE visible sibling in the caller's own tmux session, hand it its first
3
- * task in the launch argv, and let it name itself back to the caller.
2
+ * mux-fresh-call — open ONE visible sibling in the caller's own tmux server (its own session by
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.
4
5
  *
5
6
  * ── Why this is a third module and not a parameter on the leaf ──
6
7
  *
@@ -43,6 +44,36 @@
43
44
  * - the receipt echoes what was REQUESTED, exactly as `runtimePath` does. It never reports
44
45
  * `pane_current_path`: proving where the pane actually landed belongs to acceptance, not
45
46
  * to the launch receipt.
47
+ *
48
+ * ── The optional project seat (issue #105) ──
49
+ *
50
+ * A fresh sibling opens in the caller's own tmux session — unless the caller names ONE session
51
+ * on the SAME server. That input exists because the operator's seats are per-project: a sibling
52
+ * opened for the `org` project belongs in the `org` session, and before this the only way to
53
+ * put it there was for the operator to move the window by hand. The rules are as narrow as the
54
+ * cwd input's, and for the same reason:
55
+ *
56
+ * - `undefined` means the caller's own session, and the argv is byte-identical to the
57
+ * pre-#105 shape. A named session is resolved to its native `$id` by the shared
58
+ * `resolve-tmux-session.ts` leaf and ONLY that id ever reaches `-t`.
59
+ * - a session that does not exist is `tmux-session-missing` and NOTHING is created — not the
60
+ * window, not the session. There is no `ifMissing` axis and no `new-session` verb anywhere
61
+ * in this product (GLG, 2026-09-07): the operator creates the seat and calls again. A name
62
+ * outside the leaf's grammar is the separate `tmux-session-name-invalid` — a different
63
+ * repair, because that name is a shape this rail does not address rather than a session
64
+ * that is absent. The leaf owns which shapes and why; part of that set tmux genuinely
65
+ * cannot resolve and part is a narrowing this rail chose, and it says which is which.
66
+ * - `-d` is what makes this safe to do to a session someone is looking at. `[측정]` without
67
+ * it, a `new-window` into another session changes THAT session's active window and steals
68
+ * the operator's focus. It was already fixed in this argv; #105 is where it became
69
+ * load-bearing.
70
+ * - the seat is ORTHOGONAL to the cwd. `[측정 ×2]` with `-c` omitted, a window opened into
71
+ * another session lands in the cwd of the process that ran `new-window` — not the target
72
+ * session's `session_path` and not its active pane. Neither input is ever inferred from
73
+ * the other.
74
+ * - the receipt echoes the REQUESTED name and carries the OBSERVED target `$id`, which is
75
+ * the session the window is actually in. It still reports no `pane_current_path`, and
76
+ * there is no "session created" field because nothing here creates one.
46
77
  */
47
78
 
48
79
  import { randomBytes } from "node:crypto";
@@ -63,7 +94,6 @@ import {
63
94
  assertSelector,
64
95
  assertTmuxOk,
65
96
  inspectPlacement,
66
- type Placement,
67
97
  type PlacementRejectReason,
68
98
  parseWindowFields,
69
99
  requireSameContext,
@@ -71,6 +101,7 @@ import {
71
101
  type WindowHandle,
72
102
  } from "./mux-placement.ts";
73
103
  import { OMP_PREFLIGHT_HINT, type OmpPreflightRejectReason, ompFreshPreflight } from "./omp-fresh-preflight.ts";
104
+ import { classifyTmuxSessionName, resolveTmuxSessionId, type TmuxSessionRejectReason } from "./resolve-tmux-session.ts";
74
105
 
75
106
  /** The backends this rail can open. Fixed set, not a profile — a further one is a decision,
76
107
  * not a config entry. `copilot` was added by #82 RAIL 9 under the step 9 admission contract, and
@@ -341,6 +372,7 @@ export type FreshCallRejectReason =
341
372
  | PlacementRejectReason
342
373
  | LaunchRejectReason
343
374
  | TmuxCwdRejectReason
375
+ | TmuxSessionRejectReason
344
376
  | CopilotPreflightRejectReason
345
377
  | OmpPreflightRejectReason
346
378
  | "caller-identity-unavailable"
@@ -349,6 +381,14 @@ export type FreshCallRejectReason =
349
381
  | "task-empty"
350
382
  | "task-too-long";
351
383
 
384
+ /** The optional project seat: ONE existing session on the caller's own tmux server, named
385
+ * literally. An object rather than a bare string so the seat axis can never be confused with
386
+ * the cwd axis at a call site — and so a future seat fact, if one is ever measured to be
387
+ * needed, does not arrive as a second top-level parameter. */
388
+ export interface FreshCallPlacement {
389
+ tmuxSession: string;
390
+ }
391
+
352
392
  /** Coordinates plus what was handed to tmux. Read `runtimePath` as "what we asked to start".
353
393
  * There is deliberately NO field here for the callback, the nonce's arrival, or the sibling's
354
394
  * garden id — see the module header. */
@@ -359,6 +399,10 @@ export interface FreshCallReceipt extends WindowHandle {
359
399
  * of fact as `runtimePath`: what tmux was asked for, never an observation of where the pane
360
400
  * landed. */
361
401
  cwd?: string;
402
+ /** The REQUESTED session name — present only when the caller named a seat. The RESOLVED
403
+ * target is the inherited `sessionId`, which is the session the window is actually in; this
404
+ * field is the request that produced it, exactly as `cwd` is. */
405
+ tmuxSession?: string;
362
406
  runtimePath: string;
363
407
  nonce: string;
364
408
  }
@@ -403,14 +447,21 @@ const SCRUBBED_INHERITED_ENV = ["PI_SESSION_ID=", "PI_AGENT_ID="] as const;
403
447
 
404
448
  /** Launch argv: the leaf's detached-append shape, the identity scrub, optionally `-c` at the
405
449
  * resume-symmetric token position (after `-t`, before `-P -F`), the runtime, then the backend's
406
- * dialect. An omitted cwd adds no `-c` carrier at all. */
450
+ * dialect. An omitted cwd adds no `-c` carrier at all.
451
+ *
452
+ * The first parameter is the TARGET session id, not the caller's placement. Since #105 those
453
+ * are not always the same session, and taking a `Placement` here would invite exactly the
454
+ * defect this signature prevents: copying the caller's own id into a cross-session launch. A
455
+ * name never reaches this function — the seat is resolved to a native `$id` before it is
456
+ * called, and `assertSelector` refuses anything that is not one. `-d` is not optional: without
457
+ * it a window opened into another session steals that session's focus (measured). */
407
458
  export function buildFreshCallArgs(
408
- placement: Placement,
459
+ targetSessionId: string,
409
460
  runtimePath: string,
410
461
  backendArgs: readonly string[],
411
462
  cwd?: string,
412
463
  ): string[] {
413
- assertSelector("session", placement.sessionId);
464
+ assertSelector("session", targetSessionId);
414
465
  assertLaunchTarget(runtimePath);
415
466
  if (cwd !== undefined) {
416
467
  const bad = classifyTmuxCwd(cwd);
@@ -422,7 +473,7 @@ export function buildFreshCallArgs(
422
473
  "-a",
423
474
  ...SCRUBBED_INHERITED_ENV.flatMap((assignment) => ["-e", assignment]),
424
475
  "-t",
425
- `${placement.sessionId}:{end}`,
476
+ `${targetSessionId}:{end}`,
426
477
  ...(cwd === undefined ? [] : ["-c", cwd]),
427
478
  "-P",
428
479
  "-F",
@@ -443,7 +494,14 @@ export function buildFreshCallArgs(
443
494
  * against a store, or guesses it: an empty value is a named refusal, not a lookup.
444
495
  */
445
496
  export function freshCall(
446
- params: { backend: FreshCallBackend; model: string; task: string; cwd?: string; callerGardenId: string | null },
497
+ params: {
498
+ backend: FreshCallBackend;
499
+ model: string;
500
+ task: string;
501
+ cwd?: string;
502
+ placement?: FreshCallPlacement;
503
+ callerGardenId: string | null;
504
+ },
447
505
  env: NodeJS.ProcessEnv = process.env,
448
506
  nonce: string = mintNonce(),
449
507
  ): FreshCallResult {
@@ -464,6 +522,16 @@ export function freshCall(
464
522
  const badCwd = classifyTmuxCwd(cwd);
465
523
  if (badCwd) return { ok: false, reason: badCwd };
466
524
  }
525
+ // The seat's NAME is classified here, beside the cwd and for the same reason: it is decidable
526
+ // without tmux, so an unresolvable name is answered before anything else runs. Whether that
527
+ // session EXISTS is a tmux question and is asked below, after the caller's own context is
528
+ // proven — a name check that needed a live server would refuse for the wrong reason on a
529
+ // host with no tmux at all.
530
+ const seat = params.placement?.tmuxSession;
531
+ if (seat !== undefined) {
532
+ const badSeat = classifyTmuxSessionName(seat);
533
+ if (badSeat) return { ok: false, reason: badSeat };
534
+ }
467
535
 
468
536
  let runtimePath: string;
469
537
  try {
@@ -493,6 +561,17 @@ export function freshCall(
493
561
  const placement = inspected.placement;
494
562
  requireSameContext("freshCall", placement, env);
495
563
 
564
+ // The caller's own context is now proven, which is what makes the next lookup's exit code
565
+ // readable as "that session is not here" rather than "there is no server". Only the resolved
566
+ // native id continues; the name does not travel past this line. STILL PRE-MUTATION: an
567
+ // absent seat refuses with no window anywhere.
568
+ let targetSessionId = placement.sessionId;
569
+ if (seat !== undefined) {
570
+ const resolved = resolveTmuxSessionId(seat, (args) => runTmux(args, env));
571
+ if (!resolved.ok) return { ok: false, reason: resolved.reason };
572
+ targetSessionId = resolved.sessionId;
573
+ }
574
+
496
575
  const composition: FreshCallComposition = {
497
576
  prompt: buildFreshCallPrompt({
498
577
  backend: params.backend,
@@ -503,7 +582,7 @@ export function freshCall(
503
582
  bootstrapPayload: buildOmpBootstrapPayload({ callerGardenId: params.callerGardenId, nonce, task }),
504
583
  };
505
584
  const run = runTmux(
506
- buildFreshCallArgs(placement, runtimePath, buildBackendArgs(params.backend, composition, model), cwd),
585
+ buildFreshCallArgs(targetSessionId, runtimePath, buildBackendArgs(params.backend, composition, model), cwd),
507
586
  env,
508
587
  );
509
588
  assertTmuxOk("new-window", run);
@@ -516,7 +595,7 @@ export function freshCall(
516
595
  // to find "the new one" is the guess this rail forbids everywhere else, so name the orphan.
517
596
  throw new Error(
518
597
  `mux-fresh-call: launched ${runtimePath} but could not read the window handle tmux printed — a window may ` +
519
- `be open in session ${placement.sessionId} that this call cannot identify or close: ${
598
+ `be open in session ${targetSessionId} that this call cannot identify or close: ${
520
599
  err instanceof Error ? err.message : String(err)
521
600
  }`,
522
601
  );
@@ -526,11 +605,12 @@ export function freshCall(
526
605
  ok: true,
527
606
  receipt: {
528
607
  serverPid: placement.serverPid,
529
- sessionId: placement.sessionId,
608
+ sessionId: targetSessionId,
530
609
  ...fields,
531
610
  backend: params.backend,
532
611
  model,
533
612
  ...(cwd === undefined ? {} : { cwd }),
613
+ ...(seat === undefined ? {} : { tmuxSession: seat }),
534
614
  runtimePath,
535
615
  nonce,
536
616
  },
@@ -557,6 +637,10 @@ const REJECT_HINT: Record<FreshCallRejectReason, string> = {
557
637
  "cwd-missing":
558
638
  "the requested cwd does not exist; tmux would not report this, it would open the window in $HOME and look successful",
559
639
  "cwd-not-directory": "the requested cwd exists but is not a directory",
640
+ "tmux-session-name-invalid":
641
+ "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",
642
+ "tmux-session-missing":
643
+ "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",
560
644
  "model-empty": "model is empty after trimming; fresh calls require an explicit model",
561
645
  "model-invalid": `model must be one ${MODEL_MAX_CHARS}-character argv-safe id/alias without whitespace or tmux syntax`,
562
646
  "task-empty": "task is empty after trimming",
@@ -592,6 +676,9 @@ export function renderFreshCall(result: FreshCallResult): { text: string; isErro
592
676
  ` backend: ${r.backend} (${r.runtimePath})\n` +
593
677
  ` model: ${r.model} (requested on the runtime CLI)\n` +
594
678
  (r.cwd === undefined ? "" : ` cwd: ${r.cwd} (requested start directory — not an observation)\n`) +
679
+ (r.tmuxSession === undefined
680
+ ? ""
681
+ : ` seat: ${r.tmuxSession} (requested tmux session, resolved to ${r.sessionId})\n`) +
595
682
  ` window: ${r.windowId} (index ${r.windowIndex}) in session ${r.sessionId}\n` +
596
683
  ` pane: ${r.paneId} pid ${r.panePid}\n` +
597
684
  ` nonce: ${r.nonce}\n` +
@@ -8,11 +8,17 @@
8
8
  * session the operator is looking at. Inheriting the environment IS the addressing: this
9
9
  * module never takes a socket path or a session name from a caller.
10
10
  *
11
+ * That sentence is still exactly true of THIS leaf, and #105 is where it started to be worth
12
+ * saying precisely. The fresh-call composition above may now be handed one session NAME by its
13
+ * caller, but it resolves that name to a native `$id` through its own leaf before anything
14
+ * reaches here — `appendWindow` still appends only to the caller's own session, and no function
15
+ * in this file has ever seen or will see a session name.
16
+ *
11
17
  * Three actions, deliberately not four:
12
18
  *
13
19
  * inspectPlacement() the caller's own $session/@window/%pane, or a named refusal
14
20
  * appendWindow() one default-shell window at the end of that same session
15
- * closeWindow() that window, by stable id, in the context it was opened in
21
+ * closeWindow() that window, by stable id, on the server it was opened on
16
22
  *
17
23
  * What this module is NOT: a delivery transport, an address, a liveness fact, a launcher.
18
24
  * It opens a place. `entwurf_v2` still owns delivery (`V2-DELIVERY-EXCLUDES-MUX`), and
@@ -51,8 +57,20 @@
51
57
  * 4. BINDING. A `Placement` is a fact about one server and one session, and the environment
52
58
  * passed to a later call could name a different — or restarted — server where the same
53
59
  * `$3`/`@7` mean something else entirely. Every mutation re-reads the caller's placement
54
- * and refuses unless the server pid and session id still match the ones the handle was
55
- * born in.
60
+ * before it runs.
61
+ *
62
+ * The two mutations bind to DIFFERENT halves of that fact, because they are answering
63
+ * different questions. `appendWindow` asks "is the target the caller gave me still the
64
+ * caller's own seat?" and needs both halves — `isSameContext`. `closeWindow` asks "is this
65
+ * handle still the window it was born as?" and needs only the SERVER half — a `@id` is
66
+ * unique for the life of one server, so the session half adds nothing there. `[측정
67
+ * 2026-09-07, private server]` ids are handed out monotonically and are never recycled:
68
+ * after `@2` was killed the next window was `@3`, and after a whole session holding `@4`
69
+ * and `@5` was killed the next was `@6`. Requiring the handle's session to still exist
70
+ * would also break the more informative answer below — a window whose session is gone is
71
+ * `already-gone`, which is a fact the caller wants, not an error. Since #105 a launched
72
+ * window may legitimately live in a session that is not the caller's, and that is exactly
73
+ * when this distinction stops being academic.
56
74
  *
57
75
  * The machine-readable rows carry ONLY native ids and decimal numbers, joined by `|`. The
58
76
  * free-form fields tmux could also report (`socket_path`, `session_name`) are deliberately
@@ -124,10 +142,13 @@ export interface Placement {
124
142
  }
125
143
 
126
144
  /**
127
- * A window this module opened, carrying the context it was born in. `serverPid`/`sessionId`
128
- * are what let a later close prove it is acting on the same server and session rather than a
129
- * restarted one that happens to reuse the id. Index is reported for the human, never used as
130
- * a handle.
145
+ * A window this module opened, carrying the context it was born in. `serverPid` is what a later
146
+ * close binds to: a restarted tmux server hands out `@7` again, so without it a close could act
147
+ * on some other server's window. `sessionId` rides along as the RECEIPT of where the window was
148
+ * put — since #105 that may be a session other than the caller's — and it is deliberately not a
149
+ * close precondition, because `@id`s are unique for one server's whole life (measured: after
150
+ * `@2` was killed the next window was `@3`, and after a session holding `@4`/`@5` was killed the
151
+ * next was `@6`). Index is reported for the human, never used as a handle.
131
152
  */
132
153
  export interface WindowHandle {
133
154
  serverPid: string;
@@ -358,6 +379,40 @@ export function requireSameContext(label: string, origin: PlacementContext, env:
358
379
  }
359
380
  }
360
381
 
382
+ /**
383
+ * Same server, whatever the session. The close-side half of boundary 4, kept as its own pure
384
+ * predicate for the same reason `isSameContext` is one: the decision is what a deterministic
385
+ * gate can pin, and the re-read around it is not.
386
+ */
387
+ export function isSameServer(origin: PlacementContext, now: PlacementContext): boolean {
388
+ return origin.serverPid === now.serverPid;
389
+ }
390
+
391
+ /**
392
+ * Re-read the caller's placement and refuse unless the handle was born on the SAME SERVER. The
393
+ * close-side half of boundary 4, and deliberately NOT `requireSameContext`: since #105 a handle
394
+ * can name a window in a session that is not the caller's, and the session half would refuse a
395
+ * legitimate close. What it must still refuse is a handle from a DIFFERENT — or restarted —
396
+ * server, where `@7` names some other window entirely.
397
+ *
398
+ * The session half is not lost, it is covered better: `@id`s are unique for one server's whole
399
+ * life (measured — see boundary 4), so on a matching server the id alone identifies the window,
400
+ * and a window whose session has since been killed is proven absent by `closeWindow`'s own
401
+ * `list-windows -a` read and reported as `already-gone`.
402
+ */
403
+ export function requireSameServer(label: string, origin: PlacementContext, env: NodeJS.ProcessEnv): void {
404
+ const now = inspectPlacement(env);
405
+ if (!now.ok) {
406
+ throw new Error(`mux-placement: ${label} refused — the caller's placement is not resolvable (${now.reason})`);
407
+ }
408
+ if (!isSameServer(origin, now.placement)) {
409
+ throw new Error(
410
+ `mux-placement: ${label} refused — server changed (handle was born on server ${origin.serverPid}, ` +
411
+ `this environment names server ${now.placement.serverPid})`,
412
+ );
413
+ }
414
+ }
415
+
361
416
  /** One detached default-shell window at the end of the caller's own session. */
362
417
  export function appendWindow(placement: Placement, env: NodeJS.ProcessEnv = process.env): WindowHandle {
363
418
  requireSameContext("appendWindow", placement, env);
@@ -367,11 +422,11 @@ export function appendWindow(placement: Placement, env: NodeJS.ProcessEnv = proc
367
422
  }
368
423
 
369
424
  /**
370
- * Close one window by stable id, in the context it was opened in. Reports whether it was
371
- * closed or had already gone.
425
+ * Close one window by stable id, on the server it was opened on. Reports whether it was closed
426
+ * or had already gone. The binding is the server, not the session — see `requireSameServer`.
372
427
  */
373
428
  export function closeWindow(handle: WindowHandle, env: NodeJS.ProcessEnv = process.env): CloseOutcome {
374
- requireSameContext("closeWindow", handle, env);
429
+ requireSameServer("closeWindow", handle, env);
375
430
  const run = runTmux(buildCloseArgs(handle.windowId), env);
376
431
  if (run.status === 0) return "closed";
377
432
  // A signal kill is not a "tmux said no" — it is the call failing, and it carries no
@@ -0,0 +1,137 @@
1
+ /**
2
+ * resolve-tmux-session — the ONE resolution of a caller-supplied tmux SESSION NAME into the
3
+ * native `$id` a placement may target. Narrow leaf of the fresh-call composition (#105); it
4
+ * owns the name grammar and the name→id lookup and NOTHING else — it never runs tmux (the
5
+ * runner is injected), never phrases a hint (each consumer owns its own wording), never
6
+ * creates a session, and has no fallback session.
7
+ *
8
+ * Same shape and same discipline as `classify-tmux-cwd.ts`: this file imports nothing at all,
9
+ * not even a node builtin, so it stays deletable on its own and cannot acquire an opinion
10
+ * about mux, entwurf, identity or delivery. The injected runner is matched STRUCTURALLY to
11
+ * `mux-placement.TmuxRun` rather than by a type import, for the same reason.
12
+ *
13
+ * Every rule below is a MEASURED tmux 3.6a behaviour (2026-09-07, private `-S` servers; the
14
+ * research lane's two independent reproductions are in issue #105's thread), and each one is a
15
+ * way a lookup would look successful while addressing the wrong thing:
16
+ *
17
+ * 1. `-t '=NAME'` performs NO format expansion — `=a}b`, `=a|b`, `=a b`, `=a,b` all resolve
18
+ * exactly. The `-f '#{==:#{session_name},NAME}'` FILTER engine does the opposite: a `}`
19
+ * inside the name closes the comparison early, the filter becomes a truthy string, and
20
+ * EVERY session matches (6/6 measured). So the engine here is `-t '='`, never a filter.
21
+ * 2. absence is reported by EXIT CODE, not by output. `list-windows -t '=nosuch'` exits 1
22
+ * with `can't find session: nosuch`, while `display-message -p -t '=NAME'` exits 0 with
23
+ * EMPTY output for a name that exists AND for one that does not — it is unusable as a
24
+ * probe. Only an rc=0 answer is ever parsed here.
25
+ * 3. some names cannot be addressed at all, for two different measured reasons. `#` is
26
+ * FORMAT-EXPANDED when `new-session -s` stores it (`a#{x}` stored as `a`), so the
27
+ * requested name never exists; and `.`/`:` are tmux's own PANE/WINDOW separators inside a
28
+ * `-t` target, so the requested name is SPLIT before any session is matched. Measured
29
+ * with this leaf's own engine: `list-windows -t '=my.project'` → rc=1
30
+ * `can't find pane: project`, and `list-windows -t '=my:project'` → rc=1
31
+ * `can't find session: my`. The exact wording is NOT contract — it moves with which
32
+ * sessions happen to exist (`can't find window: project` once a session `my` is there) —
33
+ * the fact is that the split happens at all. Both characters are ALSO normalised to `_`
34
+ * when stored, so `a.b` and `a:b` are the same stored name `a_b` and whichever is created
35
+ * second is a `duplicate session` error rather than a second seat. The lookup half is the
36
+ * load-bearing one: whatever tmux stored, the REQUESTED name can never address it.
37
+ * NOTE the difference from `classify-tmux-cwd.ts`: there `#(…)`
38
+ * was observed EXECUTING inside a `-c` value; here it expands but does NOT execute (a
39
+ * `q#(touch …)q` name stored as `qq` and wrote no file). Do not copy that leaf's
40
+ * rationale into this one, or relaxing one will silently relax the other.
41
+ * 4. `=` protects a name against `%9`/`@1` id syntax but NOT against `$`: with a session
42
+ * NAMED `$0` beside one whose ID is `$0`, `-t '=$0'` resolves the ID (measured — the
43
+ * name-holder was `$1` and the lookup returned `$0`). And a session literally named
44
+ * `=foo` needs `==foo`. An escaping layer here would be a second parser to keep true.
45
+ *
46
+ * The grammar is `^[A-Za-z0-9][A-Za-z0-9_-]*$`, and it is WIDER than what rules 3-4 force. Say
47
+ * that plainly rather than letting the reasons above cover the whole refusal set: `a}b`, `a|b`,
48
+ * `a b`, `a;b`, `a,b` and `_a` are all created verbatim AND resolved exactly by `-t '=NAME'`
49
+ * (measured 2026-09-07, one server, six sessions, six exact ids). They are refused anyway, and
50
+ * the reason is a DECISION, not a tmux limit — this is the grammar entwurf would need to CREATE
51
+ * a session safely, kept symmetric for lookup, with the remaining foreign-name width closed
52
+ * until an operator need for it is actually observed. That is why the refusal is
53
+ * `tmux-session-name-invalid` ("this rail does not address that shape") and not
54
+ * `tmux-session-missing` ("no such session here"): the caller's repair differs, and telling an
55
+ * operator their perfectly findable session "could never be found" would be a false cause.
56
+ *
57
+ * ONE BOUNDED IMPRECISION, STATED RATHER THAN LAUNDERED: rc≠0 also covers "no server running
58
+ * on <socket>" (measured). This leaf reads every rc≠0 as `tmux-session-missing`, so a server
59
+ * that died between the caller's context proof and this lookup is reported under the narrower
60
+ * word. That is safe — both readings are refusals that mutate nothing, and the consumer's hint
61
+ * names both — and it is preferred over matching tmux's own stderr text, which would pin this
62
+ * leaf to one vendor version's wording.
63
+ */
64
+
65
+ /** Why a caller-named session could not become a target. Two stable literals — the consuming
66
+ * composition widens its own reject union with this type, so the strings are contract. */
67
+ export type TmuxSessionRejectReason = "tmux-session-name-invalid" | "tmux-session-missing";
68
+
69
+ /** What the injected runner returns. Structurally identical to `mux-placement.TmuxRun`; kept
70
+ * as its own declaration so this leaf imports nothing. */
71
+ export interface TmuxSessionLookupRun {
72
+ status: number | null;
73
+ stdout: string;
74
+ stderr: string;
75
+ }
76
+
77
+ export type TmuxSessionLookupResult = { ok: true; sessionId: string } | { ok: false; reason: TmuxSessionRejectReason };
78
+
79
+ const SESSION_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
80
+ const SESSION_ID = /^\$[0-9]+$/;
81
+
82
+ /**
83
+ * Classify a candidate session NAME. Separate from the lookup because the operator's next move
84
+ * differs: an out-of-grammar name is a shape this rail does not address — some of those tmux
85
+ * genuinely cannot resolve (rules 3-4), others it resolves fine and this rail declines anyway —
86
+ * while a missing session is something the operator creates and retries.
87
+ */
88
+ export function classifyTmuxSessionName(name: string): "tmux-session-name-invalid" | null {
89
+ return SESSION_NAME.test(name) ? null : "tmux-session-name-invalid";
90
+ }
91
+
92
+ /** The lookup argv. `-t '=NAME'` is the exact-name selector; `list-windows` is the engine that
93
+ * answers with an exit code for absence. The builder re-validates rather than trusting its
94
+ * caller, the same way the fresh-call argv builder re-checks a cwd. */
95
+ export function buildTmuxSessionLookupArgs(name: string): string[] {
96
+ if (classifyTmuxSessionName(name)) {
97
+ throw new Error(`resolve-tmux-session: refusing to build a lookup for an unresolvable session name: ${name}`);
98
+ }
99
+ return ["list-windows", "-t", `=${name}`, "-F", "#{session_id}"];
100
+ }
101
+
102
+ /**
103
+ * Resolve a session name to its native `$id` on whatever server the runner's environment names.
104
+ * The id is the ONLY thing a caller-supplied name is allowed to become: everything downstream
105
+ * targets `$id`, never the name again.
106
+ *
107
+ * A session always holds at least one window, so an rc=0 answer prints one `$id` line per
108
+ * window and they are all the same id. An rc=0 that cannot be read that way is an operational
109
+ * anomaly, not an answer about the session, and is raised rather than turned into a refusal.
110
+ */
111
+ export function resolveTmuxSessionId(
112
+ name: string,
113
+ run: (args: string[]) => TmuxSessionLookupRun,
114
+ ): TmuxSessionLookupResult {
115
+ const badName = classifyTmuxSessionName(name);
116
+ if (badName) return { ok: false, reason: badName };
117
+ const result = run(buildTmuxSessionLookupArgs(name));
118
+ // A signalled call is not tmux answering — it carries no information about the session at
119
+ // all, so it must never be read as absence.
120
+ if (result.status === null) {
121
+ throw new Error(
122
+ `resolve-tmux-session: the lookup for session "${name}" was killed by a signal: ${result.stderr.trim()}`,
123
+ );
124
+ }
125
+ if (result.status !== 0) return { ok: false, reason: "tmux-session-missing" };
126
+ const lines = result.stdout
127
+ .split("\n")
128
+ .map((line) => line.trim())
129
+ .filter((line) => line.length > 0);
130
+ const first = lines[0];
131
+ if (first === undefined || !SESSION_ID.test(first) || lines.some((line) => line !== first)) {
132
+ throw new Error(
133
+ `resolve-tmux-session: tmux answered rc=0 for session "${name}" with something that is not one native session id: ${JSON.stringify(result.stdout)}`,
134
+ );
135
+ }
136
+ return { ok: true, sessionId: first };
137
+ }
@@ -403,12 +403,44 @@ fi
403
403
  # #86 A5 credential tripwire: the retired sync_auth mutation (OAuth alias copy +
404
404
  # auth.json.bak) must never resurrect as an invocable surface in run.sh. Comments
405
405
  # documenting the removal are allowed; a function definition or call is not.
406
- if awk '!/^[[:space:]]*#/' "$REPO/run.sh" | grep -q 'sync_auth'; then
406
+ # THE PREDICATE IS A FUNCTION so the self-test below can run the SAME code against a planted
407
+ # fixture. A self-test that re-typed this pipeline would keep passing while the real one went
408
+ # blind, which is precisely the failure this cell exists to prevent.
409
+ #
410
+ # `grep -c`, never `grep -q`. This file runs under pipefail, and a `-q` grep exits at the FIRST
411
+ # match, closing the pipe while `awk` is still writing the remaining ~200KB of a 433KB run.sh.
412
+ # `awk` dies of SIGPIPE (141), pipefail promotes that to the pipeline's status, and the `if`
413
+ # takes the ELSE branch — so this tripwire went blind EXACTLY when a sync_auth surface existed
414
+ # and reported "no credential surface remains". Unlike the omp doctor's load-dependent race this
415
+ # one was deterministic, because the producer is far larger than the pipe buffer.
416
+ sync_auth_surface_count() {
417
+ awk '!/^[[:space:]]*#/' "$1" | grep -c 'sync_auth' || true
418
+ }
419
+ sync_auth_hits="$(sync_auth_surface_count "$REPO/run.sh")"
420
+ if [ "${sync_auth_hits:-0}" -gt 0 ]; then
407
421
  bad "D4b the retired sync_auth credential mutation reappeared as code in run.sh"
408
422
  else
409
423
  ok "D4b no sync_auth credential surface remains in run.sh (comments only)"
410
424
  fi
411
425
 
426
+ # D4b-self. The cell above is green on a clean run.sh — which is exactly the state in which a
427
+ # blind tripwire and a working one are indistinguishable. This plants the surface it exists to
428
+ # catch and requires the SAME predicate to see it. The plant goes NEAR THE TOP of the real,
429
+ # large run.sh on purpose: the defect only appears when the consumer can exit while the producer
430
+ # still has bulk left to write, so a small synthetic fixture would pass even with `grep -q` and
431
+ # would prove nothing. It is a copy under mktemp; run.sh is never written.
432
+ fc_probe="$(mktemp -t fresh-cut-syncauth-probe.XXXXXX)"
433
+ awk 'NR==2{print "sync_auth() { cp \"$HOME/.claude/.credentials.json\" \"$HOME/.claude/auth.json.bak\"; }"}1' \
434
+ "$REPO/run.sh" >"$fc_probe"
435
+ fc_planted="$(sync_auth_surface_count "$fc_probe")"
436
+ fc_clean="$(sync_auth_surface_count "$REPO/run.sh")"
437
+ rm -f "$fc_probe"
438
+ if [ "${fc_planted:-0}" -gt 0 ] && [ "${fc_clean:-0}" -eq 0 ]; then
439
+ ok "D4b-self the tripwire actually FIRES on a planted sync_auth surface (planted=$fc_planted clean=$fc_clean)"
440
+ else
441
+ bad "[QK:FRESHCUT-SYNCAUTH-TRIPWIRE-FIRES] D4b's predicate did not see a planted sync_auth surface (planted=$fc_planted clean=$fc_clean) — the credential tripwire is blind and a green D4b means nothing"
442
+ fi
443
+
412
444
  ilp_gate=$(awk '/^install_local_package\(\)/,/^}/{ if ($0 ~ /preflight_v3_store install/) { print NR; exit } }' "$REPO/run.sh")
413
445
  ilp_write=$(awk '/^install_local_package\(\)/,/^}/{ if ($0 ~ /register-pi-package.py|mkdir -p "\$project_dir/) { print NR; exit } }' "$REPO/run.sh")
414
446
  if [ -n "$ilp_gate" ] && [ -n "$ilp_write" ] && [ "$ilp_gate" -lt "$ilp_write" ]; then
@@ -425,12 +457,32 @@ else
425
457
  bad "D6 meta-bridge-install.sh's doctor call is missing or sits after the state snapshot (doctor=$mb_gate prepare=$mb_write)"
426
458
  fi
427
459
 
428
- if grep -n 'preflight_v3_store()' -A 40 "$REPO/run.sh" | grep -qE 'run_ts scripts/meta-bridge-fresh-cut\.ts'; then
460
+ # Same hazard as D4b. This producer happens to emit less than one pipe buffer today, so
461
+ # no SIGPIPE window exists — but that is a size coincidence, not a property, and it moves
462
+ # the moment run.sh grows another match. Counted rather than short-circuited.
463
+ preflight_cut_count() {
464
+ grep -n 'preflight_v3_store()' -A 40 "$1" | grep -cE 'run_ts scripts/meta-bridge-fresh-cut\.ts' || true
465
+ }
466
+ cut_in_preflight="$(preflight_cut_count "$REPO/run.sh")"
467
+ if [ "${cut_in_preflight:-0}" -gt 0 ]; then
429
468
  bad "D7 the preflight invokes fresh-cut — an install must never cut a generation by itself"
430
469
  else
431
470
  ok "D7 the preflight only ever asks the doctor (never runs the cut)"
432
471
  fi
433
472
 
473
+ # D7-self, same argument as D4b-self: a forbidding cell that is green on a clean tree proves
474
+ # nothing until it is shown catching the thing it forbids. A synthetic fixture is enough here
475
+ # because this producer has no SIGPIPE window to reproduce — what is being proven is detection.
476
+ d7_probe="$(mktemp -t fresh-cut-preflight-probe.XXXXXX)"
477
+ printf '%s\n' 'preflight_v3_store() {' ' run_ts scripts/meta-bridge-fresh-cut.ts' '}' >"$d7_probe"
478
+ d7_planted="$(preflight_cut_count "$d7_probe")"
479
+ rm -f "$d7_probe"
480
+ if [ "${d7_planted:-0}" -gt 0 ] && [ "${cut_in_preflight:-0}" -eq 0 ]; then
481
+ ok "D7-self the cell actually FIRES on a preflight that does call fresh-cut (planted=$d7_planted real=$cut_in_preflight)"
482
+ else
483
+ bad "[QK:FRESHCUT-PREFLIGHT-CUT-DETECTED] D7's predicate did not see a planted fresh-cut call in a preflight (planted=$d7_planted real=$cut_in_preflight)"
484
+ fi
485
+
434
486
  if [ -e "$FRESH_CUT_GATE_CLAUDE_SENTINEL" ]; then
435
487
  bad "D8 an offline gate drive crossed the store refusal and invoked Claude" "$(cat "$FRESH_CUT_GATE_CLAUDE_SENTINEL")"
436
488
  else
@@ -834,24 +834,24 @@ let manifestCount: number;
834
834
  "copilot-birth": 19,
835
835
  "copilot-launch": 14,
836
836
  "copilot-receive": 18,
837
- "fresh-cut": 1,
837
+ "fresh-cut": 3,
838
838
  "gate-qualification": 2,
839
839
  "meta-facts": 4,
840
840
  "meta-hook-session-switch": 17,
841
841
  "meta-identity": 4,
842
842
  "meta-retire": 3,
843
- "mux-boundary": 14,
844
- "mux-fresh-call": 37,
843
+ "mux-boundary": 16,
844
+ "mux-fresh-call": 44,
845
845
  "mux-launcher-fence": 7,
846
846
  "mux-parent-artifact": 3,
847
847
  "pack-install": 2,
848
848
  "pi-package-ownership": 6,
849
849
  "mux-resume-call": 12,
850
- "omp-birth": 11,
850
+ "omp-birth": 12,
851
851
  "omp-fresh": 24,
852
852
  "omp-receive": 11,
853
853
  "probe-ordering": 1,
854
- "release-gate": 14,
854
+ "release-gate": 18,
855
855
  "resume-args": 6,
856
856
  "resume-launch-identity": 6,
857
857
  "self-address": 5,