@junghanacs/entwurf 0.15.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +229 -0
- package/DELIVERY.md +3 -2
- package/README.md +70 -87
- package/VERIFY.md +4 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/external-mcp-host.md +147 -9
- package/docs/setup-clean-host.md +105 -0
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +65 -20
- package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +17 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +172 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +152 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/omp-fresh-preflight.js +271 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +988 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-receive-omp.js +574 -0
- package/mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js +84 -0
- package/mcp/entwurf-bridge/src/index.ts +65 -19
- package/mcp/entwurf-bridge/tsconfig.build.json +19 -3
- package/mcp/tsconfig.json +10 -0
- package/package.json +13 -3
- package/pi/entwurf-capabilities.json +1 -0
- package/pi/meta-bridge-omp/entwurf-meta-omp/package.json +7 -0
- package/pi/omp-receive/entwurf-receive-omp/package.json +7 -0
- package/pi-extensions/entwurf-control.ts +12 -9
- package/pi-extensions/lib/entwurf-self-address.ts +18 -7
- package/pi-extensions/lib/meta-sender-identity.ts +1 -1
- package/pi-extensions/lib/meta-session.ts +219 -5
- package/pi-extensions/lib/mux-fresh-call.ts +171 -17
- package/pi-extensions/lib/omp-fresh-preflight.ts +275 -0
- package/pi-extensions/meta-bridge-omp.ts +1244 -0
- package/pi-extensions/meta-bridge-receive-omp.ts +666 -0
- package/run.sh +296 -4
- package/scripts/check-entwurf-self-address.ts +31 -0
- package/scripts/check-gate-qualification.ts +4 -1
- package/scripts/check-harness-admission-parity.ts +143 -0
- package/scripts/check-meta-doctor-oracle.sh +20 -0
- package/scripts/check-omp-birth-hook.ts +1049 -0
- package/scripts/check-omp-fresh-preflight.ts +181 -0
- package/scripts/check-omp-receive-arm.ts +516 -0
- package/scripts/inventory-verification-surface.ts +1 -1
- package/scripts/meta-bridge-hook-log.sh +9 -1
- package/scripts/mutants/copilot-birth.json +7 -5
- package/scripts/mutants/mux-fresh-call.json +22 -22
- package/scripts/mutants/omp-birth.json +173 -0
- package/scripts/mutants/omp-fresh.json +300 -0
- package/scripts/mutants/omp-receive.json +135 -0
- package/scripts/mutants/self-address.json +34 -0
- package/scripts/omp-bridge-doctor.sh +315 -0
- package/scripts/omp-bridge-install.sh +221 -0
- package/scripts/omp-bridge-oracle.sh +154 -0
- package/scripts/omp-bridge-uninstall.sh +57 -0
- package/scripts/omp-mcp-bridge.sh +320 -0
- package/scripts/omp-mcp-config.py +392 -0
- package/scripts/omp-receive-doctor.sh +246 -0
- package/scripts/omp-receive-facts.ts +106 -0
- package/scripts/omp-receive-install.sh +228 -0
- package/scripts/omp-receive-uninstall.sh +60 -0
- package/scripts/omp-tool-surface.py +349 -0
- package/scripts/raw-omp-measure/README.md +420 -0
- package/scripts/raw-omp-measure/probe-extension.ts +76 -0
- package/scripts/raw-omp-measure/probe-receive-surface.ts +250 -0
- package/scripts/raw-omp-measure/source-audit.md +414 -0
- package/scripts/smoke-omp-bridge-state.sh +221 -0
- package/scripts/smoke-omp-fresh-live.ts +497 -0
- package/scripts/smoke-omp-mcp-state.sh +327 -0
- package/scripts/smoke-omp-receive-live.ts +340 -0
- package/scripts/smoke-omp-receive-state.sh +196 -0
- package/scripts/tsconfig.json +2 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* raw-omp-measure probe #2 — the STEP 7 (receive) surface, for #87 bundle B.
|
|
3
|
+
*
|
|
4
|
+
* Loaded into a live omp process via `omp -e <this file>`. It answers the five
|
|
5
|
+
* D5 cells the bundle B design is blocked on, and it does so WITHOUT the vendor
|
|
6
|
+
* source checkout (which is not on this host):
|
|
7
|
+
*
|
|
8
|
+
* (a) where `sendUserMessage` / `sendMessage` actually live — the factory `pi`
|
|
9
|
+
* object or the event `ctx`. (`setStatus` turned out to be on `ctx.ui`, not
|
|
10
|
+
* on `pi`, so this is a measured question, not an obvious one.)
|
|
11
|
+
* (b) whether calling it while IDLE really starts a turn. Gated behind
|
|
12
|
+
* OMP_PROBE_SEND=1 because that costs a real model turn.
|
|
13
|
+
* (c) whether `ctx.setInterval` exists and keeps firing while the session is
|
|
14
|
+
* idle — the mechanism S1 (bounded arm retry) would stand on.
|
|
15
|
+
* (d) whether a SECOND extension in the same process gets its own working API.
|
|
16
|
+
* The installed `entwurf-meta-omp` birth unit is the first extension; this
|
|
17
|
+
* probe is the second, so the run itself is the experiment.
|
|
18
|
+
* (e) the ORDER datum that decides S1 vs S2: at the moment THIS extension's
|
|
19
|
+
* `session_start` handler runs, has the birth unit already written its
|
|
20
|
+
* sender marker? Measured by looking for the marker file directly, then
|
|
21
|
+
* re-checked on a schedule so "not yet" and "never" stay distinguishable.
|
|
22
|
+
*
|
|
23
|
+
* MEASUREMENT ONLY. It mints nothing, writes no garden artifact, sends no
|
|
24
|
+
* entwurf message, and never throws into the host.
|
|
25
|
+
*
|
|
26
|
+
* Log path: $OMP_PROBE_LOG (default /tmp/omp-receive-probe.jsonl), append-only JSONL.
|
|
27
|
+
*/
|
|
28
|
+
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
29
|
+
import * as os from "node:os";
|
|
30
|
+
import * as path from "node:path";
|
|
31
|
+
|
|
32
|
+
const LOG = process.env.OMP_PROBE_LOG ?? "/tmp/omp-receive-probe.jsonl";
|
|
33
|
+
const SEND = process.env.OMP_PROBE_SEND === "1";
|
|
34
|
+
const NONCE = process.env.OMP_PROBE_NONCE ?? "no-nonce";
|
|
35
|
+
|
|
36
|
+
function log(obj: Record<string, unknown>): void {
|
|
37
|
+
try {
|
|
38
|
+
appendFileSync(LOG, `${JSON.stringify({ ts: new Date().toISOString(), ...obj })}\n`);
|
|
39
|
+
} catch {
|
|
40
|
+
// measurement probe: never take the host down
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The omp four-root base, spelled literally rather than imported: a probe must not
|
|
45
|
+
* depend on the product's own resolver, or it would be testing our copy of the answer. */
|
|
46
|
+
function senderMarkerPath(pid: number): string {
|
|
47
|
+
const home = process.env.HOME && process.env.HOME.length > 0 ? process.env.HOME : os.homedir();
|
|
48
|
+
const dir = process.env.ENTWURF_META_SENDERS_DIR;
|
|
49
|
+
const base = dir && dir.length > 0 ? dir : path.join(home, ".pi", "agent", "meta-senders");
|
|
50
|
+
return path.join(base, "omp", `${pid}.json`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function markerFacts(): Record<string, unknown> {
|
|
54
|
+
const file = senderMarkerPath(process.pid);
|
|
55
|
+
try {
|
|
56
|
+
if (!existsSync(file)) return { markerPresent: false, markerFile: file };
|
|
57
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
58
|
+
return {
|
|
59
|
+
markerPresent: true,
|
|
60
|
+
markerFile: file,
|
|
61
|
+
markerGarden: raw?.gardenId,
|
|
62
|
+
markerNative: raw?.nativeSessionId,
|
|
63
|
+
markerOwnerPid: raw?.ownerPid,
|
|
64
|
+
};
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return { markerPresent: "unreadable", markerFile: file, markerError: String(err) };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function fnKeys(obj: unknown): string[] {
|
|
71
|
+
if (!obj || (typeof obj !== "object" && typeof obj !== "function")) return [];
|
|
72
|
+
const out = new Set<string>();
|
|
73
|
+
let cur: object | null = obj as object;
|
|
74
|
+
for (let depth = 0; cur && depth < 3; depth++) {
|
|
75
|
+
for (const k of Object.getOwnPropertyNames(cur)) out.add(k);
|
|
76
|
+
cur = Object.getPrototypeOf(cur);
|
|
77
|
+
}
|
|
78
|
+
return [...out].sort();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function typeOfPath(root: any, ...segs: string[]): string {
|
|
82
|
+
let cur = root;
|
|
83
|
+
for (const s of segs) {
|
|
84
|
+
if (cur == null) return "absent";
|
|
85
|
+
cur = cur[s];
|
|
86
|
+
}
|
|
87
|
+
return typeof cur;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default function probeReceiveSurface(pi: any): void {
|
|
91
|
+
// (a)+(d) the factory surface, before any event.
|
|
92
|
+
log({
|
|
93
|
+
event: "factory",
|
|
94
|
+
pid: process.pid,
|
|
95
|
+
ppid: process.ppid,
|
|
96
|
+
piKeys: fnKeys(pi),
|
|
97
|
+
"typeof pi.sendUserMessage": typeOfPath(pi, "sendUserMessage"),
|
|
98
|
+
"typeof pi.sendMessage": typeOfPath(pi, "sendMessage"),
|
|
99
|
+
"typeof pi.setInterval": typeOfPath(pi, "setInterval"),
|
|
100
|
+
"typeof pi.setTimeout": typeOfPath(pi, "setTimeout"),
|
|
101
|
+
"typeof pi.on": typeOfPath(pi, "on"),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const snapshot = (event: string, ctx: any, extra?: Record<string, unknown>) => {
|
|
105
|
+
let sessionId: unknown;
|
|
106
|
+
try {
|
|
107
|
+
sessionId = ctx?.sessionManager?.getSessionId?.();
|
|
108
|
+
} catch {}
|
|
109
|
+
log({
|
|
110
|
+
event,
|
|
111
|
+
pid: process.pid,
|
|
112
|
+
mode: ctx?.mode,
|
|
113
|
+
hasUI: ctx?.hasUI,
|
|
114
|
+
cwd: ctx?.cwd,
|
|
115
|
+
sessionId,
|
|
116
|
+
ctxKeys: fnKeys(ctx),
|
|
117
|
+
"typeof ctx.sendUserMessage": typeOfPath(ctx, "sendUserMessage"),
|
|
118
|
+
"typeof ctx.sendMessage": typeOfPath(ctx, "sendMessage"),
|
|
119
|
+
"typeof ctx.setInterval": typeOfPath(ctx, "setInterval"),
|
|
120
|
+
"typeof ctx.setTimeout": typeOfPath(ctx, "setTimeout"),
|
|
121
|
+
"typeof ctx.ui.setStatus": typeOfPath(ctx, "ui", "setStatus"),
|
|
122
|
+
...markerFacts(),
|
|
123
|
+
...extra,
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const onEdge = (edge: string) => (event: any, ctx: any) => {
|
|
128
|
+
const t0 = Date.now();
|
|
129
|
+
snapshot(edge, ctx, { reason: event?.reason, phase: "t0" });
|
|
130
|
+
|
|
131
|
+
// (e) THE ORDER DATUM. If the marker is absent at t0 this extension ran BEFORE
|
|
132
|
+
// the birth unit, which is exactly the case a receive unit must survive. The
|
|
133
|
+
// re-checks say whether a bounded defer would have caught it, and how late.
|
|
134
|
+
for (const delayMs of [25, 100, 250, 1000, 3000, 8000]) {
|
|
135
|
+
try {
|
|
136
|
+
ctx.setTimeout?.(() => {
|
|
137
|
+
log({
|
|
138
|
+
event: `${edge}_marker_recheck`,
|
|
139
|
+
pid: process.pid,
|
|
140
|
+
mode: ctx?.mode,
|
|
141
|
+
delayMs,
|
|
142
|
+
elapsedMs: Date.now() - t0,
|
|
143
|
+
...markerFacts(),
|
|
144
|
+
});
|
|
145
|
+
}, delayMs);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
log({ event: `${edge}_recheck_schedule_failed`, delayMs, error: String(err) });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// (c) does a REPEATING contained timer exist, and does it keep firing while idle?
|
|
152
|
+
// Bounded on purpose: 6 ticks then self-clear, so the probe cannot outlive its point.
|
|
153
|
+
if (typeof ctx?.setInterval === "function") {
|
|
154
|
+
let ticks = 0;
|
|
155
|
+
try {
|
|
156
|
+
// CANCELLATION IS THE WHOLE POINT OF THIS CELL. `[LIVE run1]` there is no
|
|
157
|
+
// `ctx.clearInterval`: calling it through `?.` was a SILENT no-op and the
|
|
158
|
+
// timer ran 58 ticks past its stop condition inside the operator's TUI.
|
|
159
|
+
// The vendor's canceller is `ctx.clearTimer` (ctxKeys, run1). Probe it by
|
|
160
|
+
// NAME and record which name actually stops the timer — a retry loop that
|
|
161
|
+
// cannot be cancelled is not a mechanism a receive unit may stand on.
|
|
162
|
+
const handle = ctx.setInterval(() => {
|
|
163
|
+
ticks++;
|
|
164
|
+
log({
|
|
165
|
+
event: `${edge}_interval_tick`,
|
|
166
|
+
pid: process.pid,
|
|
167
|
+
mode: ctx?.mode,
|
|
168
|
+
tick: ticks,
|
|
169
|
+
elapsedMs: Date.now() - t0,
|
|
170
|
+
});
|
|
171
|
+
if (ticks === 3) {
|
|
172
|
+
const canceller =
|
|
173
|
+
typeof ctx?.clearTimer === "function"
|
|
174
|
+
? "clearTimer"
|
|
175
|
+
: typeof ctx?.clearInterval === "function"
|
|
176
|
+
? "clearInterval"
|
|
177
|
+
: null;
|
|
178
|
+
log({ event: `${edge}_interval_canceller`, pid: process.pid, canceller });
|
|
179
|
+
if (canceller) {
|
|
180
|
+
try {
|
|
181
|
+
ctx[canceller](handle);
|
|
182
|
+
log({ event: `${edge}_interval_clear_called`, pid: process.pid, canceller, ticks });
|
|
183
|
+
} catch (err) {
|
|
184
|
+
log({ event: `${edge}_interval_clear_failed`, canceller, error: String(err) });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (ticks === 10) {
|
|
189
|
+
log({ event: `${edge}_interval_STILL_RUNNING_after_clear`, pid: process.pid, ticks });
|
|
190
|
+
}
|
|
191
|
+
}, 500);
|
|
192
|
+
log({ event: `${edge}_interval_armed`, pid: process.pid, handleType: typeof handle });
|
|
193
|
+
} catch (err) {
|
|
194
|
+
log({ event: `${edge}_interval_failed`, error: String(err) });
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
log({ event: `${edge}_no_setInterval`, pid: process.pid, mode: ctx?.mode });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// (b) THE COSTED CELL. Only on the visible host, only when explicitly enabled.
|
|
201
|
+
if (SEND && ctx?.mode === "tui") {
|
|
202
|
+
try {
|
|
203
|
+
ctx.setTimeout?.(() => {
|
|
204
|
+
const target =
|
|
205
|
+
typeof pi?.sendUserMessage === "function"
|
|
206
|
+
? { obj: pi, where: "pi" }
|
|
207
|
+
: typeof ctx?.sendUserMessage === "function"
|
|
208
|
+
? { obj: ctx, where: "ctx" }
|
|
209
|
+
: null;
|
|
210
|
+
if (!target) {
|
|
211
|
+
log({ event: "send_probe_absent", pid: process.pid });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const body = `PROBE ${NONCE}: reply with exactly the token PROBE-ACK-${NONCE} and nothing else.`;
|
|
215
|
+
try {
|
|
216
|
+
const r = target.obj.sendUserMessage(body, { deliverAs: "user" });
|
|
217
|
+
log({
|
|
218
|
+
event: "send_probe_called",
|
|
219
|
+
pid: process.pid,
|
|
220
|
+
where: target.where,
|
|
221
|
+
elapsedMs: Date.now() - t0,
|
|
222
|
+
returnType: typeof r,
|
|
223
|
+
thenable: typeof r?.then === "function",
|
|
224
|
+
});
|
|
225
|
+
if (r && typeof r.then === "function") {
|
|
226
|
+
r.then(
|
|
227
|
+
(v: unknown) => log({ event: "send_probe_resolved", value: String(v) }),
|
|
228
|
+
(e: unknown) => log({ event: "send_probe_rejected", error: String(e) }),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
log({ event: "send_probe_threw", where: target.where, error: String(err) });
|
|
233
|
+
}
|
|
234
|
+
}, 12_000);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
log({ event: "send_probe_schedule_failed", error: String(err) });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
pi.on("session_start", onEdge("session_start"));
|
|
242
|
+
pi.on("session_switch", onEdge("session_switch"));
|
|
243
|
+
// Turn edges: they say whether (b) actually produced a model turn, and they are the
|
|
244
|
+
// edges a Copilot-style retry would have used — measured so the "retry is not enough"
|
|
245
|
+
// claim keeps a receipt of its own.
|
|
246
|
+
pi.on("agent_start", (_e: any, ctx: any) => snapshot("agent_start", ctx));
|
|
247
|
+
pi.on("turn_start", (_e: any, ctx: any) => snapshot("turn_start", ctx));
|
|
248
|
+
pi.on("turn_end", (_e: any, ctx: any) => snapshot("turn_end", ctx));
|
|
249
|
+
pi.on("session_shutdown", (_e: any, ctx: any) => snapshot("session_shutdown", ctx));
|
|
250
|
+
}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
Author: xai/grok-4.6 (entwurf fresh sibling, 2026-08-27) — source audit for #87; review as a separate viewpoint.
|
|
2
|
+
|
|
3
|
+
Vendor: `~/repos/3rd/oh-my-pi` HEAD `4142f881` = tag `v18.0.0` (`02696f5b`) + 2 natives lint commits. Paths below are `packages/coding-agent/src/…` unless noted. Code is the oracle.
|
|
4
|
+
|
|
5
|
+
# CORRECTIONS SUMMARY
|
|
6
|
+
|
|
7
|
+
Only CORRECTED / UNRESOLVED. Fence-design impact first.
|
|
8
|
+
|
|
9
|
+
1. **A2 — `hasUI` is not the host predicate; `mode==="tui"` is stronger, not weaker.**
|
|
10
|
+
rpc, rpc-ui, and ACP all pass a real `uiContext`, so `ExtensionRunner.hasUI()` is **true** (`runner.ts:879-881` = `uiContext !== noOpUIContext`). types.ts:465 comment ("false in print/RPC mode") **disagrees with code**. No initialize site other than `extension-ui-controller.ts` passes `"tui"`. **Do not gate birth on `hasUI`. Keep `mode==="tui"`.** Nothing in source weakens that fence.
|
|
11
|
+
|
|
12
|
+
2. **H2 — `setFooter` is a no-op on the TUI at v18.** `extension-ui-controller.ts:139` `setFooter: () => {}`. Visible identity cannot use `setFooter`. `setStatus` **does** render (hook-status lines under the status line; `statusLine.showHookStatus` default true).
|
|
13
|
+
|
|
14
|
+
3. **H3 — built-in `statusLine` has no custom-text / command segment.** Closed `StatusLineSegmentId` enum (`status-line/segments.ts:731-757`). Extension-owned persistent text = `setStatus`, not a Copilot-style `statusLine.command`.
|
|
15
|
+
|
|
16
|
+
4. **A1 — initialize call-site map was incomplete.** Direct `ExtensionRunner.initialize` sites: TUI `:302`/`:531` (`"tui"`), ACP `:2524` (`"rpc"`), executor `:3252` (default `"print"`), plus shared wrapper `runtime-init.ts:56` used by print/json/rpc and subagent revive. `rpc-mode.ts:961` is `initializeExtensions({mode:"rpc"})`, not a direct runner call. `runner.ts:651` is `initialize()` itself, not the field default (`:438`).
|
|
17
|
+
|
|
18
|
+
5. **A4 — `omp acp` does load discovered extensions.** `createAcpSessionFactory` (`main.ts:434`) calls `createAgentSession` without `disableExtensionDiscovery`. Mode `"rpc"`, `ExtensionContext.hasUI:true`, session-level `hasUI:false`, `enableMCP:false` at create (ACP then injects client servers). A `mode==="tui"` birth guard **stays silent**.
|
|
19
|
+
|
|
20
|
+
6. **B2 — bundled task agents do NOT set `restrictToolNames:true`.** That flag is a spawn option (planMode / host restrict / security coordinator), not agent frontmatter. scout/reviewer/librarian/security-reviewer have `tools:` allowlists but still inherit extensions unless the flag is set. Default `task`/`sonic` load extensions.
|
|
21
|
+
|
|
22
|
+
UNRESOLVED: none at source layer. LIVE slots in the ledger stay empty by design.
|
|
23
|
+
|
|
24
|
+
# Item ledger
|
|
25
|
+
|
|
26
|
+
## A. Discriminator completeness
|
|
27
|
+
|
|
28
|
+
### A1. Every `ExtensionRunner.initialize` call site — CORRECTED
|
|
29
|
+
|
|
30
|
+
Field default `#mode = "print"`: `extensibility/extensions/runner.ts:438`.
|
|
31
|
+
`initialize(..., mode: ExtensionMode = "print")`: `runner.ts:651-655`.
|
|
32
|
+
|
|
33
|
+
Direct call sites (grep `extensionRunner.initialize` / `runner.initialize`):
|
|
34
|
+
|
|
35
|
+
| site | mode passed | notes |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `modes/controllers/extension-ui-controller.ts:302` | `"tui"` | `initHooksAndCustomTools`; **emits `session_start` :309-311** |
|
|
38
|
+
| `modes/controllers/extension-ui-controller.ts:531` | `"tui"` | `initializeHookRunner`; **no `session_start`**. Production callers of this method: none (only a unit test). |
|
|
39
|
+
| `modes/runtime-init.ts:56` | `options.mode` default `"print"` | shared wrapper; then `emit session_start` `:147` |
|
|
40
|
+
| `modes/acp/acp-agent.ts:2524` 5th arg `:2619` | `"rpc"` | then `emit session_start` `:2603` |
|
|
41
|
+
| `task/executor.ts:3252` | omitted → `"print"` | then `emit session_start` `:3305` |
|
|
42
|
+
|
|
43
|
+
`runtime-init` callers: `modes/print-mode.ts:120` (`mode === "json" ? "json" : "print"`); `modes/rpc/rpc-mode.ts:960-961` (`mode: "rpc"`); executor revive `executor.ts:3164` (no mode → print).
|
|
44
|
+
|
|
45
|
+
Claimed `rpc-mode.ts:961` as a direct initialize: **wrong shape, right mode** — it is `initializeExtensions({mode:"rpc"})`.
|
|
46
|
+
|
|
47
|
+
`cli/models-cli.ts:311` constructs `new ExtensionRunner` and does **not** call `initialize`.
|
|
48
|
+
|
|
49
|
+
### A2. CLI → ExtensionMode + hasUI — CORRECTED (hasUI); tui predicate still sound
|
|
50
|
+
|
|
51
|
+
CLI `Mode` (`cli/args.ts:23`): `"text" | "json" | "rpc" | "acp" | "rpc-ui"`.
|
|
52
|
+
Interactive TUI iff `!print && !autoPrint && mode === undefined` (`main.ts:1406-1407`).
|
|
53
|
+
`sessionOptions.hasUI = isInteractive || mode === "rpc-ui"` (`main.ts:1737`) — this is **session-level** `CreateAgentSessionOptions.hasUI`, not `ExtensionContext.hasUI`.
|
|
54
|
+
|
|
55
|
+
`ExtensionContext.hasUI` = `runner.hasUI()` = `this.#uiContext !== noOpUIContext` (`runner.ts:879-881`, wired at `runner.ts:1163`).
|
|
56
|
+
|
|
57
|
+
| CLI | host runner mode | ExtensionContext.hasUI | session.hasUI |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| plain interactive TUI | `"tui"` | true (TUI uiContext) | true |
|
|
60
|
+
| `-p` / `--print` / `--mode text` | `"print"` | false (no uiContext) | false |
|
|
61
|
+
| `--mode json` | `"json"` | false | false |
|
|
62
|
+
| `--mode rpc` | `"rpc"` | **true** (`rpcUiContext` always passed, `rpc-mode.ts:956-975`) | false |
|
|
63
|
+
| `--mode rpc-ui` | `"rpc"` (not `"tui"`) | **true** | true |
|
|
64
|
+
| `omp acp` / `--mode acp` | `"rpc"` | **true** (ACP uiContext) | false |
|
|
65
|
+
|
|
66
|
+
**Operator-visible TUI whose extensions see `mode !== "tui"`?** No. Only the TUI controller passes `"tui"`.
|
|
67
|
+
**rpc-ui:** operator-visible *elsewhere* (RPC UI client), extensions see `"rpc"`. Issue #87 forbids garden ids for non-tui — this is intended exclusion, not a hole.
|
|
68
|
+
**Non-visible session that sees `"tui"`?** No production path. `:531` is test-only rewire.
|
|
69
|
+
|
|
70
|
+
Fence: `mode==="tui"` is the sound top-level predicate. `hasUI` would admit rpc/rpc-ui/ACP.
|
|
71
|
+
|
|
72
|
+
### A3. Task subagents — CONFIRMED
|
|
73
|
+
|
|
74
|
+
`hasUI: false` at `task/executor.ts:3115` (`CreateAgentSessionOptions`).
|
|
75
|
+
`extensionRunner.initialize(actions, contextActions)` two-arg at `:3252` → default mode `"print"`, default uiContext no-op → `ExtensionContext.hasUI:false`.
|
|
76
|
+
`await extensionRunner.emit({ type: "session_start" })` at `:3305`.
|
|
77
|
+
|
|
78
|
+
### A4. `omp acp` — CORRECTED
|
|
79
|
+
|
|
80
|
+
ACP **does** load discovered extensions: `createAcpSessionFactory` (`main.ts:434-449`) spreads `baseOptions` into `createAgentSession` with `preloadedExtensions: trustedExtensions` (undefined unless `--trusted-extension`) and **does not** set `disableExtensionDiscovery`. Path 3 in `sdk.ts:2031-2034` then discovers + `loadExtensions`.
|
|
81
|
+
`enableMCP: false` at create (`main.ts:446`); client MCP is applied later (`acp-agent.ts:#configureMcpServers`).
|
|
82
|
+
Initialize: mode `"rpc"` (`:2619`), real `createAcpExtensionUiContext` (`:2496`) → `hasUI:true` on the extension context.
|
|
83
|
+
`session_start` at `:2603`.
|
|
84
|
+
A birth extension guarding `mode==="tui"` **stays silent**.
|
|
85
|
+
|
|
86
|
+
### A5. Subagent factory rebind — CONFIRMED
|
|
87
|
+
|
|
88
|
+
The "rebind" is not a later mutation of a live runner. Each subagent `createAgentSession` re-imports factories against **this** session's API:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
sdk.ts:2000-2005 // preloadedExtensionPaths (subagent): skip FS scan, always re-call loadExtensions
|
|
92
|
+
sdk.ts:2024-2028 extensionPaths = options.preloadedExtensionPaths;
|
|
93
|
+
extensionsResult = await loadExtensions(extensionPaths, cwd, eventBus);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`loadExtensions` → `bindExtension` → `runExtensionFactory(factory, api, runtime)` (`loader.ts:362-422`, `:435-443`). `api` is a new `ConcreteExtensionAPI` for that session (`loader.ts:421`).
|
|
97
|
+
|
|
98
|
+
Host must never pass `preloadedExtensions` across the session boundary (`sdk.ts:441-443`).
|
|
99
|
+
|
|
100
|
+
## B. Subagent inheritance
|
|
101
|
+
|
|
102
|
+
### B1. CLI `-e` → subagent — CONFIRMED
|
|
103
|
+
|
|
104
|
+
`main.ts:1304-1306`: `cliExtensionPaths = [...extensions, ...hooks]` → `options.additionalExtensionPaths`.
|
|
105
|
+
`discoverSessionExtensionPaths` (`sdk.ts:720-721`) concatenates those onto discovery.
|
|
106
|
+
Host `createAgentSession` with `preloadedExtensions` captures `toolSession.extensionPaths` (`sdk.ts:2041`).
|
|
107
|
+
Task spawn: `structured-subagent.ts:442` `preloadedExtensionPaths: restrictToolNames ? [] : session.extensionPaths`.
|
|
108
|
+
Executor forwards at `:3096`.
|
|
109
|
+
|
|
110
|
+
**Yes: an `-e` probe extension loads inside task subagents by default**, re-executed per A5, unless `restrictToolNames`.
|
|
111
|
+
|
|
112
|
+
### B2. Bundled agents vs `restrictToolNames` — CORRECTED
|
|
113
|
+
|
|
114
|
+
`task/agents.ts` bundled defs: scout, designer, reviewer, security-reviewer, librarian, task, sonic. **None set `restrictToolNames`.**
|
|
115
|
+
`tools:` allowlists in markdown (not a discovery skip):
|
|
116
|
+
- scout: `read, grep, glob, web_search`
|
|
117
|
+
- reviewer / librarian: `read, grep, glob, bash, lsp, web_search, ast_grep`
|
|
118
|
+
- security-reviewer: `read, grep, glob, lsp, ast_grep`
|
|
119
|
+
- task / sonic / designer: no `tools:` → full host tool set
|
|
120
|
+
|
|
121
|
+
`restrictToolNames: true` is a **spawn option**:
|
|
122
|
+
- `structured-subagent.ts:386` `policy.planMode || session.restrictToolNames === true`
|
|
123
|
+
- `security/coordinator.ts:254` (security scan session, not a TUI task agent)
|
|
124
|
+
- `compress/session.ts:49`
|
|
125
|
+
- isolation runner **clears** paths (`isolation-runner.ts:170`)
|
|
126
|
+
|
|
127
|
+
Agents that would **not** load extensions: any spawn with `restrictToolNames:true` (plan-mode subagents, restricted host, isolated subprocess). Default `task` **does** load them.
|
|
128
|
+
|
|
129
|
+
### B3. Subagent MCP proxy — CONFIRMED
|
|
130
|
+
|
|
131
|
+
`executor.ts:3021-3024`:
|
|
132
|
+
```
|
|
133
|
+
restrictToolNames = options.restrictToolNames === true
|
|
134
|
+
enableMCP = !restrictToolNames && (options.enableMCP ?? true)
|
|
135
|
+
mcpManager = enableMCP ? options.mcpManager : undefined
|
|
136
|
+
mcpProxyTools = mcpManager ? createMCPProxyTools(mcpManager) : []
|
|
137
|
+
```
|
|
138
|
+
Passed as `mcpManager` + `customTools: mcpProxyTools` into `createAgentSession` (`:3123-3124`).
|
|
139
|
+
`createMCPProxyTools` (`executor.ts:807-813`) copies `tool.name` / `mcpServerName` / `mcpToolName` from the **parent** manager's `getTools()`. Same public `mcp__…` names. One bridge child (parent's). No per-subagent MCP process.
|
|
140
|
+
|
|
141
|
+
## C. session_start semantics (host)
|
|
142
|
+
|
|
143
|
+
### C1. When TUI host `session_start` fires — CONFIRMED
|
|
144
|
+
|
|
145
|
+
`interactive-mode.ts:1221` first `requestRender(true)` (window paints).
|
|
146
|
+
`:1238` `await this.initHooksAndCustomTools()` → `extension-ui-controller.ts:302` initialize `"tui"` then `:309-311` emit `session_start`.
|
|
147
|
+
|
|
148
|
+
Fires **after first paint, before first prompt**. No model turn required. **Once per process** for the host runner: `/new` does not re-emit `session_start` (C2).
|
|
149
|
+
|
|
150
|
+
Comment at `:1204-1207` notes session_start `sendMessage({display:true})` can race `renderInitialMessages`.
|
|
151
|
+
|
|
152
|
+
### C2. Re-fire on /new, --resume, --continue, switch/branch — CONFIRMED
|
|
153
|
+
|
|
154
|
+
Host process start (including CLI `--resume`/`--continue`/`--fork`): one `session_start` at TUI init, on whatever session was opened.
|
|
155
|
+
|
|
156
|
+
In-process thereafter, **`session_switch` / `session_branch`, not `session_start`:**
|
|
157
|
+
|
|
158
|
+
| action | events |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `/new` | `session_before_switch` reason `"new"` (`agent-session.ts:6910-6913`) then `session_switch` reason `"new"` (`:6984-6987`) |
|
|
161
|
+
| fork | `session_before_switch` `"fork"` (`:7021`) then `session_switch` `"fork"` (`:7073-7076`) |
|
|
162
|
+
| switch / in-TUI resume | `session_before_switch` `"resume"` (`:7979-7982`) then `session_switch` `"resume"` (`:8071-8074`) |
|
|
163
|
+
| branch | `session_branch` (`:8350`, `:8481`) |
|
|
164
|
+
|
|
165
|
+
A birth hook that mints on `session_start` will not re-mint on `/new` unless it also listens to `session_switch`. Idempotent upsert on switch is the usual pattern; this audit does not implement it.
|
|
166
|
+
|
|
167
|
+
## D. Session identity API
|
|
168
|
+
|
|
169
|
+
### D1. ReadonlySessionManager + file naming — CONFIRMED
|
|
170
|
+
|
|
171
|
+
`session-manager.ts:359-376` `ReadonlySessionManager = Pick<SessionManager, "getCwd" | "getSessionDir" | "getSessionId" | "getSessionFile" | …>`.
|
|
172
|
+
- `getSessionId()` `:1946-1948` → `this.#sessionId`
|
|
173
|
+
- `getSessionFile()` `:1950-1952` → `this.#sessionFile` (`string | undefined`)
|
|
174
|
+
- mint: `mintSessionId()` `:95-97` `return Bun.randomUUIDv7();`
|
|
175
|
+
- file: `` `${fileSafeTimestamp(timestamp)}_${this.#sessionId}.jsonl` `` `:1134-1137`
|
|
176
|
+
- `fileSafeTimestamp` `:103-104` `iso.replace(/[:.]/g, "-")`
|
|
177
|
+
- dir: `getSessionsDir` → `~/.omp/agent/sessions/<cwd-encoded>/` (`utils/src/dirs.ts:781-783`; encoding `session-paths.ts:62-80`)
|
|
178
|
+
|
|
179
|
+
`nativeSessionId` candidate = UUIDv7 string (not the filename). Filename = `<iso-with-colons-as-dashes>_<uuidv7>.jsonl`. Persistence is lazy (`getSessionFile` may name a path not yet on disk).
|
|
180
|
+
|
|
181
|
+
### D2. Extension factory + probe — CONFIRMED (probe names were already right)
|
|
182
|
+
|
|
183
|
+
`ExtensionFactory = (pi: ExtensionAPI) => void | Promise<void>` (`types.ts:1592`).
|
|
184
|
+
`-e` files: default export **or** the module itself if it is a function (`loader.ts:57-61`).
|
|
185
|
+
`ExtensionAPI.on` (`types.ts:1222+`) and `ExtensionAPI.getAllTools` (`types.ts:1433`) exist with those names.
|
|
186
|
+
|
|
187
|
+
Probe guessed `getSessionFile` / `getSessionId` — those **are** the v18 names. Removed nonexistent `getId()` fallback; added `getSessionDir`. File: `scripts/raw-omp-measure/probe-extension.ts`.
|
|
188
|
+
|
|
189
|
+
## E. MCP tool-name dialect
|
|
190
|
+
|
|
191
|
+
### E1. — CONFIRMED
|
|
192
|
+
|
|
193
|
+
`sanitizeMCPToolNamePart` `mcp/tool-bridge.ts:351-357`: lower, `[^a-z_]+` → `_`, collapse `_`, trim edge `_`.
|
|
194
|
+
Redundant prefix strip `:368-374` if `sanitizedToolName.startsWith(sanitizedServerName + "_")`.
|
|
195
|
+
Mint `:375` `capMCPToolNameLength(\`mcp__${sanitizedServerName}_${normalizedToolName}\`)` cap 64 (`:365-366`, `:396`).
|
|
196
|
+
|
|
197
|
+
Server `"entwurf-bridge"` → `entwurf_bridge`.
|
|
198
|
+
None of the tools start with `entwurf_bridge_` → **no prefix-strip**.
|
|
199
|
+
Digit in `entwurf_v2`: `2` → `_` → `entwurf_v_` → trim → `entwurf_v` (end-digit is eaten, not kept as `_`).
|
|
200
|
+
|
|
201
|
+
| tool | minted (all ≤64, unique) |
|
|
202
|
+
|---|---|
|
|
203
|
+
| entwurf_v2 | `mcp__entwurf_bridge_entwurf_v` |
|
|
204
|
+
| entwurf_self | `mcp__entwurf_bridge_entwurf_self` |
|
|
205
|
+
| entwurf_peers | `mcp__entwurf_bridge_entwurf_peers` |
|
|
206
|
+
| entwurf_inbox_read | `mcp__entwurf_bridge_entwurf_inbox_read` |
|
|
207
|
+
| entwurf_register_native | `mcp__entwurf_bridge_entwurf_register_native` |
|
|
208
|
+
| entwurf_fresh_call | `mcp__entwurf_bridge_entwurf_fresh_call` |
|
|
209
|
+
| entwurf_resume_call | `mcp__entwurf_bridge_entwurf_resume_call` |
|
|
210
|
+
|
|
211
|
+
Permission/approval: **same string**. `MCPTool.name` is the minted name (`tool-bridge.ts:481`); `readonly approval = "write"` (`:490`). `tools/approval.ts:110` consults `tools.approval.<tool.name>` (or `policyKey`). `approval.ts:261` special-cases `tool.name.startsWith("mcp__")`. No second Copilot-style dialect.
|
|
212
|
+
|
|
213
|
+
Live tool list remains the acceptance oracle (ledger).
|
|
214
|
+
|
|
215
|
+
## F. MCP discovery / precedence
|
|
216
|
+
|
|
217
|
+
### F1. Source order — CONFIRMED (priority table)
|
|
218
|
+
|
|
219
|
+
Providers inserted highest-priority first (`capability/index.ts:84-91`). Dedupe **first wins** (`:183`).
|
|
220
|
+
|
|
221
|
+
MCP provider priorities:
|
|
222
|
+
|
|
223
|
+
| priority | id | role |
|
|
224
|
+
|---|---|---|
|
|
225
|
+
| 100 | `native` / builtin | OMP `~/.omp/agent/mcp.json`, `<cwd>/.omp/mcp.json` |
|
|
226
|
+
| 90 | `omp-plugins` | installed omp plugins |
|
|
227
|
+
| 80 | `claude` | Claude Code — **third** among these families |
|
|
228
|
+
| 70 | `claude-plugins` | marketplace plugins |
|
|
229
|
+
| 60 | gemini | |
|
|
230
|
+
| 55 | opencode | |
|
|
231
|
+
| 50 | cursor | |
|
|
232
|
+
| 20 | vscode | |
|
|
233
|
+
| 5 | `mcp-json` | standalone project `mcp.json`/`.mcp.json` |
|
|
234
|
+
|
|
235
|
+
Within native (`builtin.ts:207-210`) and Claude (`claude.ts:104-117`) loaders: **project entries pushed before user entries** so a project name claims the dedupe key first. Claude comment `:104-105` ("Load project entries before user entries"). Same-name shadowing = first (highest priority provider, and within a provider project-before-user).
|
|
236
|
+
|
|
237
|
+
### F2. disabledServers / enabledServers / mcp.enableProjectConfig — CONFIRMED
|
|
238
|
+
|
|
239
|
+
`mcp/config.ts:104-127`:
|
|
240
|
+
- `disabledServers` (user mcp.json): `suppressServer` `:123` — **highest-precedence denylist**; suppressed items still claim the dedupe key (`:163-165`).
|
|
241
|
+
- `enabledServers`: allowlist that overrides a source `enabled: false` (`:125`).
|
|
242
|
+
- `mcp.enableProjectConfig` default true (`settings-schema.ts:4660-4666`); when false, project-level items are **filtered out before dedupe** (`config.ts:115-116`) so they cannot shadow.
|
|
243
|
+
|
|
244
|
+
Wired at `sdk.ts:1864`.
|
|
245
|
+
|
|
246
|
+
### F3. Claude import env — CONFIRMED (with `${VAR}` expansion)
|
|
247
|
+
|
|
248
|
+
`discovery/claude.ts:87-97`: `expandEnvVarsDeep(json.mcpServers)` then `env: serverConfig.env as Record<string, string>`.
|
|
249
|
+
`expandEnvVars` (`helpers.ts:446-452`) only substitutes `${NAME}` / `${NAME:-default}`. A literal `ENTWURF_BRIDGE_EXTERNAL_AGENT_ID=external-mcp/claude-code` **rides along unchanged**. Keys are not rewritten.
|
|
250
|
+
|
|
251
|
+
### F4. omp never writes another tool's config — CONFIRMED
|
|
252
|
+
|
|
253
|
+
`mcp/config-writer.ts:301-304`: sourcePath allowed only for native `.omp/mcp.json` and `mcp-json` files. "Tool-owned configs (opencode.json, claude.json, settings.json …) MUST be omitted; we never mutate another tool's file." Toggle of imported servers goes to user `disabledServers` / `enabledServers` (`:320-324`). Writer targets `writeMCPConfigFile` on those omp-owned paths only.
|
|
254
|
+
|
|
255
|
+
## G. MCP child process + env
|
|
256
|
+
|
|
257
|
+
### G1. Bun.spawn env — CONFIRMED
|
|
258
|
+
|
|
259
|
+
`mcp/transports/stdio.ts:577-581`:
|
|
260
|
+
```
|
|
261
|
+
const env = { ...Bun.env, ...this.config.env };
|
|
262
|
+
```
|
|
263
|
+
Full process env inherited, then config entry `env` merged on top. Spawn `:600-609` uses that `env`. **No omp-injected session/agent id.** No `OMP_*` / `PI_SESSION_ID` planted here. Danger is pure inheritance passthrough of the parent's env (M6).
|
|
264
|
+
|
|
265
|
+
### G2. Linux detached + cleanup — CONFIRMED
|
|
266
|
+
|
|
267
|
+
`stdio.ts:41-47`: Linux `detached: true` → setsid (own session, no controlling tty). macOS/Windows `false`.
|
|
268
|
+
`resolveStdioSpawnCommand` `:334` non-win32: `{ cmd, detached: platform !== "darwin" }`.
|
|
269
|
+
setsid does **not** reparent: child ppid stays the omp pid.
|
|
270
|
+
Kill: `signalStdioProcess` `:474-494` — if detached POSIX, `process.kill(-pid, SIGTERM)` (process group), then SIGKILL after grace (`:497+`). `manager.disconnectAll` `:946` discards every connection on teardown.
|
|
271
|
+
|
|
272
|
+
### G3. PI_* inventory (packages/, production) — CONFIRMED absence of PI_SESSION_ID / PI_AGENT_ID
|
|
273
|
+
|
|
274
|
+
Repo-wide `PI_SESSION_ID` / `PI_AGENT_ID`: **nowhere**. Only `MNEMOPI_SESSION_ID` at `packages/mnemopi/src/mcp-tools.ts:434`.
|
|
275
|
+
|
|
276
|
+
Identity-overlap knobs omp **does** read:
|
|
277
|
+
|
|
278
|
+
| var | where |
|
|
279
|
+
|---|---|
|
|
280
|
+
| `PI_CONFIG_DIR` | `packages/utils/src/dirs.ts:210` (default `.omp`) |
|
|
281
|
+
| `PI_CODING_AGENT_DIR` | `dirs.ts:358+` agent dir override |
|
|
282
|
+
| `PI_PROFILE` | `dirs.ts:38,90,120` (legacy; `OMP_PROFILE` wins) |
|
|
283
|
+
| `PI_SMOL_MODEL` / `PI_SLOW_MODEL` / `PI_PLAN_MODEL` | `coding-agent/src/main.ts:1467-1469`; help `cli/help-extra.ts:59-61` |
|
|
284
|
+
| `PI_CODING_AGENT_SESSION_DIR` | `cli/args.ts:156` |
|
|
285
|
+
|
|
286
|
+
Other PI_* omp reads (not identity carriers; incomplete-by-theme, not a second address axis): `PI_COMPILED`, `PI_BUNDLED`, `PI_TIMING`, `PI_DEBUG_STARTUP`, `PI_NO_PTY`, `PI_NO_TITLE`, `PI_NOTIFICATIONS`, `PI_CONFIG_FILES`, `PI_EDIT_VARIANT`, `PI_FORCE_IMAGE_PROTOCOL`, `PI_TINY_*`, `PI_DIALECT`, `PI_PROXY*`, `PI_STREAM_*`, `PI_OPENAI_STREAM_*`, `PI_OPENROUTER_RESPONSES`, `PI_CODEX_*`, `PI_CACHE_RETENTION`, `PI_NO_INTERLEAVED_THINKING`, `PI_NO_THINKING_LOOP_GUARD`, `PI_AUTH_NO_BORROW`, `PI_AI_ANTIGRAVITY_*`, `PI_AI_GEMINI_CLI_VERSION`, `PI_TUI_*`, `PI_BASH_NO_CI`, `PI_SHELL_PREFIX`, `PI_DISABLE_UUTILS_BUILTINS`, `PI_SUBPROCESS_CMD`, `PI_BLOCKED_AGENT`, `PI_TASK_MAX_OUTPUT_*`, `PI_RPC_EMIT_TITLE`, `PI_COMMIT_*`, `PI_PERPLEXITY_*`, `PI_CODEX_WEB_SEARCH_MODEL`, `PI_TOKENIZER_ACCURATE`, `PI_NO_INTENT`, `PI_NATIVE_VARIANT`, `PI_TEST_RUNTIME`, `PI_PACKAGE_DIR`, `PI_DOCS_EMBED`, `PI_TINY_TRANSFORMERS_VERSION`.
|
|
287
|
+
|
|
288
|
+
### G4. Legacy `.pi` — CONFIRMED not a native discovery root
|
|
289
|
+
|
|
290
|
+
Native extension dirs (`discovery/builtin.ts:58-72`, `:482-483`): `PATHS.projectDir` = `CONFIG_DIR_NAME` = **`.omp`** (`utils/src/dirs.ts:23`) and `getAgentDir()` = `~/.omp/agent` (or profile). Scans `<dir>/extensions`. **Not** `~/.pi/agent/extensions` or `<cwd>/.pi/extensions`.
|
|
291
|
+
|
|
292
|
+
`.pi` **is** honored as a **package.json manifest alias**:
|
|
293
|
+
- extension modules: `pkg?.omp ?? pkg?.pi` (`discovery/helpers.ts:616-617`)
|
|
294
|
+
- plugins: `pkg.omp ?? pkg.pi` (`extensibility/plugins/loader.ts:267-273`; `types.ts:27` "from package.json omp or pi field")
|
|
295
|
+
|
|
296
|
+
Examples still mention the old `~/.pi/agent/extensions` path; that is docs drift, not a loader root. **entwurf's pi extensions under `~/.pi/` cannot be ambiently loaded by omp.**
|
|
297
|
+
|
|
298
|
+
## H. Receive + visible identity
|
|
299
|
+
|
|
300
|
+
### H1. `pi.sendUserMessage` idle starts a turn — CONFIRMED
|
|
301
|
+
|
|
302
|
+
`types.ts:1417-1418`: "idle starts a turn; streaming queues as steer unless deliverAs is set."
|
|
303
|
+
`agent-session.ts:6515-6555`: omitted `deliverAs` → `this.prompt(...)` (starts a turn); `followUp`/`steer` only queue.
|
|
304
|
+
TUI wires `sendUserMessage: this.#sendExtensionUserMessage` (`extension-ui-controller.ts:176`) which calls `session.sendUserMessage`. `ctx.setTimeout` is `runner.ts:1178` managed timers — a background callback in TUI mode can call it. (Idle-wake demo is step 7 / out of this audit.)
|
|
305
|
+
|
|
306
|
+
### H2. setStatus / setFooter — CORRECTED
|
|
307
|
+
|
|
308
|
+
API: `types.ts:285` `setStatus`, `:294` `setFooter`.
|
|
309
|
+
TUI implementation (`extension-ui-controller.ts:110,139`):
|
|
310
|
+
```
|
|
311
|
+
setStatus: (key, text) => this.setHookStatus(key, text), // real
|
|
312
|
+
setFooter: () => {}, // NO-OP
|
|
313
|
+
setHeader: () => {}, // NO-OP
|
|
314
|
+
```
|
|
315
|
+
`setHookStatus` → `statusLine.setHookStatus` (`:575-576` → `status-line/component.ts:678-682`).
|
|
316
|
+
Render: `component.ts:2246-2251`, gated by `statusLine.showHookStatus` default **true** (`settings-schema.ts:952-956`).
|
|
317
|
+
ACP uiContext: `setStatus`/`setFooter` both no-ops (`acp-agent.ts:574,577`).
|
|
318
|
+
|
|
319
|
+
**v18 TUI does not render extension `setFooter`.** Persistent garden-id surface = `setStatus` hook lines (or a built-in segment — H3).
|
|
320
|
+
|
|
321
|
+
### H3. Built-in statusLine — CORRECTED (no custom command)
|
|
322
|
+
|
|
323
|
+
`config.yml` `statusLine.*`: preset, leftSegments, rightSegments, separator, showHookStatus, sessionAccent, transparent, segmentOptions, compactThinkingLevel, contextLine (`interactive-mode.ts:1977-1987`).
|
|
324
|
+
Segments are a **closed enum** (`status-line/segments.ts:731-757`): pi, model, mode, path, git, pr, subagents, token_*, cost, context_*, time*, session, hostname, cache_*, session_name, usage, collab. **No `command`, no free-text, no argv renderer.**
|
|
325
|
+
Not an alternative garden-id surface unless one of those segments already shows an id (none does). Use `setStatus`.
|
|
326
|
+
|
|
327
|
+
## I. Config writer shapes
|
|
328
|
+
|
|
329
|
+
### I1. mcp/config-writer.ts — CONFIRMED
|
|
330
|
+
|
|
331
|
+
User file: `getMCPConfigPath("user")` = `path.join(getAgentDir(), "mcp.json")` = `~/.omp/agent/mcp.json` (profile-aware) (`utils/src/dirs.ts:922-926`).
|
|
332
|
+
Project file: `path.join(cwd, ".omp", "mcp.json")` (`getProjectAgentDir` uses literal `CONFIG_DIR_NAME` `.omp`).
|
|
333
|
+
`addMCPServer` (`config-writer.ts:111-143`) writes `{ $schema, mcpServers: { [name]: config } }` via `writeMCPConfigFile`.
|
|
334
|
+
|
|
335
|
+
Minimal valid stdio entry (`config/mcp-schema.json` `$defs/stdioServer`, `mcp/types.ts:99-112`):
|
|
336
|
+
|
|
337
|
+
```json
|
|
338
|
+
{
|
|
339
|
+
"$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
|
|
340
|
+
"mcpServers": {
|
|
341
|
+
"entwurf-bridge": {
|
|
342
|
+
"command": "/abs/path/to/start.sh",
|
|
343
|
+
"args": [],
|
|
344
|
+
"env": {
|
|
345
|
+
"ENTWURF_BRIDGE_EXTERNAL_AGENT_ID": "external-mcp/omp"
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`type: "stdio"` optional (default). `command` required. `env` is a string map.
|
|
353
|
+
|
|
354
|
+
### I2. `omp config` → config.yml — CONFIRMED
|
|
355
|
+
|
|
356
|
+
`cli/config-cli.ts:4-5`, `handleSet` `:363-378`: `parseAndSetValue` then `await settings.flush()`.
|
|
357
|
+
`Settings.#configPath` = `path.join(this.#agentDir, MAIN_CONFIG_FILENAMES[0])` (`settings.ts:401`) with `MAIN_CONFIG_FILENAMES = ["config.yml", "config.yaml"]` (`utils/src/dirs.ts:26`).
|
|
358
|
+
`flush` → `#saveNow` → `#writeYamlAtomically` to that path. **Yes: `omp config set` writes `~/.omp/agent/config.yml`.**
|
|
359
|
+
|
|
360
|
+
# Cross-review of external review (2026-08-27)
|
|
361
|
+
|
|
362
|
+
Reviewer: B-bot (sonnet) on issue #87 comment. Oracle: same v18.0.0 checkout. This section only; original ledger unchanged.
|
|
363
|
+
|
|
364
|
+
## (1) CORRECTION-1 — `:531` is test-only vs production TUI caller — OVERSTATED (B-bot); original "test-only" STANDS
|
|
365
|
+
|
|
366
|
+
B-bot: `interactive-mode.ts:4468` calls `initializeHookRunner` in production, so `:531` is not test-only; predicate still holds because that caller is a TUI surface.
|
|
367
|
+
|
|
368
|
+
Walk of the full caller chain:
|
|
369
|
+
|
|
370
|
+
- `extension-ui-controller.ts:531` lives in `initializeHookRunner` (`:393`). Always passes `"tui"` (`:531`); `_hasUI` is unused.
|
|
371
|
+
- In-repo `.initializeHookRunner(` call sites (whole `packages/`):
|
|
372
|
+
- `interactive-mode.ts:4468` — **method body**, not a caller. Public delegate `InteractiveMode.initializeHookRunner` → controller.
|
|
373
|
+
- `test/repro-issue-1020-ctx-shutdown.test.ts:46` — **only actual call**, on the controller, from a unit test.
|
|
374
|
+
- Production TUI boot does **not** go through `:531`. It goes `interactive-mode.ts:1238` `initHooksAndCustomTools()` → controller `:100` → initialize `:302` + `session_start` `:309-311`.
|
|
375
|
+
- Grep for `ctx.initializeHookRunner` / `mode.initializeHookRunner` / `session.initializeHookRunner`: **zero hits**.
|
|
376
|
+
|
|
377
|
+
B-bot mistook a **definition** (`:4467-4468`) for a production call. Fable's spot-check was right.
|
|
378
|
+
|
|
379
|
+
Public-API nuance (does not revive B-bot's claim):
|
|
380
|
+
- `InteractiveMode` is a package export (`src/index.ts:35` `export * from "./modes"` → `modes/index.ts:9` `export * from "./interactive-mode"`; `package.json` `"./modes"` / `"./modes/*"`).
|
|
381
|
+
- `InteractiveModeContext.initializeHookRunner` is on the interface (`modes/types.ts:244`).
|
|
382
|
+
- `ExtensionUiController` is reachable via `"./modes/controllers/*"`.
|
|
383
|
+
- An out-of-repo SDK consumer *could* call `:531`. That path still hardcodes `mode: "tui"`. It is not the operator `omp` TUI. Predicate soundness holds.
|
|
384
|
+
|
|
385
|
+
Final: in-repo operator path, `:531` is test-only. Keep the original A1 note.
|
|
386
|
+
|
|
387
|
+
## (2) CORRECTION-2 — digit eaten by sanitize, not cap — CONFIRMED
|
|
388
|
+
|
|
389
|
+
Matches E1. `sanitizeMCPToolNamePart` (`mcp/tool-bridge.ts:351-357`) charset is `[a-z_]`; `2` in `entwurf_v2` → `_` → trim → `entwurf_v`. Length of `mcp__entwurf_bridge_entwurf_v` is 29, under the 64-char cap (`:365-366`, `:396`). Cap never fires.
|
|
390
|
+
|
|
391
|
+
## (3) DEFECT-1 — native shadows import iff same server key — CONFIRMED (with denylist amendment)
|
|
392
|
+
|
|
393
|
+
Location: `capability/mcp.ts:72-92` `isSameMCPConnection` (stdio: command/args/**env**/cwd `deepEquals`); `key: server => server.name` `:98`; `equivalent: isSameMCPConnection` `:99`. Dedupe: `capability/index.ts:183-210`.
|
|
394
|
+
|
|
395
|
+
**(a) Two paths, ordered.** For each item:
|
|
396
|
+
1. If suppressed: claim `key` into `seen`, skip (never survives, never `equivalent`-shadows) (`:191-196`).
|
|
397
|
+
2. If `key` already in `seen` → `_shadowed = true`, not pushed to `deduped` (`:203-207`). **`equivalent` is not consulted.**
|
|
398
|
+
3. Else `aliasSeen = equivalent(existing, item)` only when `!keySeen` (`:205`). Same connection, different name → later name dropped; one connection under the first name.
|
|
399
|
+
|
|
400
|
+
`loadAllMCPConfigs` consumes **`result.items` only** (`mcp/config.ts:137-140`) = `deduped`. Shadowed entries are silent (no warning in the dedupe loop).
|
|
401
|
+
|
|
402
|
+
**(b) Same name, different env (planned case).** Native `ENTWURF_BRIDGE_EXTERNAL_AGENT_ID=external-mcp/omp` vs imported `…=external-mcp/claude-code`: `equivalent` would be **false**, but it never runs because `keySeen` is true. First-wins-by-key **fully suppresses** the import. Not both loaded, not replaced, no warning. Native provider priority 100 before Claude 80 (`discovery/builtin.ts:42` vs `claude.ts:35`), so the native entry is first.
|
|
403
|
+
|
|
404
|
+
**(c) Claude import key.** `discovery/claude.ts:88-92`: `Object.entries(mcpServers).map(([name, config]) => ({ name, … }))`. Plain `mcpServers` object key. No prefix. Host `~/.claude.json` `mcpServers.entwurf-bridge` → `name: "entwurf-bridge"`.
|
|
405
|
+
|
|
406
|
+
**(d) `disabledServers`.** `suppressServer` (`mcp/config.ts:123-127`) is `disabledServers.has(server.name)`. Both native and imported share `name === "entwurf-bridge"`, so **both are suppressed**. Suppressed items still claim the key (`index.ts:163-165`, `:191-196`) → nothing else named `entwurf-bridge` survives. Denylisting `entwurf-bridge` to hide the Claude import would also hide the native writer entry. That is a different operator action from shadowing.
|
|
407
|
+
|
|
408
|
+
**Acceptance sentence:** "native writer uses the same server key as the imported entry, gate-pinned" is **necessary and sufficient for shadowing**. Amend: pin the literal key `entwurf-bridge` (the `mcpServers` map key); env may differ (that is the label swap); do **not** treat `disabledServers: ["entwurf-bridge"]` as the hide-import mechanism when a native entry exists — it would kill both.
|
|
409
|
+
|
|
410
|
+
## (4) ACP `enableMCP:false` as a free second fence — CONFIRMED restatement, OVERSTATED as a fence
|
|
411
|
+
|
|
412
|
+
A4 stands. `createAcpSessionFactory` comment + flag (`main.ts:407-412`, `:446`): `enableMCP: false` so `createAgentSession` skips on-disk `.mcp.json` discovery (else host tools shadow client servers, issue #1234).
|
|
413
|
+
|
|
414
|
+
Not a fence against **all** MCP. ACP MCP is owned by the client: `session/new.mcpServers` → `#configureMcpServers` (`acp-agent.ts:2608-2669`) builds a fresh `MCPManager` and `connectServers` on whatever the client sent (`:2649-2659`). If an ACP client passes an `entwurf-bridge` stdio/http server, those tools **do** surface (`refreshMCPTools`). Disk-import is skipped; client-supplied MCP is not. "Bridge tools never appear in ACP even if the tui fence loosened" is overstated — they appear iff the ACP client hands them over.
|