@junghanacs/entwurf 0.22.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -1
- package/BASELINE.md +1 -0
- package/CHANGELOG.md +514 -0
- package/DELIVERY.md +10 -2
- package/README.md +43 -4
- package/VERIFY.md +47 -0
- package/docs/acp-backend-rail.md +96 -17
- package/docs/setup-clean-host.md +11 -1
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +39 -40
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +95 -56
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +46 -273
- package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
- package/mcp/entwurf-bridge/src/index.ts +41 -41
- package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
- package/package.json +9 -8
- package/pi-extensions/entwurf-control.ts +91 -52
- package/pi-extensions/lib/acp/acp-client.ts +3 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
- package/pi-extensions/lib/acp/backend.ts +4 -4
- package/pi-extensions/lib/acp/event-mapper.ts +5 -5
- package/pi-extensions/lib/acp/tool-surface.ts +2 -1
- package/pi-extensions/lib/codex-declaration.js +612 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +107 -51
- package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
- package/pi-extensions/lib/entwurf-facts.ts +14 -1
- package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
- package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
- package/pi-extensions/lib/entwurf-self-address.ts +33 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
- package/pi-extensions/lib/fresh-call-composition.ts +493 -0
- package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
- package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
- package/pi-extensions/lib/herdr-placement.ts +276 -0
- package/pi-extensions/lib/mux-fresh-call.ts +95 -301
- package/run.sh +588 -204
- package/scripts/check-acp-usage-accounting.ts +9 -9
- package/scripts/check-codex-birth-hook.ts +187 -0
- package/scripts/check-entwurf-control-rpc.ts +138 -0
- package/scripts/check-entwurf-fact-provider.ts +4 -2
- package/scripts/check-entwurf-peers-surface.ts +3 -1
- package/scripts/check-entwurf-self-address.ts +68 -0
- package/scripts/check-fresh-call-dispatch.ts +301 -0
- package/scripts/check-gate-qualification.ts +17 -4
- package/scripts/check-herdr-activation.ts +1293 -0
- package/scripts/check-herdr-fresh-call.ts +1250 -0
- package/scripts/check-herdr-placement.ts +383 -0
- package/scripts/check-herdr-plugin-build.ts +842 -0
- package/scripts/check-herdr-plugin-profile.ts +371 -0
- package/scripts/check-herdr-plugin.ts +523 -0
- package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
- package/scripts/check-herdr-sandbox.ts +456 -0
- package/scripts/check-herdr-supply.ts +184 -0
- package/scripts/check-mux-launcher-fence.ts +138 -0
- package/scripts/check-peer-facts.ts +331 -0
- package/scripts/check-pi-launch.ts +358 -0
- package/scripts/check-release-gate-outcomes.ts +19 -0
- package/scripts/check-typing-call-fence.ts +296 -0
- package/scripts/codex-birth-doctor.sh +159 -16
- package/scripts/codex-birth-install.sh +178 -72
- package/scripts/codex-birth-uninstall.sh +162 -7
- package/scripts/fixtures/herdr-supply.json +29 -0
- package/scripts/herdr-activation.mjs +536 -0
- package/scripts/herdr-plugin-activate.mjs +270 -0
- package/scripts/herdr-plugin-deactivate.mjs +193 -0
- package/scripts/herdr-runtime.mjs +1203 -0
- package/scripts/install-herdr-ci.sh +96 -0
- package/scripts/inventory-verification-surface.ts +86 -10
- package/scripts/lib/codex-fresh-live-protocol.ts +2 -2
- package/scripts/lib/tree-digest.ts +97 -0
- package/scripts/meta-bridge-install.sh +19 -2
- package/scripts/meta-bridge-state.py +167 -11
- package/scripts/mutants/codex-caller-seat.json +6 -8
- package/scripts/mutants/codex-native.json +195 -29
- package/scripts/mutants/control-socket-disconnect.json +57 -0
- package/scripts/mutants/fresh-call-dispatch.json +154 -0
- package/scripts/mutants/herdr-activation.json +305 -0
- package/scripts/mutants/herdr-fresh-call.json +448 -0
- package/scripts/mutants/herdr-placement.json +161 -0
- package/scripts/mutants/herdr-plugin-build.json +160 -0
- package/scripts/mutants/herdr-plugin-profile.json +202 -0
- package/scripts/mutants/herdr-plugin.json +133 -0
- package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
- package/scripts/mutants/herdr-supply.json +133 -0
- package/scripts/mutants/meta-hook-session-switch.json +2 -2
- package/scripts/mutants/mux-fresh-call.json +104 -17
- package/scripts/mutants/mux-launcher-fence.json +13 -0
- package/scripts/mutants/omp-fresh.json +4 -4
- package/scripts/mutants/peer-facts.json +98 -0
- package/scripts/mutants/pi-launch.json +113 -0
- package/scripts/mutants/resume-args.json +6 -6
- package/scripts/mutants/self-address.json +33 -0
- package/scripts/mutants/typing-call-fence.json +17 -0
- package/scripts/mutants/v2-surface.json +2 -2
- package/scripts/mutants/v2-visible-resume.json +17 -17
- package/scripts/peer-facts.ts +120 -0
- package/scripts/register-pi-package.py +8 -0
- package/scripts/register-pi-provider.py +88 -12
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-codex-birth.sh +281 -12
- package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
- package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
- package/scripts/smoke-herdr-raw-install-live.sh +849 -0
- package/scripts/tsconfig.json +5 -0
- package/scripts/check-entwurf-facts.ts +0 -385
- package/scripts/check-entwurf-resume-args.ts +0 -122
- package/scripts/check-entwurf-v2-decider.ts +0 -657
- package/scripts/check-entwurf-v2-lock.ts +0 -371
- package/scripts/check-entwurf-v2-mailbox.ts +0 -261
- package/scripts/check-entwurf-v2-matrix.ts +0 -401
- package/scripts/check-entwurf-v2-native-push.ts +0 -221
- package/scripts/check-entwurf-v2-release.ts +0 -151
- package/scripts/check-entwurf-v2-runner.ts +0 -448
- package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
- package/scripts/check-entwurf-v2-send.ts +0 -458
- package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
- package/scripts/check-socket-discovery.ts +0 -401
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* peer-facts — the read-only JSON projection of the OBSERVED peer listing (#116 M2-a).
|
|
4
|
+
*
|
|
5
|
+
* `meta-facts` (#65) emits what the meta-record STORE owns. This emits what the fact
|
|
6
|
+
* PROVIDER owns: the same `listEntwurfFacts` assembly `entwurf_peers` renders, serialized
|
|
7
|
+
* instead of drawn. It exists for the same reason #65 exists — so a consumer stops
|
|
8
|
+
* carrying a decaying copy of a join it does not own.
|
|
9
|
+
*
|
|
10
|
+
* THE COPY THIS REFUSES. The placement axis joins a herdr pane to a citizen on
|
|
11
|
+
* `nativeSessionId`, and on the pi side that key is recovered from a session FILENAME by a
|
|
12
|
+
* strict conversion measured against one vendor floor (`herdr-placement.ts`
|
|
13
|
+
* piNativeSessionIdFromPath). A consumer that re-implements that join forks a vendor floor
|
|
14
|
+
* into a file no gate covers and no document owns (`docs/mux-launch-rail.md` §7). With this
|
|
15
|
+
* verb the consumer's whole join becomes ONE opaque pane-id string equality, which is a
|
|
16
|
+
* thing a shell script may correctly do.
|
|
17
|
+
*
|
|
18
|
+
* ONE PROVIDER, ONE RENDERER, NO SECOND JOIN. The payload is `renderEntwurfPeers().payload`
|
|
19
|
+
* — the same provider (`listEntwurfFacts`) through the same renderer the MCP `entwurf_peers`
|
|
20
|
+
* surface uses, so the SHAPE is that surface's payload shape and each row is a `PeerFact`
|
|
21
|
+
* whose keyset `check-entwurf-peers-surface` pins to `{peers, diagnostics}`. Nothing here
|
|
22
|
+
* recomputes, filters, re-sorts or re-words a provider fact, and nothing here re-implements
|
|
23
|
+
* the join.
|
|
24
|
+
*
|
|
25
|
+
* It is NOT the same bytes as an `entwurf_peers` call, and the difference is deliberate, not
|
|
26
|
+
* a drift: that surface is a human listing, so it passes `observationLimit` and returns only
|
|
27
|
+
* rendered TEXT over the wire. This is a machine projection, so its observation is
|
|
28
|
+
* UNBOUNDED — a rationed row would say `unobserved`, which a machine consumer cannot tell
|
|
29
|
+
* apart from "nobody could look". Older rows can therefore read `exists`/`active` here where
|
|
30
|
+
* the human surface says `unobserved`. Same facts, measured for more rows.
|
|
31
|
+
*
|
|
32
|
+
* `placement` STAYS STRUCTURED. The human surface prints `herdr <pane>` so a reader never
|
|
33
|
+
* mistakes a pane id for one of ours; a machine consumer needs the tagged union itself
|
|
34
|
+
* (`{kind:"herdr-pane",paneId}` / `unobserved` / `none` / `ambiguous`), so `renderPlacement`
|
|
35
|
+
* is NOT applied here. A pane is an ephemeral view (Hard Rule 16), never an address.
|
|
36
|
+
*
|
|
37
|
+
* WHAT IS DELIBERATELY ABSENT. No herdr `agent_status`, no `interactive_ready`, no screen
|
|
38
|
+
* text: those are herdr's own display verdicts, they are not delivery evidence
|
|
39
|
+
* (`docs/herdr-launch-rail.md` §9), and keeping them out of every entwurf payload is what
|
|
40
|
+
* makes "observed activity" unable to drift into delivery liveness. A consumer that wants
|
|
41
|
+
* to show them reads herdr itself and labels them as herdr's report.
|
|
42
|
+
*
|
|
43
|
+
* ONE PLACEMENT READ. `readPlacementIndex` is left undefined, so the provider performs its
|
|
44
|
+
* own single read per listing — the anti-watcher shape `entwurf-fact-provider.ts` documents.
|
|
45
|
+
* This verb never loops, retries, or waits for a pane whose session reference has not landed.
|
|
46
|
+
*
|
|
47
|
+
* NO SOCKET COORDINATE LEAVES THIS VERB. `ENTWURF_DIR` selects the socket world that is
|
|
48
|
+
* PROBED, and that is all it does: the control dir is never emitted. #50 C4 retired the
|
|
49
|
+
* legacy `sessions` projection "with the `controlDir` it exposed" because the record is the
|
|
50
|
+
* sole address axis and socket paths are dispatch-internal transport
|
|
51
|
+
* (`entwurf-peers-render.ts` header). Re-publishing that path under a new verb would undo
|
|
52
|
+
* that retirement, and no renderer needs it.
|
|
53
|
+
*
|
|
54
|
+
* stdout (deterministic, 2-space indent, trailing newline):
|
|
55
|
+
* {
|
|
56
|
+
* "schemaVersion": 1, // of THIS projection
|
|
57
|
+
* "storeDir": "/abs", // the meta-record store that was read
|
|
58
|
+
* "peers": [ <PeerFact, verbatim, provider order> ],
|
|
59
|
+
* "diagnostics": [ <EntwurfDiagnostic, verbatim> ]
|
|
60
|
+
* }
|
|
61
|
+
*
|
|
62
|
+
* EXIT CONTRACT — identical in shape to meta-facts, for the same reason:
|
|
63
|
+
* 0 — the store was readable; the JSON above is on stdout (diagnostics in-band).
|
|
64
|
+
* A store that does not exist is a readable EMPTY store (ENOENT only).
|
|
65
|
+
* 2 — usage error (bad argv).
|
|
66
|
+
* 3 — the store could not be READ. No JSON is emitted: an unreadable host must
|
|
67
|
+
* never look like an empty one.
|
|
68
|
+
* A provider wiring invariant (duplicate identity / unprobed in-domain citizen) is left to
|
|
69
|
+
* THROW. It is not an unreadable store and must not be dressed as one (Hard Rule 15).
|
|
70
|
+
*/
|
|
71
|
+
import os from "node:os";
|
|
72
|
+
import { defaultControlSocketDir } from "../pi-extensions/lib/control-socket-path.js";
|
|
73
|
+
import { listEntwurfFacts } from "../pi-extensions/lib/entwurf-fact-provider.js";
|
|
74
|
+
import { renderEntwurfPeers } from "../pi-extensions/lib/entwurf-peers-render.js";
|
|
75
|
+
import { defaultMetaSessionsDir, makeStoreRecordReader, readActiveStoreEntries, } from "../pi-extensions/lib/meta-session.js";
|
|
76
|
+
const arg = process.argv[2];
|
|
77
|
+
// A dash argv is a flag this command does not have, not a store directory — treating it as
|
|
78
|
+
// a path would answer `--help` with "empty store, exit 0", a silent wrong fact.
|
|
79
|
+
if (process.argv.length > 3 || (arg !== undefined && arg.startsWith("-"))) {
|
|
80
|
+
console.error("usage: entwurf peer-facts [meta-sessions-dir]");
|
|
81
|
+
process.exit(2);
|
|
82
|
+
}
|
|
83
|
+
const storeDir = arg ?? defaultMetaSessionsDir();
|
|
84
|
+
// The SAME override the bridge honours (`mcp/entwurf-bridge/src/index.ts` ENTWURF_DIR), so
|
|
85
|
+
// this verb and `entwurf_peers` probe one socket world. The pi side has no such override;
|
|
86
|
+
// the path grammar is the shared leaf either way. This value is an INPUT to the probe and
|
|
87
|
+
// never an output — see NO SOCKET COORDINATE LEAVES THIS VERB above.
|
|
88
|
+
const controlDir = process.env.ENTWURF_DIR ?? defaultControlSocketDir(os.homedir());
|
|
89
|
+
let metaEntries;
|
|
90
|
+
try {
|
|
91
|
+
// Entries WITH their kind, never bare names: the listing must be able to refuse a
|
|
92
|
+
// symlinked record without following it (Hard Rule 7). ENOENT is the empty store.
|
|
93
|
+
metaEntries = readActiveStoreEntries(storeDir);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
console.error(`FAIL: ${err instanceof Error ? err.message : String(err)}`);
|
|
97
|
+
process.exit(3); // see the EXIT CONTRACT above: 3 = unreadable store, never an empty one
|
|
98
|
+
}
|
|
99
|
+
const result = await listEntwurfFacts({
|
|
100
|
+
metaEntries,
|
|
101
|
+
readRecord: makeStoreRecordReader(storeDir),
|
|
102
|
+
socket: { dir: controlDir },
|
|
103
|
+
// No `observationLimit`: a budget would emit `unobserved` for rows nobody chose to
|
|
104
|
+
// skip, and in a machine payload that is indistinguishable from "no herdr here". The
|
|
105
|
+
// human surface may ration its own rows; a projection may not ration its facts.
|
|
106
|
+
});
|
|
107
|
+
const { payload } = renderEntwurfPeers(result);
|
|
108
|
+
const projection = { schemaVersion: 1, storeDir, ...payload };
|
|
109
|
+
process.stdout.write(`${JSON.stringify(projection, null, 2)}\n`);
|
|
@@ -65,7 +65,6 @@ import * as process from "node:process";
|
|
|
65
65
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
66
66
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
67
67
|
import { z } from "zod";
|
|
68
|
-
import { codexCallerFreshPreflight, codexFreshPreflight } from "../../../pi-extensions/lib/codex-fresh-preflight.ts";
|
|
69
68
|
import { controlSocketPathIn, defaultControlSocketDir } from "../../../pi-extensions/lib/control-socket-path.js";
|
|
70
69
|
import { resolveMailboxReceiverFacts } from "../../../pi-extensions/lib/entwurf-deliverability.ts";
|
|
71
70
|
import { listEntwurfFacts } from "../../../pi-extensions/lib/entwurf-fact-provider.ts";
|
|
@@ -80,6 +79,7 @@ import {
|
|
|
80
79
|
type VisibleResumeDeps,
|
|
81
80
|
visibleResume,
|
|
82
81
|
} from "../../../pi-extensions/lib/entwurf-v2-visible-resume.ts";
|
|
82
|
+
import { dispatchFreshCall, renderDispatchedFreshCall } from "../../../pi-extensions/lib/fresh-call-dispatch.ts";
|
|
83
83
|
import {
|
|
84
84
|
type CodexRequestSender,
|
|
85
85
|
probeNativeSenderAlive,
|
|
@@ -99,7 +99,6 @@ import {
|
|
|
99
99
|
readMetaSenderMarker,
|
|
100
100
|
requireBackend,
|
|
101
101
|
} from "../../../pi-extensions/lib/meta-session.ts";
|
|
102
|
-
import { freshCall, renderFreshCall } from "../../../pi-extensions/lib/mux-fresh-call.ts";
|
|
103
102
|
import { RESUME_CALL_REJECT_HINT, resumeCall } from "../../../pi-extensions/lib/mux-resume-call.ts";
|
|
104
103
|
import { registerNativeConversation } from "../../../pi-extensions/lib/native-push/register.ts";
|
|
105
104
|
|
|
@@ -716,30 +715,32 @@ server.tool(
|
|
|
716
715
|
// against that answer would call home to a garden id nobody holds.
|
|
717
716
|
server.tool(
|
|
718
717
|
"entwurf_fresh_call",
|
|
719
|
-
"Open ONE fresh visible sibling
|
|
720
|
-
"
|
|
721
|
-
"
|
|
722
|
-
"
|
|
723
|
-
"
|
|
724
|
-
"
|
|
725
|
-
"
|
|
726
|
-
"
|
|
727
|
-
"
|
|
728
|
-
"
|
|
729
|
-
"
|
|
730
|
-
"
|
|
731
|
-
"for a
|
|
732
|
-
"
|
|
733
|
-
"
|
|
734
|
-
"
|
|
735
|
-
"
|
|
736
|
-
"
|
|
737
|
-
"
|
|
738
|
-
"
|
|
718
|
+
"Open ONE fresh visible sibling beside you and hand it a first task. WHERE it opens is decided by where THIS " +
|
|
719
|
+
"agent runs, never by a parameter: inside herdr (HERDR_ENV=1) it opens a NEW UNFOCUSED TAB in your own " +
|
|
720
|
+
"herdr workspace, pi and claude-code " +
|
|
721
|
+
"only; everywhere else it opens in the operator's tmux with all five backends (pi, " +
|
|
722
|
+
"claude-code, copilot, omp, codex). No fallback: an incomplete herdr context is refused by name, " +
|
|
723
|
+
"never by a tmux window you cannot see from herdr. The sibling's FIRST action " +
|
|
724
|
+
"is a callback to you carrying a nonce, whose sender envelope is its garden id — that is how you learn the " +
|
|
725
|
+
"address of a thing that did not exist a moment ago. This returns a LAUNCH receipt (the owner's " +
|
|
726
|
+
"coordinates plus that nonce): it does NOT mean the runtime started, the first turn ran, or " +
|
|
727
|
+
"the task was delivered. Those coordinates are a VIEW, never an address — a herdr tab/pane id can change under " +
|
|
728
|
+
"a running sibling. Nothing polls. " +
|
|
729
|
+
"For EXISTING citizens use entwurf_v2 — this tool only creates, and entwurf_peers only reports. Model is REQUIRED and passed to the chosen " +
|
|
730
|
+
"runtime CLI (`provider/model` for pi, an id/alias for Claude Code, a pattern for the rest). On tmux, copilot/omp/codex are refused BEFORE any window " +
|
|
731
|
+
"opens when their birth, MCP, receive or visible-identity units are absent, and codex also needs the " +
|
|
732
|
+
"operator-owned app-server socket entwurf never starts; in herdr they are refused by name first. " +
|
|
733
|
+
"Optional placement.tmuxSession is a TMUX-ONLY seat naming ONE EXISTING session and " +
|
|
734
|
+
"ALWAYS wins; omitted, the seat follows the CALLER, never the backend opened: a CODEX CALLER opens beside " +
|
|
735
|
+
"its own TUI pane, every other caller in its own session; a missing one is tmux-session-missing and " +
|
|
736
|
+
"NOTHING is created. In herdr the field is refused by name. Do not put secrets in the task — model and task argv are visible to same-user " +
|
|
737
|
+
"processes.",
|
|
739
738
|
{
|
|
740
739
|
backend: z
|
|
741
740
|
.enum(["pi", "claude-code", "copilot", "omp", "codex"])
|
|
742
|
-
.describe(
|
|
741
|
+
.describe(
|
|
742
|
+
"Which fixed runtime to open. Only these five, and only pi/claude-code when this agent runs inside herdr; there is no arbitrary command.",
|
|
743
|
+
),
|
|
743
744
|
model: z
|
|
744
745
|
.string()
|
|
745
746
|
.min(1)
|
|
@@ -767,7 +768,7 @@ server.tool(
|
|
|
767
768
|
.string()
|
|
768
769
|
.optional()
|
|
769
770
|
.describe(
|
|
770
|
-
"Optional literal ABSOLUTE path of an existing directory to start the sibling in (cross-repo fresh).
|
|
771
|
+
"Optional literal ABSOLUTE path of an existing directory to start the sibling in (cross-repo fresh). Omit or pass \"\" to start where the CALLER is — this agent's own cwd on BOTH rails, or for a Codex caller its own record directory, because this bridge is the app-server's child and that process's directory is not the caller's. Taken exactly as given: no trim, no realpath, no project-name resolution. '#' is refused on the tmux rail only, because tmux format-expands a start directory; inside herdr it is an ordinary path character. The receipt echoes the directory that was REQUESTED or the caller record it came from, never an observation of where the pane landed.",
|
|
771
772
|
),
|
|
772
773
|
placement: z
|
|
773
774
|
.object({
|
|
@@ -779,7 +780,7 @@ server.tool(
|
|
|
779
780
|
})
|
|
780
781
|
.optional()
|
|
781
782
|
.describe(
|
|
782
|
-
"Optional expert seat override: open the sibling in ONE EXISTING tmux session of this agent's own server, and it always wins. When omitted the seat follows the CALLER, never the backend being opened: a Codex CALLER opens beside its own TUI pane, matched by thread-id in that pane's terminal title (0 or 2+ matching panes refuse, never fall back); every other caller opens in its own session. A pane title is a placement input only — never an address, liveness or delivery fact. Nothing is ever created. Independent of cwd; neither is inferred from the other. The receipt reports the selected name (absent for the caller-pane rule, which observed a session rather than requesting a name), its source, and resolved target session id.",
|
|
783
|
+
"Optional expert seat override, TMUX ONLY: open the sibling in ONE EXISTING tmux session of this agent's own server, and it always wins. When omitted the seat follows the CALLER, never the backend being opened: a Codex CALLER opens beside its own TUI pane, matched by thread-id in that pane's terminal title (0 or 2+ matching panes refuse, never fall back); every other caller opens in its own session. A pane title is a placement input only — never an address, liveness or delivery fact. Nothing is ever created. Inside herdr this field is refused by name — placement there belongs to herdr, and a tmux session name would silently place the sibling somewhere else. Independent of cwd; neither is inferred from the other. The receipt reports the selected name (absent for the caller-pane rule, which observed a session rather than requesting a name), its source, and resolved target session id.",
|
|
783
784
|
),
|
|
784
785
|
},
|
|
785
786
|
async ({ backend, model, task, cwd, placement }, extra) => {
|
|
@@ -813,22 +814,21 @@ server.tool(
|
|
|
813
814
|
callerGardenId = null;
|
|
814
815
|
}
|
|
815
816
|
try {
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
//
|
|
819
|
-
//
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const rendered = renderFreshCall(result);
|
|
817
|
+
// Rail choice, Codex preflight ordering and rendering all live in the composition root,
|
|
818
|
+
// so this surface and pi's own cannot drift apart on any of them. The two caller-derived
|
|
819
|
+
// codex inputs ride with the request: the root consults them only on the tmux rail,
|
|
820
|
+
// which is where #95 lane B/C measured them.
|
|
821
|
+
const dispatched = await dispatchFreshCall({
|
|
822
|
+
backend,
|
|
823
|
+
model,
|
|
824
|
+
task,
|
|
825
|
+
cwd,
|
|
826
|
+
placement,
|
|
827
|
+
callerGardenId,
|
|
828
|
+
callerNativeSessionId,
|
|
829
|
+
callerCwd,
|
|
830
|
+
});
|
|
831
|
+
const rendered = renderDispatchedFreshCall(dispatched);
|
|
832
832
|
return rendered.isError ? textErr(rendered.text) : textOk(rendered.text);
|
|
833
833
|
} catch (err) {
|
|
834
834
|
return textErr(`entwurf_fresh_call error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -142,6 +142,7 @@
|
|
|
142
142
|
"../../scripts/meta-bridge-prune.ts",
|
|
143
143
|
"../../scripts/meta-bridge-fresh-cut.ts",
|
|
144
144
|
"../../scripts/meta-facts.ts",
|
|
145
|
+
"../../scripts/peer-facts.ts",
|
|
145
146
|
"../../scripts/omp-receive-facts.ts",
|
|
146
147
|
"../../scripts/codex-socket-path.ts",
|
|
147
148
|
"../../pi-extensions/meta-bridge-hook.ts",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junghanacs/entwurf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.1",
|
|
4
4
|
"description": "Garden-citizen dispatch substrate and meta-bridge for pi, Claude Code, Copilot CLI, OMP, Antigravity, and Codex CLI; Claude and Snowflake Cortex Code ACP backends.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -73,7 +73,8 @@
|
|
|
73
73
|
"CONTRIBUTING.md",
|
|
74
74
|
"CHANGELOG.md",
|
|
75
75
|
"!**/__pycache__",
|
|
76
|
-
"!**/*.pyc"
|
|
76
|
+
"!**/*.pyc",
|
|
77
|
+
"!pi-extensions/**/*.test.ts"
|
|
77
78
|
],
|
|
78
79
|
"engines": {
|
|
79
80
|
"node": ">=24.0.0"
|
|
@@ -82,7 +83,7 @@
|
|
|
82
83
|
"claudeCodeFloor": ">=2.1.217"
|
|
83
84
|
},
|
|
84
85
|
"dependencies": {
|
|
85
|
-
"@agentclientprotocol/claude-agent-acp": "0.
|
|
86
|
+
"@agentclientprotocol/claude-agent-acp": "0.79.0",
|
|
86
87
|
"@agentclientprotocol/sdk": "1.4.0",
|
|
87
88
|
"@anthropic-ai/sdk": "0.100.1",
|
|
88
89
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
@@ -107,7 +108,7 @@
|
|
|
107
108
|
"vitest": "4.1.9"
|
|
108
109
|
},
|
|
109
110
|
"scripts": {
|
|
110
|
-
"prepare": "husky 2
|
|
111
|
+
"prepare": "husky 1>&2 || true; npm run --silent build-bridge 1>&2",
|
|
111
112
|
"postinstall": "node scripts/postinstall-chmod.cjs",
|
|
112
113
|
"build-bridge": "bash scripts/with-dist-lock.sh bash scripts/build-bridge.sh",
|
|
113
114
|
"prepack": "pnpm --silent run build-bridge",
|
|
@@ -127,7 +128,6 @@
|
|
|
127
128
|
"check-meta-capability-source": "./run.sh check-meta-capability-source",
|
|
128
129
|
"check-socket-probe": "./run.sh check-socket-probe",
|
|
129
130
|
"check-project-trust-handler": "./run.sh check-project-trust-handler",
|
|
130
|
-
"check-entwurf-v2-lock": "./run.sh check-entwurf-v2-lock",
|
|
131
131
|
"check-model-lock": "./run.sh check-model-lock",
|
|
132
132
|
"check-dep-versions": "./run.sh check-dep-versions",
|
|
133
133
|
"check-node-floor-coherence": "./run.sh check-node-floor-coherence",
|
|
@@ -135,6 +135,7 @@
|
|
|
135
135
|
"check-copilot-birth-hook": "./run.sh check-copilot-birth-hook",
|
|
136
136
|
"check-copilot-statusline": "./run.sh check-copilot-statusline",
|
|
137
137
|
"check-copilot-launch": "./run.sh check-copilot-launch",
|
|
138
|
+
"check-pi-launch": "./run.sh check-pi-launch",
|
|
138
139
|
"smoke-copilot-statusline-state": "./run.sh smoke-copilot-statusline-state",
|
|
139
140
|
"smoke-copilot-mcp-state": "./run.sh smoke-copilot-mcp-state",
|
|
140
141
|
"smoke-omp-bridge-state": "./run.sh smoke-omp-bridge-state",
|
|
@@ -173,9 +174,9 @@
|
|
|
173
174
|
"check:full": "bash scripts/check-elapsed.sh check:full check:core check:hermetic check:package",
|
|
174
175
|
"check:core": "pnpm run check:toolchain && pnpm run check:vitest && pnpm run check:contracts",
|
|
175
176
|
"check:toolchain": "pnpm lint && pnpm typecheck",
|
|
176
|
-
"check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface",
|
|
177
|
-
"check:contracts": "./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-
|
|
178
|
-
"check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-meta-hook-session-switch && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-codex-app-server-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-birth-hook && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-usage-accounting && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-gate-manifests && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex && ./run.sh check-codex-bridge-identity && ./run.sh check-codex-birth-hook && ./run.sh smoke-codex-config-state && ./run.sh smoke-codex-birth",
|
|
177
|
+
"check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface && ./run.sh check-tests-beside-behavior",
|
|
178
|
+
"check:contracts": "./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-send && ./run.sh check-compaction-send-guard && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-control-socket-path && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-herdr-placement && ./run.sh check-herdr-fresh-call && ./run.sh check-herdr-supply && ./run.sh check-fresh-call-dispatch && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push",
|
|
179
|
+
"check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-meta-hook-session-switch && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-pi-launch && ./run.sh check-codex-app-server-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-birth-hook && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-peer-facts && ./run.sh check-herdr-plugin && ./run.sh check-herdr-plugin-profile && ./run.sh check-herdr-plugin-build && ./run.sh check-herdr-runtime-bootstrap && ./run.sh check-herdr-activation && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-usage-accounting && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-typing-call-fence && ./run.sh check-gate-manifests && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex && ./run.sh check-codex-bridge-identity && ./run.sh check-codex-birth-hook && ./run.sh smoke-codex-config-state && ./run.sh smoke-codex-birth && ./run.sh check-herdr-sandbox",
|
|
179
180
|
"check:package": "./run.sh check-install-surface && ./run.sh smoke-meta-install-state && ./run.sh smoke-agy-install-state && ./run.sh smoke-setup-verdict && ./run.sh check-pack-pin-matcher && ./run.sh check-package-source-routing && ./run.sh check-install-preflight && ./run.sh check-pack"
|
|
180
181
|
},
|
|
181
182
|
"pi": {
|
|
@@ -90,6 +90,7 @@ import {
|
|
|
90
90
|
} from "./lib/compaction-send-guard.js";
|
|
91
91
|
import { CONTROL_SOCKET_SUFFIX, controlSocketPathIn, defaultControlSocketDir } from "./lib/control-socket-path.js";
|
|
92
92
|
import {
|
|
93
|
+
attachAcceptedSocketDisconnectPolicy,
|
|
93
94
|
formatSenderInfoBlock,
|
|
94
95
|
type RpcCommand,
|
|
95
96
|
type RpcResponse,
|
|
@@ -220,7 +221,11 @@ function writeResponse(socket: net.Socket, response: RpcResponse): void {
|
|
|
220
221
|
try {
|
|
221
222
|
socket.write(`${JSON.stringify(response)}\n`);
|
|
222
223
|
} catch {
|
|
223
|
-
//
|
|
224
|
+
// SYNCHRONOUS throws only — a destroyed stream lands here. This catch CANNOT see an
|
|
225
|
+
// asynchronous stream error: an EPIPE on a peer that hung up arrives as an `error` event,
|
|
226
|
+
// and `attachAcceptedSocketDisconnectPolicy`, installed on every accepted connection in
|
|
227
|
+
// `createServer`, is what carries that half of the contract. Reading this catch as full
|
|
228
|
+
// coverage is what let a late response kill a resident session.
|
|
224
229
|
}
|
|
225
230
|
}
|
|
226
231
|
|
|
@@ -841,6 +846,10 @@ async function handleCommand(
|
|
|
841
846
|
|
|
842
847
|
async function createServer(pi: ExtensionAPI, state: SocketState, socketPath: string): Promise<net.Server> {
|
|
843
848
|
const server = net.createServer((socket) => {
|
|
849
|
+
// FIRST — before setEncoding, before the data handler, and before any response can be
|
|
850
|
+
// written back. The policy itself (and why this process died without it) lives with the
|
|
851
|
+
// rest of the wire protocol in lib/entwurf-control-rpc.ts.
|
|
852
|
+
attachAcceptedSocketDisconnectPolicy(socket);
|
|
844
853
|
socket.setEncoding("utf8");
|
|
845
854
|
let buffer = "";
|
|
846
855
|
socket.on("data", (chunk) => {
|
|
@@ -1032,6 +1041,43 @@ function applyEmacsAgentSocketEnv(pi: ExtensionAPI): void {
|
|
|
1032
1041
|
}
|
|
1033
1042
|
}
|
|
1034
1043
|
|
|
1044
|
+
/**
|
|
1045
|
+
* The one-line answer to "I installed entwurf, why is nothing here?" (#116 M3-b3 F).
|
|
1046
|
+
*
|
|
1047
|
+
* `[관측: GLG, 날것 PC, 2026-09-17]` a herdr plugin install wires this extension into pi at USER
|
|
1048
|
+
* scope, so it loads in EVERY pi session on the host — but citizenship is argv-gated on purpose, so
|
|
1049
|
+
* a plain `pi` has no garden id, no socket and no tools. From the outside those two facts are
|
|
1050
|
+
* indistinguishable from "the install did nothing", and the operator has no reason to guess at a
|
|
1051
|
+
* flag nobody showed them. This says it once, at exactly the moment they are looking at the wrong
|
|
1052
|
+
* thing.
|
|
1053
|
+
*
|
|
1054
|
+
* UI ONLY, AND THAT IS THE CONTRACT. The refusal path below writes stderr ALWAYS because a control
|
|
1055
|
+
* surface that failed to come up is a durable fault. This is not that: it is an ordinary,
|
|
1056
|
+
* deliberate state. Writing it to stderr would put a sentence into every `pi -p …` pipeline and
|
|
1057
|
+
* every script on the host to tell a human something no human is reading there.
|
|
1058
|
+
*
|
|
1059
|
+
* WHETHER TO SPEAK IS NOT DECIDED HERE. It is `decideUncitizenedNotice` in the self-address fence,
|
|
1060
|
+
* reached by the same non-literal dynamic import as its sibling — and that placement is a repair,
|
|
1061
|
+
* not taste. `[측정 2026-09-17, 독립 검수 + 재현]` while the three conditions were `if`s at this
|
|
1062
|
+
* call site and the gate pinned them with a source regex, two mutants walked through green: moving
|
|
1063
|
+
* the call into the CITIZEN branch, and deleting the once-latch. A regex sees that a call exists;
|
|
1064
|
+
* it cannot see which branch it sits in or how often it fires. So the call is now UNCONDITIONAL and
|
|
1065
|
+
* carries `controlEnabled` as a fact — the branch is gone, and what is left is a truth table.
|
|
1066
|
+
*/
|
|
1067
|
+
let uncitizenedNoticeShown = false;
|
|
1068
|
+
async function noticeUncitizenedSession(ctx: ExtensionContext, controlEnabled: boolean): Promise<void> {
|
|
1069
|
+
const self = (await import(ENTWURF_SELF_ADDRESS_MODULE)) as unknown as EntwurfSelfAddressModule;
|
|
1070
|
+
if (!self.decideUncitizenedNotice({ controlEnabled, hasUI: ctx.hasUI, alreadyShown: uncitizenedNoticeShown })) {
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
uncitizenedNoticeShown = true;
|
|
1074
|
+
ctx.ui.notify(
|
|
1075
|
+
"🪛 entwurf is installed here, but this session is not a garden citizen — start pi with " +
|
|
1076
|
+
"--entwurf-control for a garden id, a control socket and the entwurf tools.",
|
|
1077
|
+
"info",
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1035
1081
|
function shouldRegisterControlTools(pi: ExtensionAPI): boolean {
|
|
1036
1082
|
return pi.getFlag(ENTWURF_FLAG) === true || wasBooleanFlagPassed(ENTWURF_FLAG);
|
|
1037
1083
|
}
|
|
@@ -1106,6 +1152,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1106
1152
|
// non-control session.
|
|
1107
1153
|
applyEmacsAgentSocketEnv(pi);
|
|
1108
1154
|
const enabled = pi.getFlag(ENTWURF_FLAG) === true;
|
|
1155
|
+
await noticeUncitizenedSession(ctx, enabled);
|
|
1109
1156
|
if (!enabled) {
|
|
1110
1157
|
await stopControlServer(state);
|
|
1111
1158
|
residentGardenId = null;
|
|
@@ -1279,6 +1326,7 @@ type SelfAddressabilityFn = (facts: {
|
|
|
1279
1326
|
|
|
1280
1327
|
interface EntwurfSelfAddressModule {
|
|
1281
1328
|
computeSelfAddressability: SelfAddressabilityFn;
|
|
1329
|
+
decideUncitizenedNotice: (facts: { controlEnabled: boolean; hasUI: boolean; alreadyShown: boolean }) => boolean;
|
|
1282
1330
|
}
|
|
1283
1331
|
|
|
1284
1332
|
/**
|
|
@@ -1371,8 +1419,8 @@ A peer entwurf_peers shows as liveness=alive → fire-and-forget. A citizen with
|
|
|
1371
1419
|
(liveness=unsupported) is ALSO fire-and-forget — unsupported means only "no control-socket probe" — and the
|
|
1372
1420
|
decider picks its own rail: a self-fetch backend (e.g. Claude Code) gets the mailbox, a native-push backend
|
|
1373
1421
|
(e.g. Antigravity) gets direct injection and has NO mailbox at all. THERE IS A THIRD RESULT: the mailbox
|
|
1374
|
-
delivers only to a DELIVERABLE citizen, so a terminated session
|
|
1375
|
-
|
|
1422
|
+
delivers only to a DELIVERABLE citizen, so a terminated self-fetch session is mailbox-undeliverable
|
|
1423
|
+
rather than queued for an inbox nobody drains. The native-push probe is 3-valued:
|
|
1376
1424
|
alive → injected; dead → native-push-target-dead; indeterminate → native-push-probe-indeterminate
|
|
1377
1425
|
(unestablished ≠ gone). DORMANT IS UNREACHABLE: a socket-domain citizen that is not running gets
|
|
1378
1426
|
dormant-fire-forget-unsupported — same receiver rule as the mailbox, no active drainer means no
|
|
@@ -1530,16 +1578,15 @@ function registerListSessionsTool(pi: ExtensionAPI): void {
|
|
|
1530
1578
|
// Tool: entwurf_fresh_call
|
|
1531
1579
|
// ============================================================================
|
|
1532
1580
|
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
}
|
|
1581
|
+
// ONE dynamic import, and it resolves the composition root rather than a rail: which rail opens
|
|
1582
|
+
// the sibling, when the Codex preflight runs, and how the receipt renders are decisions this
|
|
1583
|
+
// surface must not own a second copy of (#116 C3). The import stays non-literal-free and lazy for
|
|
1584
|
+
// the same startup-fence reason it always was.
|
|
1585
|
+
const FRESH_CALL_DISPATCH_MODULE = "./lib/fresh-call-dispatch.ts";
|
|
1539
1586
|
|
|
1540
|
-
interface
|
|
1541
|
-
|
|
1542
|
-
|
|
1587
|
+
interface FreshCallDispatchModule {
|
|
1588
|
+
dispatchFreshCall(
|
|
1589
|
+
request: {
|
|
1543
1590
|
backend: "pi" | "claude-code" | "copilot" | "omp" | "codex";
|
|
1544
1591
|
model: string;
|
|
1545
1592
|
task: string;
|
|
@@ -1548,8 +1595,11 @@ interface MuxFreshCallModule {
|
|
|
1548
1595
|
callerGardenId: string | null;
|
|
1549
1596
|
},
|
|
1550
1597
|
env?: NodeJS.ProcessEnv,
|
|
1551
|
-
): { ok: boolean }
|
|
1552
|
-
|
|
1598
|
+
): Promise<{ rail: "herdr" | "tmux"; result: { ok: boolean } }>;
|
|
1599
|
+
renderDispatchedFreshCall(dispatched: { rail: "herdr" | "tmux"; result: { ok: boolean } }): {
|
|
1600
|
+
text: string;
|
|
1601
|
+
isError: boolean;
|
|
1602
|
+
};
|
|
1553
1603
|
}
|
|
1554
1604
|
|
|
1555
1605
|
/**
|
|
@@ -1570,27 +1620,25 @@ function registerFreshCallTool(pi: ExtensionAPI): void {
|
|
|
1570
1620
|
registerTool({
|
|
1571
1621
|
name: "entwurf_fresh_call",
|
|
1572
1622
|
label: "Open Fresh Sibling",
|
|
1573
|
-
description: `Open ONE fresh visible sibling
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
surface opens in its own session. A missing named session is tmux-session-missing and NOTHING is created. There are no
|
|
1589
|
-
arbitrary command/env knobs. Do not put secrets in the task — model and task argv are visible to same-user
|
|
1590
|
-
processes on this host.`,
|
|
1623
|
+
description: `Open ONE fresh visible sibling beside you and hand it a first task. WHERE it opens is decided by
|
|
1624
|
+
where THIS agent runs, never by a parameter: inside herdr (HERDR_ENV=1) it opens a NEW UNFOCUSED TAB in your own
|
|
1625
|
+
herdr workspace and only pi and claude-code may be opened; everywhere else it opens in the operator's tmux with all five backends (pi, claude-code,
|
|
1626
|
+
copilot, omp, codex). There is no fallback — an incomplete herdr context is refused by name rather than opening a tmux
|
|
1627
|
+
window you cannot see from in herdr. The sibling's FIRST action is a callback to you carrying a nonce, whose sender
|
|
1628
|
+
envelope is its garden id — that is how you learn the address of a thing that did not exist a moment ago. This returns
|
|
1629
|
+
a LAUNCH receipt (the owner's coordinates plus that nonce) and nothing else: it does NOT mean the runtime started, the
|
|
1630
|
+
first turn ran, or the task was delivered. Those coordinates are a VIEW, never an address — a herdr tab/pane id can
|
|
1631
|
+
change under a running sibling. Nothing polls; if the callback never arrives the tab is visible. For EXISTING citizens use entwurf_v2 — this tool only creates, and entwurf_peers only reports. Model is REQUIRED and passed to the chosen runtime CLI (provider/model for pi, an id/alias for Claude Code, a name or pattern
|
|
1632
|
+
for the rest). On tmux, copilot/omp/codex are refused BEFORE any window opens when their birth, MCP, receive or
|
|
1633
|
+
visible-identity units are absent, and codex also needs the operator-owned app-server socket entwurf never starts; in
|
|
1634
|
+
herdr those three are refused by name first. Optional
|
|
1635
|
+
placement.tmuxSession is a TMUX-ONLY seat naming ONE EXISTING session and ALWAYS wins; omitted, the seat
|
|
1636
|
+
follows the CALLER, and on this surface that is always your own session — a missing one is
|
|
1637
|
+
tmux-session-missing and NOTHING is created. In herdr the field is refused by name. Do not put secrets in the task — model and task argv are visible to same-user processes.`,
|
|
1591
1638
|
parameters: Type.Object({
|
|
1592
1639
|
backend: StringEnum(["pi", "claude-code", "copilot", "omp", "codex"], {
|
|
1593
|
-
description:
|
|
1640
|
+
description:
|
|
1641
|
+
"Which fixed runtime to open. Only these five, and only pi/claude-code when this agent runs inside herdr; there is no arbitrary command.",
|
|
1594
1642
|
}),
|
|
1595
1643
|
model: Type.String({
|
|
1596
1644
|
minLength: 1,
|
|
@@ -1608,7 +1656,7 @@ processes on this host.`,
|
|
|
1608
1656
|
cwd: Type.Optional(
|
|
1609
1657
|
Type.String({
|
|
1610
1658
|
description:
|
|
1611
|
-
"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
|
|
1659
|
+
"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 — on BOTH rails. Taken exactly as given: no trim, no realpath, no project-name resolution. '#' is refused on the tmux rail only, because tmux format-expands a start directory; inside herdr it is an ordinary path character. The receipt echoes what was REQUESTED, never an observation.",
|
|
1612
1660
|
}),
|
|
1613
1661
|
),
|
|
1614
1662
|
placement: Type.Optional(
|
|
@@ -1621,7 +1669,7 @@ processes on this host.`,
|
|
|
1621
1669
|
},
|
|
1622
1670
|
{
|
|
1623
1671
|
description:
|
|
1624
|
-
"Optional expert seat override: open the sibling in ONE EXISTING tmux session of this agent's own server, and it always wins. When omitted the seat follows the CALLER, never the backend being opened: every caller on THIS surface opens in its own session (a pi session is never a Codex citizen, so the Codex caller-pane rule the MCP bridge carries cannot arise here). Nothing is ever created. Independent of cwd; neither is inferred from the other. The receipt reports the selected name, its source, and resolved target session id.",
|
|
1672
|
+
"Optional expert seat override, TMUX ONLY: open the sibling in ONE EXISTING tmux session of this agent's own server, and it always wins. When omitted the seat follows the CALLER, never the backend being opened: every caller on THIS surface opens in its own session (a pi session is never a Codex citizen, so the Codex caller-pane rule the MCP bridge carries cannot arise here). Nothing is ever created. Inside herdr this field is refused by name — placement there belongs to herdr, and a tmux session name would silently place the sibling somewhere else. Independent of cwd; neither is inferred from the other. The receipt reports the selected name, its source, and resolved target session id.",
|
|
1625
1673
|
},
|
|
1626
1674
|
),
|
|
1627
1675
|
),
|
|
@@ -1640,29 +1688,20 @@ processes on this host.`,
|
|
|
1640
1688
|
_ctx: ExtensionContext,
|
|
1641
1689
|
) {
|
|
1642
1690
|
try {
|
|
1643
|
-
const
|
|
1644
|
-
// ONE input object for ONE
|
|
1645
|
-
//
|
|
1646
|
-
//
|
|
1647
|
-
//
|
|
1648
|
-
const
|
|
1691
|
+
const dispatch = (await import(FRESH_CALL_DISPATCH_MODULE)) as unknown as FreshCallDispatchModule;
|
|
1692
|
+
// ONE input object for ONE dispatch call. Rail selection and the Codex preflight
|
|
1693
|
+
// ordering live behind it: duplicating either here would put the same decision in
|
|
1694
|
+
// two places, which is how a mutant that plants a defect in one of them survives
|
|
1695
|
+
// on the other.
|
|
1696
|
+
const dispatched = await dispatch.dispatchFreshCall({
|
|
1649
1697
|
backend: params.backend,
|
|
1650
1698
|
model: params.model,
|
|
1651
1699
|
task: params.task,
|
|
1652
1700
|
cwd: params.cwd,
|
|
1653
1701
|
placement: params.placement,
|
|
1654
1702
|
callerGardenId: residentGardenId,
|
|
1655
|
-
};
|
|
1656
|
-
const
|
|
1657
|
-
params.backend === "codex"
|
|
1658
|
-
? await (async () => {
|
|
1659
|
-
const preflight = (await import(CODEX_FRESH_PREFLIGHT_MODULE)) as unknown as CodexFreshPreflightModule;
|
|
1660
|
-
const missing = await preflight.codexFreshPreflight(process.env);
|
|
1661
|
-
if (missing) return { ok: false as const, reason: missing };
|
|
1662
|
-
return mux.freshCall(call);
|
|
1663
|
-
})()
|
|
1664
|
-
: mux.freshCall(call);
|
|
1665
|
-
const rendered = mux.renderFreshCall(result);
|
|
1703
|
+
});
|
|
1704
|
+
const rendered = dispatch.renderDispatchedFreshCall(dispatched);
|
|
1666
1705
|
return {
|
|
1667
1706
|
content: [{ type: "text", text: rendered.text }],
|
|
1668
1707
|
isError: rendered.isError,
|