@junghanacs/entwurf 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -30,7 +30,7 @@ For agents that own this repo: invariant principles and reproducible verificatio
30
30
  - **pi adapter** attaches a pi session to a record at `session_start`, hosts the record-keyed control socket, and exposes the native pi tool surface.
31
31
  - **Native bridges** register already-running native sessions without taking over their transcript or auth: Claude Code is mailbox/self-fetch; Antigravity is probe-backed native-push. Codex has archived probe evidence, but its managed native lane was declined because pi already supplies the official GPT provider path; do not duplicate it as a native citizen or ACP backend.
32
32
  - **ACP plugin** registers provider `entwurf` inside a pi host session and drives a backend under an isolated overlay. The host pi session is already a record-backed socket citizen; the plugin does not mint another citizen/socket/peer layer.
33
- - **mux is launch-only and deliberately small.** Shipped: the tmux placement leaf (`mux-placement.ts` — inspect/append/close inside the caller's own session), the visible runtime launch composition (`mux-launch.ts`), the fresh-call composition (`mux-fresh-call.ts`), and the cwd-aware resume placement composition (`mux-resume-call.ts`). Fresh call is exposed as `entwurf_fresh_call`; visible same-id resume is `entwurf_resume_call`, composed at the two public surfaces by injecting `mux-resume-call` into the v2-side `entwurf-v2-visible-resume.ts`. The exact import graph is enumerated in [docs/mux-launch-rail.md](./docs/mux-launch-rail.md) §11. `entwurf_v2` behavior is unchanged and delivery still never imports launch. Fresh call learns a new sibling's garden id from the CALLBACK it makes, never from a lookup — so the pre-injected-token → identity-lookup design (rail §6) is **closed, not paused**. Do not reopen it without new evidence and explicit GLG re-approval. Fresh-call requires one explicit model and passes it in each runtime's measured CLI dialect (Pi `--model <provider/model>`, Claude Code `--model=<id-or-alias>`); resume takes only an existing target id and gets transcript/model/provider/cwd from its record. Do not grow either narrow carrier into a generic driver, harness profile, arbitrary command/cwd/env selection, or a second creation API. Ownership and import prohibitions: [docs/mux-launch-rail.md](./docs/mux-launch-rail.md) §11.
33
+ - **mux is launch-only and deliberately small.** Shipped: the tmux placement leaf (`mux-placement.ts` — inspect/append/close inside the caller's own session), the visible runtime launch composition (`mux-launch.ts`), the fresh-call composition (`mux-fresh-call.ts`), and the cwd-aware resume placement composition (`mux-resume-call.ts`). Fresh call is exposed as `entwurf_fresh_call`; visible same-id resume is `entwurf_resume_call`, composed at the two public surfaces by injecting `mux-resume-call` into the v2-side `entwurf-v2-visible-resume.ts`. The exact import graph is enumerated in [docs/mux-launch-rail.md](./docs/mux-launch-rail.md) §11. `entwurf_v2` behavior is unchanged and delivery still never imports launch. Fresh call learns a new sibling's garden id from the CALLBACK it makes, never from a lookup — so the pre-injected-token → identity-lookup design (rail §6) is **closed, not paused**. Do not reopen it without new evidence and explicit GLG re-approval. Fresh-call requires one explicit model and passes it in each runtime's measured CLI dialect (Pi `--model <provider/model>`, Claude Code `--model=<id-or-alias>`); it also takes ONE optional literal absolute `cwd` (#73 — omitted/`""` means the caller's own directory; no trim, no realpath, no project-name resolver; classified by the shared `classify-tmux-cwd.ts` leaf; the receipt echoes the REQUEST only), so a cross-repo fresh sibling is a fresh-call fact and never a reason to resume a dormant record. Resume takes only an existing target id and gets transcript/model/provider/cwd from its record. Do not grow either narrow carrier into a generic driver, harness profile, arbitrary command/env selection, or a second creation API. Ownership and import prohibitions: [docs/mux-launch-rail.md](./docs/mux-launch-rail.md) §11.
34
34
  - **One delivery verb:** `entwurf_v2` addresses an existing garden id. Current routes are live control-socket send, active self-fetch mailbox enqueue, and probe-alive native-push. NO route starts a process: the hidden background resume for a dormant citizen was withdrawn under the visible-first rule, so a dormant in-domain citizen rejects as `dormant-fire-forget-unsupported` and the intent axis is single-valued. Every other complementary state pair rejects honestly too. Fresh creation is a separate verb — `entwurf_fresh_call` — and it returns a launch receipt only; the new sibling's address arrives asynchronously as the sender envelope of its nonce callback.
35
35
 
36
36
  ## Hard Rules
@@ -101,6 +101,17 @@ A `--entwurf-control` pi session is a citizen for the same reason a native bridg
101
101
  - Sender envelope: `{ sessionId, agentId, cwd, timestamp, origin?, replyable? }`.
102
102
  - Human-opened and spawned/resumed siblings use the same addressing and messaging semantics.
103
103
 
104
+ ## Issue queue — a manual sweeper, not a backlog
105
+
106
+ - OPEN issues are live contracts only, capped at five. A sixth means something closes first; the cap does not bend.
107
+ - A slot is earned by a current defect or executable contract, not by importance, age, or possible future value.
108
+ - Direction, philosophy, frozen invariants and observations without a current action live in `ROADMAP.md`, durable docs, or closed history.
109
+ - Nothing stays open "in case." If a closed problem recurs, it returns as a new issue carrying the new evidence.
110
+ - Anything whose only value is knowing it gets moved to the document that owns it, then closed.
111
+ - A new issue brings the best available observation plus an executable next measurement. Recovering a missing signature may be the first acceptance; an idea with neither evidence nor a next measurement is not an issue.
112
+ - No collection points, umbrella trackers, or fallout buckets. Merge issues only when they share one cause and one acceptance.
113
+ - Sweep manually after a release and before opening an issue. Solved work closes at its durable SHA; it does not wait for the next release.
114
+
104
115
  ## Verification
105
116
 
106
117
  Two axes are required: deterministic/package gates and opt-in LIVE evidence.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,35 @@ All notable changes to this project will be documented here. Format follows [Kee
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.14.1 — 2026-08-13
8
+
9
+ This patch fills the cross-repository creation gap in the visible mux lifecycle shipped in 0.14.0, so one operator tmux session can form siblings in multiple project roots without borrowing a dormant session as a cwd carrier.
10
+
11
+ ### Added
12
+
13
+ - **`entwurf_fresh_call` accepts one optional literal absolute `cwd` for cross-repository siblings.** Omitting it (or passing `""`) keeps the caller's current directory; Pi and Claude Code receive the selected directory through their existing visible launch path, and the launch receipt reports the requested value without claiming runtime acceptance.
14
+
15
+ ### Changed
16
+
17
+ - **The lifecycle selection matrix now separates address, continuity, and project root.** Use `entwurf_v2` for an existing live citizen, `entwurf_resume_call` only for continuity with a dormant Pi citizen under its recorded cwd and garden id, and `entwurf_fresh_call` for a new sibling — omitting `cwd` for the caller's project or passing an absolute `cwd` for another repository. Resume is not a substitute for cross-repository fresh creation.
18
+
19
+ ### Fixed
20
+
21
+ - **The Claude meta-bridge installer relinquishes its retired ownership of `skipDangerousModePermissionPrompt`.** Proven prior install state is restored once when the value is still exactly entwurf's former managed value; operator-changed or unproven values remain untouched, malformed ownership evidence fails loud, and later uninstall cannot overwrite the returned choice.
22
+ - **The cross-harness chain LIVE smoke no longer inherits the runner's `PI_SESSION_ID` / `PI_AGENT_ID`.** When release-gate (or any pi `--entwurf-control` parent) spawned hop A, the MCP bridge preferred those carriers over A's SessionStart meta-sender marker and stamped the runner's garden id on hop 1 — payload still traversed, identity assertion failed. Child env now strips the pi identity carriers so the Claude hop's own marker is authoritative.
23
+
24
+ ### Verification
25
+
26
+ - Release-gate candidate: the in-gate `pnpm run check:full` exited 0 in 208s, and `./run.sh check-gate-qualification` killed **173/173** committed mutants.
27
+ - `LIVE=1 ./run.sh release-gate /tmp/entwurf-release-gate-0.14.1.j2eAVu --cut` — **MUST PASS=20 FAIL=0 SKIP=0**, **BEHAVIOR PASS=1 FAIL=0 SKIP=0**, `cut: OK`. Full log: `/tmp/entwurf-release-gate-0.14.1.j2eAVu/release-gate.log` (SHA-256 `bdd4d0b332d08c24210e9f56382b5898c0544fb6dffbf70287dade2f00e67c36`).
28
+ - Pre-tag installed-consumer diagnostics also passed: `./run.sh check-pack-install` installed the 0.14.1 tarball and proved the seven-tool boot plus physical delivery; `ENTWURF_REQUIRE_DOCKER=1 ./run.sh check-install-container` reached `container-consumer: PASS` in a checkout-invisible non-root Node 24 consumer (throwaway candidate SHA-256 `48778d33d1088af8078fe6b59ad932fb34ac8564e80bef84c34b25bab2bd5ef6`). These are diagnostic pack-once artifacts, not the preserved release candidate.
29
+ - On the source-owned operator host, `./run.sh install-meta-bridge` completed the retired-warning relinquishment and `./run.sh doctor-meta-bridge` passed with live MCP owner join, physical delivery, and writer parity. Exact prepared-HEAD CI and acceptance of one preserved immutable candidate remain `make` responsibilities.
30
+
31
+ ### Notes
32
+
33
+ - #76 (subscription-first refusal of `openrouter/*` sibling models), #60, #72, and #75 remain outside this patch.
34
+ - The chain-smoke repair only scrubs ambient pi identity carriers from its children. The MCP bridge's existing pi-carrier precedence is unchanged; any general nested-runner product guard is a separate contract, not part of 0.14.1 or #76.
35
+
7
36
  ## 0.14.0 — 2026-08-08
8
37
 
9
38
  Two arcs meet in this release. The product arc makes sibling sessions **visible**: opening and resuming a garden citizen is now an explicit, observable act, and the hidden alternatives were removed rather than kept as compatibility surface. The verification arc keeps the product from being swallowed by its own test cost: the everyday gate now runs in under a minute while the release floor stays strict. Claude Code native + pi + the meta-record store remain the main rail; the ACP backends (Claude ACP, Cortex) are optional service adapters on top of it.
package/README.md CHANGED
@@ -114,6 +114,12 @@ npx entwurf install .
114
114
  npx entwurf check-bridge
115
115
  ```
116
116
 
117
+ For an npm upgrade, rerun the install command in the same scope (use
118
+ `@junghanacs/entwurf@latest` when you want the registry's stable line explicitly),
119
+ then make the first check from that same scope: `entwurf check-bridge` for a global
120
+ install or `npx entwurf check-bridge` for a project-local install. Native-harness
121
+ installers and process restarts remain a separate post-upgrade step below.
122
+
117
123
  For manual MCP registration from a project-local install, point the host at:
118
124
 
119
125
  ```text
@@ -139,7 +145,11 @@ pnpm install
139
145
 
140
146
  A development clone runs the bridge source through Node's strip-types path;
141
147
  an npm-installed package runs the prebuilt JS under `mcp/entwurf-bridge/dist/`
142
- because Node refuses to strip `.ts` files under `node_modules`.
148
+ because Node refuses to strip `.ts` files under `node_modules`. The dev launcher's
149
+ source path means `./run.sh check-bridge` needs no build. After `git pull`, however,
150
+ run `pnpm install` when the lockfile changed and run `pnpm run build-bridge` before
151
+ artifact-consuming checks such as `./run.sh check-bridge-delivery`: `dist/` is
152
+ gitignored and may be absent or stale immediately after a checkout or pull.
143
153
 
144
154
  ### Pi adapter / ACP plugin lane
145
155
 
@@ -218,9 +228,8 @@ host evidence boundaries are [VERIFY.md](./VERIFY.md) and [BASELINE.md](./BASELI
218
228
 
219
229
  The curated model registry exposes unprefixed Claude ids plus `cortex-` rows.
220
230
  Codex is not an ACP backend or a shipped managed citizen lane: it has verified
221
- native-delivery probe evidence only. Release 0.14.0 does not add Codex support;
222
- any future managed lane still owes lifecycle, identity, installation, and doctors
223
- before calling it supported.
231
+ native-delivery probe evidence only. No managed Codex support is claimed; lifecycle,
232
+ identity, installation, and doctors remain prerequisites for any such lane.
224
233
 
225
234
  **Snowflake Cortex Code is the second ACP backend** (contract and audit:
226
235
  [docs/acp-backend-rail.md](./docs/acp-backend-rail.md#cortex-code-audit-d1d10)). Curated ids are
@@ -418,7 +427,7 @@ A two-pane recording covers the pre-0.12 v1 surface end-to-end — sibling resum
418
427
 
419
428
  Live peer messaging carries a sender envelope `{ sessionId, agentId, cwd, timestamp, origin?, replyable? }`; `entwurf_self` returns that authoritative envelope for the current pi session or trusted meta-session. Plain external MCP hosts are non-replyable. A garden-native meta-session carries a trusted `meta-session` envelope, but **`replyable` is a fact its own rail decides, not a consequence of being trusted** — a self-fetch citizen needs a live armed receiver, a native-push citizen needs an alive adapter probe, and a pi session needs its control socket. `entwurf_self` also reports which rail a meta-session reply would ride, because a native-push citizen has no mailbox to name. `wants_reply` is an etiquette marker rendered as a `(wants reply)` badge — not a transport contract, no wait, no polling. **v2 never gates on it:** a `wants_reply` from an external/non-replyable caller is passed through and surfaced honestly beside that sender's `replyable: false`, not rejected — the decider routes on target + intent, never on sender replyability. (The retired v1 `entwurf_send` did reject it; that behaviour went with the verb.)
420
429
 
421
- In ACP-backed and external native-harness sessions, `entwurf-bridge` exposes seven tools: `entwurf_v2`, `entwurf_peers`, `entwurf_fresh_call`, `entwurf_resume_call`, `entwurf_self`, `entwurf_inbox_read`, and the explicit/manual `entwurf_register_native` fallback. Native pi exposes the shared capability directly through the extension surface (`entwurf_v2`, `entwurf_peers`, `entwurf_fresh_call`, `entwurf_resume_call` tools; the socket-scan `/entwurf-sessions` command is gone — #50 C4). **For garden-id delivery/reply use `entwurf_v2`** — the canonical surface that classifies the target and routes to live-pi / Claude-Code-meta-mailbox / Antigravity-native-push, and refuses a dormant target instead of waking it. **To open a sibling that does not exist yet use `entwurf_fresh_call`** — it launches one fixed backend (`pi` or `claude-code`) as a visible window in the operator's own tmux session, requires an explicit model, passes that model on the runtime CLI, and hands it a first task; the sibling's first action is a nonce callback whose sender envelope carries its garden id. The launch receipt records the requested model plus tmux coordinates and never claims that the runtime accepted the model or completed delivery. (The v1 verbs `entwurf` / `entwurf_resume` / `entwurf_send` are gone.) Garden-native operator commands require `--entwurf-control`. There is no spawn target allowlist — the target registry is gone (#50 C3): `entwurf_v2` and `entwurf_resume_call` address an existing record-backed citizen, while `entwurf_fresh_call` takes its explicit backend/model/task directly rather than resolving a model tuple from a registry file. **To reopen a DORMANT pi citizen use `entwurf_resume_call {target}`** — the record supplies transcript, model, provider and cwd, so it takes no prompt, no task and no model override; it runs no turn, returns a LAUNCH receipt and a separate OBSERVATION receipt (only the second says the citizen is back), refuses a non-pi target as `target-not-pi`, and on an unobserved socket leaves the visible window open and releases its lock rather than retrying.
430
+ In ACP-backed and external native-harness sessions, `entwurf-bridge` exposes seven tools: `entwurf_v2`, `entwurf_peers`, `entwurf_fresh_call`, `entwurf_resume_call`, `entwurf_self`, `entwurf_inbox_read`, and the explicit/manual `entwurf_register_native` fallback. Native pi exposes the shared capability directly through the extension surface (`entwurf_v2`, `entwurf_peers`, `entwurf_fresh_call`, `entwurf_resume_call` tools; the socket-scan `/entwurf-sessions` command is gone — #50 C4). **For garden-id delivery/reply use `entwurf_v2`** — the canonical surface that classifies the target and routes to live-pi / Claude-Code-meta-mailbox / Antigravity-native-push, and refuses a dormant target instead of waking it. **To open a sibling that does not exist yet use `entwurf_fresh_call`** — it launches one fixed backend (`pi` or `claude-code`) as a visible window in the operator's own tmux session, requires an explicit model, accepts one optional literal absolute `cwd` (omit it or pass `""` to use the caller's cwd), passes the model and selected directory through the runtime's visible launch path, and hands it a first task; the sibling's first action is a nonce callback whose sender envelope carries its garden id. The launch receipt records the requested model/cwd plus tmux coordinates and never claims that the runtime accepted them or completed delivery. (The v1 verbs `entwurf` / `entwurf_resume` / `entwurf_send` are gone.) Garden-native operator commands require `--entwurf-control`. There is no spawn target allowlist — the target registry is gone (#50 C3): `entwurf_v2` and `entwurf_resume_call` address an existing record-backed citizen, while `entwurf_fresh_call` takes its explicit backend/model/task and optional cwd directly rather than resolving a model tuple from a registry file. **To reopen a DORMANT pi citizen use `entwurf_resume_call {target}`** — the record supplies transcript, model, provider and cwd, so it takes no prompt, no task and no model override; it runs no turn, returns a LAUNCH receipt and a separate OBSERVATION receipt (only the second says the citizen is back), refuses a non-pi target as `target-not-pi`, and on an unobserved socket leaves the visible window open and releases its lock rather than retrying.
422
431
 
423
432
  ### `entwurf_v2` — canonical dispatch verb
424
433
 
@@ -437,7 +446,7 @@ In ACP-backed and external native-harness sessions, `entwurf-bridge` exposes sev
437
446
 
438
447
  **`entwurf_v2` is the canonical surface for garden-id delivery.** When you have a garden id and want to reach whoever it names — message, reply, or hand-off — `entwurf_v2` is the one surface that reads whether the target is live pi, dormant pi, mailbox-backed Claude Code, or native-push Antigravity and routes correctly; *when unsure which transport, use `entwurf_v2`*. This prevents callers from guessing a rail from the shape of an id.
439
448
 
440
- What v2 provides is a **deterministic dispatch substrate** that moves the "which transport?" decision out of the fallible caller/model and into the decider, with transport-appropriate locking and an honest reject (no `✓ delivered`, no `.msg` garbage) when a target cannot receive. It still does **not** mint siblings, and it does not relaunch one either: every row above either reaches a citizen that is already running or refuses. Reopening a dormant pi citizen is `entwurf_resume_call`, a separate lifecycle verb that never routes through this decider. Fresh creation is the separate `entwurf_fresh_call` verb. It opens one fixed Pi or Claude Code runtime visibly in the caller's tmux session with a required explicit model passed in that runtime's measured CLI dialect, returns only a synchronous launch receipt, and lets the sibling report its new address asynchronously through the sender envelope of a nonce callback. The meta-mailbox row requires an **active** self-fetch receiver; native-push requires a record-backed, probe-alive native conversation and never borrows mailbox state. The [mux launch lane](./docs/mux-launch-rail.md) owns placement, fixed-runtime launch, and the two narrow compositions above it (fresh-call and resume-call placement); delivery does not import launch, and mux is not a delivery transport.
449
+ What v2 provides is a **deterministic dispatch substrate** that moves the "which transport?" decision out of the fallible caller/model and into the decider, with transport-appropriate locking and an honest reject (no `✓ delivered`, no `.msg` garbage) when a target cannot receive. It still does **not** mint siblings, and it does not relaunch one either: every row above either reaches a citizen that is already running or refuses. Reopening a dormant pi citizen is `entwurf_resume_call`, a separate lifecycle verb that never routes through this decider. Fresh creation is the separate `entwurf_fresh_call` verb. It opens one fixed Pi or Claude Code runtime visibly in the caller's tmux session with a required explicit model passed in that runtime's measured CLI dialect and one optional literal absolute `cwd`; omitted or `""` means the caller's cwd. It returns only a synchronous launch receipt and lets the sibling report its new address asynchronously through the sender envelope of a nonce callback. Use this cwd input for a new cross-repository sibling; do not resume a dormant citizen as a cwd substitute. The meta-mailbox row requires an **active** self-fetch receiver; native-push requires a record-backed, probe-alive native conversation and never borrows mailbox state. The [mux launch lane](./docs/mux-launch-rail.md) owns placement, fixed-runtime launch, and the two narrow compositions above it (fresh-call and resume-call placement); delivery does not import launch, and mux is not a delivery transport.
441
450
 
442
451
  A live pi target is *reached* over its control socket, but the socket is dispatch-internal transport, never identity (#50 C4). A control socket that no meta-record claims — a pre-record-era resident, an unreadable store, or a stale/planted file — is refused for **every** intent as `record-less-socket`, and the reject names the fix (restart the resident so `session_start` births its record, or quiesce and run the fresh-cut). `entwurf_peers` reports the same state as an aggregated `record-less-socket` diagnostic rather than a peer row.
443
452
 
@@ -9,7 +9,7 @@ the one-line registration; everything an operator needs to actually wire a host
9
9
  - **plain external MCP host**: no garden meta-record / sender marker. It can call the read surfaces (`entwurf_peers`, `entwurf_inbox_read`), but `entwurf_v2` sends are **refused by default** (#50 C4: "if we don't know who sent it, we don't send it"). The operator may wire the explicit hatch below; the send then goes out external/non-replyable.
10
10
  - **garden-native native session**: a trusted lifecycle hook minted a garden id and sender marker — `SessionStart` for Claude Code, `PreInvocation` for agy. It is not a pi control-socket session, but it can be replyable by garden id when its own mailbox/probe rail says so.
11
11
 
12
- **Which verb an external agent should reach for:** to deliver to / reply to a garden id, use **`entwurf_v2`** — it is the canonical delivery surface and the only one that reads whether the target is live pi, dormant pi, mailbox-backed Claude Code, or native-push Antigravity and routes correctly. Discover targets with `entwurf_peers`, confirm your own identity with `entwurf_self`, drain a mailbox with `entwurf_inbox_read`, and use `entwurf_register_native` only as the explicit/manual fallback for binding an already-running agy conversation (normal agy birth is automatic through the installed hook). Open a NEW sibling with **`entwurf_fresh_call`**, and reopen a DORMANT pi citizen under its own garden id with **`entwurf_resume_call {target}`**. Fresh call needs its selected runtime (`pi` or `claude`) on the server's PATH; resume call always needs `pi`. Both start a runtime, while delivery does not. (The old v1 verbs `entwurf` / `entwurf_resume` / `entwurf_send` are gone.)
12
+ **Which verb an external agent should reach for:** to deliver to / reply to a garden id, use **`entwurf_v2`** — it is the canonical delivery surface and the only one that reads whether the target is live pi, dormant pi, mailbox-backed Claude Code, or native-push Antigravity and routes correctly. Discover targets with `entwurf_peers`, confirm your own identity with `entwurf_self`, drain a mailbox with `entwurf_inbox_read`, and use `entwurf_register_native` only as the explicit/manual fallback for binding an already-running agy conversation (normal agy birth is automatic through the installed hook). Open a NEW sibling with **`entwurf_fresh_call {backend, model, task, cwd?}`**, and reopen a DORMANT pi citizen under its own garden id with **`entwurf_resume_call {target}`**. Fresh call accepts one optional literal absolute `cwd`: omit it or pass `""` for the caller's cwd; otherwise it must name an existing directory and may not contain `#`. Use that input for a new cross-repository sibling — resume preserves a dormant Pi citizen's recorded continuity and is not a cwd substitute. Fresh call needs its selected runtime (`pi` or `claude`) on the server's PATH; resume call always needs `pi`. Both start a runtime, while delivery does not. (The old v1 verbs `entwurf` / `entwurf_resume` / `entwurf_send` are gone.)
13
13
 
14
14
  Observed: Claude Code, Codex CLI, and Antigravity CLI all reach the read surfaces through this MCP bridge from a plain external host — `entwurf_peers` is a pure fact projection, while `entwurf_inbox_read` is a **mutating drain** (it archives the messages and stamps the read-receipt), so "read" here does not mean side-effect-free; **sending** needs an identity lane. Claude becomes symmetric/replyable through its mailbox-backed meta-session; agy becomes symmetric/replyable through its record-backed sender marker plus live native-push probe. Codex has no managed citizen lifecycle yet, so a Codex host cannot send without the explicit anonymous hatch below.
15
15
 
@@ -26,7 +26,8 @@
26
26
  * this read is the receipt.
27
27
  * - entwurf_register_native — explicit/manual fallback binding an ALREADY-RUNNING native
28
28
  * conversation (antigravity) to a garden id. Never a spawn.
29
- * - entwurf_fresh_call — open ONE fresh visible sibling in the operator's own tmux session;
29
+ * - entwurf_fresh_call — open ONE fresh visible sibling in the operator's own tmux session,
30
+ * optionally at ONE literal requested cwd (cross-repo fresh, #73);
30
31
  * returns a LAUNCH receipt only, and the new address arrives later as the
31
32
  * sender envelope of the sibling's nonce callback.
32
33
  * - entwurf_resume_call — reopen ONE DORMANT pi citizen under its OWN garden id in a visible
@@ -568,8 +569,10 @@ server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling in the operato
568
569
  "else: it does NOT mean the runtime started, the first turn ran, or the task was delivered. Nothing polls " +
569
570
  "for the callback; if it never arrives the window is visible and can be read directly. For EXISTING " +
570
571
  "citizens use entwurf_v2 — this tool only creates, and entwurf_peers only reports. Model is REQUIRED and " +
571
- "is passed to the chosen runtime CLI (`provider/model` for pi; model id/alias for Claude Code); there are no " +
572
- "arbitrary command/cwd/env knobs. Do not put secrets in the task model and task argv are visible to " +
572
+ "is passed to the chosen runtime CLI (`provider/model` for pi; model id/alias for Claude Code). An optional " +
573
+ "cwd starts the sibling in ONE literal absolute existing directory (cross-repo fresh) never pick resume " +
574
+ "for a dormant record's cwd; resume is continuity-only. Omitted/empty cwd means the caller's own directory. " +
575
+ "There are no arbitrary command/env knobs. Do not put secrets in the task — model and task argv are visible to " +
573
576
  "same-user processes on this host. Requires that this agent itself runs " +
574
577
  "inside tmux: without a pane anchor there is no session to open a sibling beside.", {
575
578
  backend: z
@@ -594,7 +597,11 @@ server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling in the operato
594
597
  .min(1)
595
598
  .max(16000)
596
599
  .describe("What the sibling should do after it calls you back. Plain instructions; no secrets (see the tool description)."),
597
- }, async ({ backend, model, task }) => {
600
+ cwd: z
601
+ .string()
602
+ .optional()
603
+ .describe("Optional literal ABSOLUTE path of an existing directory to start the sibling in (cross-repo fresh). Omit or pass \"\" to start in this agent's own cwd. Taken exactly as given — no trim, no realpath, no project-name resolution; '#' is refused (tmux format expansion). The receipt echoes what was REQUESTED, never an observation."),
604
+ }, async ({ backend, model, task, cwd }) => {
598
605
  let callerGardenId = null;
599
606
  try {
600
607
  const self = await buildAuthoritativeSelfEnvelope();
@@ -613,7 +620,7 @@ server.tool("entwurf_fresh_call", "Open ONE fresh visible sibling in the operato
613
620
  callerGardenId = null;
614
621
  }
615
622
  try {
616
- const rendered = renderFreshCall(freshCall({ backend, model, task, callerGardenId }));
623
+ const rendered = renderFreshCall(freshCall({ backend, model, task, cwd, callerGardenId }));
617
624
  return rendered.isError ? textErr(rendered.text) : textOk(rendered.text);
618
625
  }
619
626
  catch (err) {
@@ -0,0 +1,47 @@
1
+ /**
2
+ * classify-tmux-cwd — the ONE classification of a start directory that is about to be handed
3
+ * to tmux as a `-c` value. Shared leaf of the resume and fresh launch compositions; it owns
4
+ * the classification and NOTHING else — no hints (each consumer phrases its own: resume says
5
+ * "recorded cwd", fresh says "requested cwd"), no argv, no tmux, no fallback directory.
6
+ *
7
+ * Every rule below is a MEASURED tmux 3.6a behaviour (2026-08-06, private server), and each
8
+ * one is a way a launch would look successful while being wrong:
9
+ *
10
+ * 1. a NONEXISTENT `-c` is silent. tmux exits 0, opens the window, and the child falls back
11
+ * to `$HOME`. A launch whose directory has been deleted would therefore open a visible
12
+ * window in the wrong project and look successful. Nothing downstream can catch that:
13
+ * the launch receipt would be perfectly well-formed.
14
+ * 2. `-c` is FORMAT-EXPANDED. `#{pane_id}` inside the value silently rewrote the path
15
+ * (`<dir>/#{pane_id}` → `<dir>/%0`), and a `#(…)` value was observed running its
16
+ * command. A path is data; tmux reads it as a format. So `#` is refused outright.
17
+ * 3. whitespace is SAFE — argv is an array and nothing re-splits. A dir named `with space`
18
+ * arrived intact. So there is no quoting grammar here, and none is owed.
19
+ *
20
+ * That is the entire defence: one existence check and one character. No escaping layer, no
21
+ * sanitiser, no trim, no realpath/symlink policy — a symlinked project dir is a normal thing
22
+ * to work in, and a value is classified exactly as given.
23
+ */
24
+ import { statSync } from "node:fs";
25
+ import path from "node:path";
26
+ /**
27
+ * Classify a candidate start directory. Split into separate reasons rather than one because
28
+ * the operator's next move differs: an absolute-path bug is a caller defect, a missing
29
+ * directory is a moved/deleted project, and a `#` is a path tmux would rewrite under us.
30
+ */
31
+ export function classifyTmuxCwd(cwd) {
32
+ if (!path.isAbsolute(cwd))
33
+ return "cwd-not-absolute";
34
+ // tmux expands formats inside the `-c` VALUE. `#{…}` rewrote the path silently and `#(…)`
35
+ // was observed executing; neither is something to escape our way out of.
36
+ if (cwd.includes("#"))
37
+ return "cwd-format-token";
38
+ let st;
39
+ try {
40
+ st = statSync(cwd);
41
+ }
42
+ catch {
43
+ // tmux would NOT report this — it opens the window and lands the child in $HOME.
44
+ return "cwd-missing";
45
+ }
46
+ return st.isDirectory() ? null : "cwd-not-directory";
47
+ }
@@ -25,8 +25,27 @@
25
25
  * caller's own inbound surface. Merging them would claim knowledge this module cannot have.
26
26
  * 4. A launch with no callback is a REAL outcome, not an error to retry. No watcher, no poll,
27
27
  * no timeout supervisor. The window is visible; the operator can look.
28
+ *
29
+ * ── The optional REQUESTED cwd (issue #73) ──
30
+ *
31
+ * A fresh sibling starts wherever the caller happens to be — unless the caller names ONE
32
+ * literal start directory. That input exists so a cross-repo fresh consultation never has to
33
+ * ride `entwurf_resume_call` for a dormant record's recorded cwd: resume stays a continuity
34
+ * verb, and placement pressure stays here. The rules are deliberately narrow:
35
+ *
36
+ * - `undefined` and the exact empty string mean OMIT: no `-c` reaches tmux and the argv is
37
+ * byte-identical to the pre-#73 shape. Anything else is taken LITERALLY — no trim, no
38
+ * realpath, no project-name resolution, no store/peers/record lookup. The caller is the
39
+ * only cwd authority this module knows.
40
+ * - the value is classified by the shared `classify-tmux-cwd.ts` leaf BEFORE any mutation
41
+ * (same four stable reasons as resume; the measured tmux 3.6a facts live on that leaf).
42
+ * This module's hints phrase them as the REQUESTED cwd; resume's say RECORDED.
43
+ * - the receipt echoes what was REQUESTED, exactly as `runtimePath` does. It never reports
44
+ * `pane_current_path`: proving where the pane actually landed belongs to acceptance, not
45
+ * to the launch receipt.
28
46
  */
29
47
  import { randomBytes } from "node:crypto";
48
+ import { classifyTmuxCwd } from "./classify-tmux-cwd.js";
30
49
  import { assertLaunchTarget, LaunchPreconditionError, resolveRuntimeOnPath, } from "./mux-launch.js";
31
50
  import { APPEND_FORMAT, assertSelector, assertTmuxOk, inspectPlacement, parseWindowFields, requireSameContext, runTmux, } from "./mux-placement.js";
32
51
  /** The two backends this rail can open. Fixed set, not a profile — a third one is a decision,
@@ -118,16 +137,24 @@ export function mintNonce(randomHex = defaultRandomHex) {
118
137
  function defaultRandomHex() {
119
138
  return randomBytes(12).toString("hex");
120
139
  }
121
- /** Launch argv: the leaf's detached-append shape, the runtime, then the backend's dialect. */
122
- export function buildFreshCallArgs(placement, runtimePath, backendArgs) {
140
+ /** Launch argv: the leaf's detached-append shape, optionally `-c` at the resume-symmetric token
141
+ * position (after `-t`, before `-P -F`), the runtime, then the backend's dialect. An omitted cwd
142
+ * yields the exact pre-#73 argv — no carrier at all. */
143
+ export function buildFreshCallArgs(placement, runtimePath, backendArgs, cwd) {
123
144
  assertSelector("session", placement.sessionId);
124
145
  assertLaunchTarget(runtimePath);
146
+ if (cwd !== undefined) {
147
+ const bad = classifyTmuxCwd(cwd);
148
+ if (bad)
149
+ throw new Error(`mux-fresh-call: refusing to build argv with an unusable cwd (${bad}): ${cwd}`);
150
+ }
125
151
  return [
126
152
  "new-window",
127
153
  "-d",
128
154
  "-a",
129
155
  "-t",
130
156
  `${placement.sessionId}:{end}`,
157
+ ...(cwd === undefined ? [] : ["-c", cwd]),
131
158
  "-P",
132
159
  "-F",
133
160
  APPEND_FORMAT,
@@ -159,6 +186,15 @@ export function freshCall(params, env = process.env, nonce = mintNonce()) {
159
186
  return { ok: false, reason: "task-empty" };
160
187
  if (task.length > TASK_MAX_CHARS)
161
188
  return { ok: false, reason: "task-too-long" };
189
+ // ONLY `undefined` and the exact empty string mean "no cwd". Everything else is the literal
190
+ // value — deliberately untrimmed, so a whitespace-mangled path is refused loudly by the
191
+ // classification below instead of being silently repaired into a different directory.
192
+ const cwd = params.cwd === undefined || params.cwd === "" ? undefined : params.cwd;
193
+ if (cwd !== undefined) {
194
+ const badCwd = classifyTmuxCwd(cwd);
195
+ if (badCwd)
196
+ return { ok: false, reason: badCwd };
197
+ }
162
198
  let runtimePath;
163
199
  try {
164
200
  runtimePath = resolveRuntimeOnPath(FRESH_CALL_RUNTIME[params.backend], env);
@@ -179,7 +215,7 @@ export function freshCall(params, env = process.env, nonce = mintNonce()) {
179
215
  callerGardenId: params.callerGardenId,
180
216
  nonce,
181
217
  });
182
- const run = runTmux(buildFreshCallArgs(placement, runtimePath, buildBackendArgs(params.backend, prompt, model)), env);
218
+ const run = runTmux(buildFreshCallArgs(placement, runtimePath, buildBackendArgs(params.backend, prompt, model), cwd), env);
183
219
  assertTmuxOk("new-window", run);
184
220
  let fields;
185
221
  try {
@@ -199,6 +235,7 @@ export function freshCall(params, env = process.env, nonce = mintNonce()) {
199
235
  ...fields,
200
236
  backend: params.backend,
201
237
  model,
238
+ ...(cwd === undefined ? {} : { cwd }),
202
239
  runtimePath,
203
240
  nonce,
204
241
  },
@@ -212,6 +249,10 @@ const REJECT_HINT = {
212
249
  "anchor-unresolved": "tmux resolved no pane for this agent's anchor",
213
250
  "anchor-mismatch": "tmux answered about a different pane than the one asked about",
214
251
  "caller-identity-unavailable": "this surface has no record-backed garden id for the caller, so the sibling would have no address to call back to",
252
+ "cwd-not-absolute": "the requested cwd is not an absolute path (the value is taken literally — nothing trims or resolves it)",
253
+ "cwd-format-token": "the requested cwd contains '#', which tmux expands as a format inside -c — it would silently rewrite the path or run a command",
254
+ "cwd-missing": "the requested cwd does not exist; tmux would not report this, it would open the window in $HOME and look successful",
255
+ "cwd-not-directory": "the requested cwd exists but is not a directory",
215
256
  "model-empty": "model is empty after trimming; fresh calls require an explicit model",
216
257
  "model-invalid": `model must be one ${MODEL_MAX_CHARS}-character argv-safe id/alias without whitespace or tmux syntax`,
217
258
  "task-empty": "task is empty after trimming",
@@ -244,6 +285,7 @@ export function renderFreshCall(result) {
244
285
  text: `[entwurf fresh call →]\n` +
245
286
  ` backend: ${r.backend} (${r.runtimePath})\n` +
246
287
  ` model: ${r.model} (requested on the runtime CLI)\n` +
288
+ (r.cwd === undefined ? "" : ` cwd: ${r.cwd} (requested start directory — not an observation)\n`) +
247
289
  ` window: ${r.windowId} (index ${r.windowIndex}) in session ${r.sessionId}\n` +
248
290
  ` pane: ${r.paneId} pid ${r.panePid}\n` +
249
291
  ` nonce: ${r.nonce}\n` +
@@ -4,12 +4,13 @@
4
4
  *
5
5
  * ── Why this is a module and not a parameter on fresh-call ──
6
6
  *
7
- * `mux-fresh-call` carries a TASK to a runtime it names; the sibling starts wherever the caller
8
- * happens to be. A resume carries neither: the argv comes from the record (`entwurf-v2-visible-
9
- * resume` builds it) and the cwd comes from the record too it is the directory the citizen's
10
- * own transcript header remembers. Those are different inputs with a different risk, so they get
11
- * a different module rather than a fourth parameter on a composition whose contract is
12
- * "identity is an OUTPUT".
7
+ * `mux-fresh-call` carries a TASK to a runtime it names, and starts the sibling wherever the
8
+ * caller happens to be unless the caller REQUESTS one literal start directory (#73). A resume
9
+ * carries neither a task nor a caller choice: the argv comes from the record (`entwurf-v2-
10
+ * visible-resume` builds it) and the cwd comes from the record too it is the directory the
11
+ * citizen's own transcript header remembers, never something the caller picks. Those are
12
+ * different inputs with a different risk, so they get a different module rather than a fourth
13
+ * parameter on a composition whose contract is "identity is an OUTPUT".
13
14
  *
14
15
  * visible-resume composition → resume-call → placement leaf (unchanged, carrier-free)
15
16
  * resume-call -X-> garden identity, records, locks, delivery
@@ -20,22 +21,15 @@
20
21
  *
21
22
  * `mux-placement`'s `buildAppendArgs` deliberately emits no `-c` ("default shell only"), and it
22
23
  * stays that way — a resume must not widen the leaf's grammar for the three other callers. So
23
- * the `-c` shape lives here, with the three refusals MEASURED on tmux 3.6a (2026-08-06, private
24
- * server):
24
+ * the `-c` SHAPE lives here, while the classification of the value lives in the shared
25
+ * `classify-tmux-cwd.ts` leaf (fresh-call hands tmux the same flag, and a twin copy of the
26
+ * measured rules would rot apart on the next tmux hazard). The measured tmux 3.6a facts —
27
+ * a nonexistent `-c` silently lands the child in `$HOME`, `#` is format-expanded, whitespace
28
+ * is safe — are documented on that leaf. `|` is fine too: the cwd never enters the `-F` row
29
+ * (see `APPEND_FORMAT` below).
25
30
  *
26
- * 1. a NONEXISTENT `-c` is silent. tmux exits 0, opens the window, and the child falls back to
27
- * `$HOME`. A resume whose recorded cwd has been deleted would therefore open a visible
28
- * window in the wrong project and look successful. Nothing downstream can catch that: the
29
- * launch receipt would be perfectly well-formed.
30
- * 2. `-c` is FORMAT-EXPANDED. `#{pane_id}` inside the value silently rewrote the path
31
- * (`<dir>/#{pane_id}` → `<dir>/%0`), and a `#(…)` value was observed running its command.
32
- * A path is data; tmux reads it as a format. So `#` is refused outright.
33
- * 3. whitespace is SAFE — argv is an array and nothing re-splits. A dir named `with space`
34
- * arrived intact. So there is no quoting grammar here, and none is owed. `|` is fine too:
35
- * the cwd never enters the `-F` row (see `APPEND_FORMAT` below).
36
- *
37
- * That is the entire defence: one existence check and one character. No escaping layer, no
38
- * sanitiser, no symlink policy — a symlinked project dir is a normal thing to work in.
31
+ * What stays HERE is the phrasing: this module's hints say "recorded cwd", because a resume's
32
+ * directory comes from the record fresh-call's say "requested cwd" for the same reasons.
39
33
  *
40
34
  * ── What the receipt does NOT say ──
41
35
  *
@@ -46,36 +40,13 @@
46
40
  * where the pane actually landed is a separate query against the stable pane id, and it belongs
47
41
  * to acceptance, not to the product's launch receipt.
48
42
  */
49
- import { statSync } from "node:fs";
50
- import path from "node:path";
43
+ import { classifyTmuxCwd } from "./classify-tmux-cwd.js";
51
44
  import { assertLaunchTarget, LaunchPreconditionError, resolveRuntimeOnPath, } from "./mux-launch.js";
52
45
  import { APPEND_FORMAT, assertSelector, assertTmuxOk, inspectPlacement, parseWindowFields, requireSameContext, runTmux, } from "./mux-placement.js";
53
46
  /** The fixed runtime a resume reopens. Only pi stands a control socket up, so only pi has a
54
47
  * same-id resume at all — the backend boundary is enforced upstream by
55
48
  * `resolveResumeLaunchIdentity`, and this constant is the mux half of the same fact. */
56
49
  export const RESUME_CALL_RUNTIME = "pi";
57
- /**
58
- * Classify a candidate start directory. Split into three reasons rather than one because the
59
- * operator's next move differs: an absolute-path bug is a caller defect, a missing directory is
60
- * a moved/deleted project, and a `#` is a path tmux would rewrite under us.
61
- */
62
- export function classifyResumeCwd(cwd) {
63
- if (!path.isAbsolute(cwd))
64
- return "cwd-not-absolute";
65
- // tmux expands formats inside the `-c` VALUE. `#{…}` rewrote the path silently and `#(…)`
66
- // was observed executing; neither is something to escape our way out of.
67
- if (cwd.includes("#"))
68
- return "cwd-format-token";
69
- let st;
70
- try {
71
- st = statSync(cwd);
72
- }
73
- catch {
74
- // tmux would NOT report this — it opens the window and lands the child in $HOME.
75
- return "cwd-missing";
76
- }
77
- return st.isDirectory() ? null : "cwd-not-directory";
78
- }
79
50
  /**
80
51
  * Launch argv: the leaf's detached-append shape plus `-c`, the runtime, then the caller's flags.
81
52
  * `--` is what keeps tmux from reading the runtime or its flags as tmux options.
@@ -83,7 +54,7 @@ export function classifyResumeCwd(cwd) {
83
54
  export function buildResumeCallArgs(placement, cwd, runtimePath, runtimeArgs) {
84
55
  assertSelector("session", placement.sessionId);
85
56
  assertLaunchTarget(runtimePath);
86
- const bad = classifyResumeCwd(cwd);
57
+ const bad = classifyTmuxCwd(cwd);
87
58
  if (bad)
88
59
  throw new Error(`mux-resume-call: refusing to build argv with an unusable cwd (${bad}): ${cwd}`);
89
60
  return [
@@ -110,7 +81,7 @@ export function buildResumeCallArgs(placement, cwd, runtimePath, runtimeArgs) {
110
81
  * `runtimeArgs` is passed through untouched. This module does not know what `--session` means.
111
82
  */
112
83
  export function resumeCall(params, env = process.env) {
113
- const badCwd = classifyResumeCwd(params.cwd);
84
+ const badCwd = classifyTmuxCwd(params.cwd);
114
85
  if (badCwd)
115
86
  return { ok: false, reason: badCwd };
116
87
  let runtimePath;
@@ -26,7 +26,8 @@
26
26
  * this read is the receipt.
27
27
  * - entwurf_register_native — explicit/manual fallback binding an ALREADY-RUNNING native
28
28
  * conversation (antigravity) to a garden id. Never a spawn.
29
- * - entwurf_fresh_call — open ONE fresh visible sibling in the operator's own tmux session;
29
+ * - entwurf_fresh_call — open ONE fresh visible sibling in the operator's own tmux session,
30
+ * optionally at ONE literal requested cwd (cross-repo fresh, #73);
30
31
  * returns a LAUNCH receipt only, and the new address arrives later as the
31
32
  * sender envelope of the sibling's nonce callback.
32
33
  * - entwurf_resume_call — reopen ONE DORMANT pi citizen under its OWN garden id in a visible
@@ -661,8 +662,10 @@ server.tool(
661
662
  "else: it does NOT mean the runtime started, the first turn ran, or the task was delivered. Nothing polls " +
662
663
  "for the callback; if it never arrives the window is visible and can be read directly. For EXISTING " +
663
664
  "citizens use entwurf_v2 — this tool only creates, and entwurf_peers only reports. Model is REQUIRED and " +
664
- "is passed to the chosen runtime CLI (`provider/model` for pi; model id/alias for Claude Code); there are no " +
665
- "arbitrary command/cwd/env knobs. Do not put secrets in the task model and task argv are visible to " +
665
+ "is passed to the chosen runtime CLI (`provider/model` for pi; model id/alias for Claude Code). An optional " +
666
+ "cwd starts the sibling in ONE literal absolute existing directory (cross-repo fresh) never pick resume " +
667
+ "for a dormant record's cwd; resume is continuity-only. Omitted/empty cwd means the caller's own directory. " +
668
+ "There are no arbitrary command/env knobs. Do not put secrets in the task — model and task argv are visible to " +
666
669
  "same-user processes on this host. Requires that this agent itself runs " +
667
670
  "inside tmux: without a pane anchor there is no session to open a sibling beside.",
668
671
  {
@@ -690,8 +693,14 @@ server.tool(
690
693
  .describe(
691
694
  "What the sibling should do after it calls you back. Plain instructions; no secrets (see the tool description).",
692
695
  ),
696
+ cwd: z
697
+ .string()
698
+ .optional()
699
+ .describe(
700
+ "Optional literal ABSOLUTE path of an existing directory to start the sibling in (cross-repo fresh). Omit or pass \"\" to start in this agent's own cwd. Taken exactly as given — no trim, no realpath, no project-name resolution; '#' is refused (tmux format expansion). The receipt echoes what was REQUESTED, never an observation.",
701
+ ),
693
702
  },
694
- async ({ backend, model, task }) => {
703
+ async ({ backend, model, task, cwd }) => {
695
704
  let callerGardenId: string | null = null;
696
705
  try {
697
706
  const self = await buildAuthoritativeSelfEnvelope();
@@ -708,7 +717,7 @@ server.tool(
708
717
  callerGardenId = null;
709
718
  }
710
719
  try {
711
- const rendered = renderFreshCall(freshCall({ backend, model, task, callerGardenId }));
720
+ const rendered = renderFreshCall(freshCall({ backend, model, task, cwd, callerGardenId }));
712
721
  return rendered.isError ? textErr(rendered.text) : textOk(rendered.text);
713
722
  } catch (err) {
714
723
  return textErr(`entwurf_fresh_call error: ${err instanceof Error ? err.message : String(err)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "Garden-citizen dispatch substrate and meta-bridge for Claude Code, Codex, Antigravity, and pi harnesses.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -1489,7 +1489,7 @@ const MUX_FRESH_CALL_MODULE = "./lib/mux-fresh-call.ts";
1489
1489
 
1490
1490
  interface MuxFreshCallModule {
1491
1491
  freshCall(
1492
- params: { backend: "pi" | "claude-code"; model: string; task: string; callerGardenId: string | null },
1492
+ params: { backend: "pi" | "claude-code"; model: string; task: string; cwd?: string; callerGardenId: string | null },
1493
1493
  env?: NodeJS.ProcessEnv,
1494
1494
  ): { ok: boolean };
1495
1495
  renderFreshCall(result: { ok: boolean }): { text: string; isError: boolean };
@@ -1520,8 +1520,10 @@ not exist a moment ago. This returns a LAUNCH receipt (tmux window/pane plus tha
1520
1520
  it does NOT mean the runtime started, the first turn ran, or the task was delivered. Nothing polls for the
1521
1521
  callback; if it never arrives the window is visible and can be read directly. For EXISTING citizens use
1522
1522
  entwurf_v2 — this tool only creates, and entwurf_peers only reports. Model is REQUIRED and passed to the
1523
- chosen runtime CLI (provider/model for pi; model id/alias for Claude Code); there are no arbitrary
1524
- command/cwd/env knobs. Do not put secrets in the taskmodel and task argv are visible to same-user
1523
+ chosen runtime CLI (provider/model for pi; model id/alias for Claude Code). An optional cwd starts the
1524
+ sibling in ONE literal absolute existing directory (cross-repo fresh)never pick resume for a dormant
1525
+ record's cwd; resume is continuity-only. Omitted/empty cwd means the caller's own directory. There are no
1526
+ arbitrary command/env knobs. Do not put secrets in the task — model and task argv are visible to same-user
1525
1527
  processes on this host.`,
1526
1528
  parameters: Type.Object({
1527
1529
  backend: StringEnum(["pi", "claude-code"], {
@@ -1539,10 +1541,16 @@ processes on this host.`,
1539
1541
  description:
1540
1542
  "What the sibling should do after it calls you back. Plain instructions; no secrets (see the tool description).",
1541
1543
  }),
1544
+ cwd: Type.Optional(
1545
+ Type.String({
1546
+ description:
1547
+ "Optional literal ABSOLUTE path of an existing directory to start the sibling in (cross-repo fresh). Omit or pass \"\" to start in this agent's own cwd. Taken exactly as given — no trim, no realpath, no project-name resolution; '#' is refused (tmux format expansion). The receipt echoes what was REQUESTED, never an observation.",
1548
+ }),
1549
+ ),
1542
1550
  }),
1543
1551
  async execute(
1544
1552
  _toolCallId: string,
1545
- params: { backend: "pi" | "claude-code"; model: string; task: string },
1553
+ params: { backend: "pi" | "claude-code"; model: string; task: string; cwd?: string },
1546
1554
  _signal: AbortSignal | undefined,
1547
1555
  _onUpdate: unknown,
1548
1556
  _ctx: ExtensionContext,
@@ -1553,6 +1561,7 @@ processes on this host.`,
1553
1561
  backend: params.backend,
1554
1562
  model: params.model,
1555
1563
  task: params.task,
1564
+ cwd: params.cwd,
1556
1565
  callerGardenId: residentGardenId,
1557
1566
  });
1558
1567
  const rendered = mux.renderFreshCall(result);