@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,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* omp-fresh-preflight — the pre-mutation capability check an OMP fresh call needs
|
|
3
|
+
* (#87 Bundle C, `docs/adding-a-harness.md` step 9 clauses 3, 4 and 5).
|
|
4
|
+
*
|
|
5
|
+
* Same shape and the same reasons as `copilot-fresh-preflight.ts`: a doctor answers "is this
|
|
6
|
+
* host correctly wired?" and may be slow, spawn the vendor and read live processes. This is not
|
|
7
|
+
* that. It answers ONE narrower question at ONE moment — before `mux-fresh-call` mutates the
|
|
8
|
+
* operator's tmux session, are the things the fresh contract requires actually in place on this
|
|
9
|
+
* filesystem?
|
|
10
|
+
*
|
|
11
|
+
* ── Why OMP has FIVE axes where Copilot has four ──
|
|
12
|
+
*
|
|
13
|
+
* birth — without the birth extension the session mints no record, so the callback
|
|
14
|
+
* carries no garden id and the sibling never becomes addressable.
|
|
15
|
+
* MCP hand — without the native `entwurf-bridge` server the callback tool does not
|
|
16
|
+
* exist in that session and the first turn has nothing to call.
|
|
17
|
+
* receive — without the receiver extension the sibling can be launched and can call
|
|
18
|
+
* home, and then nothing can ever be delivered TO it.
|
|
19
|
+
* visible identity — the garden id must be on the harness's own persistent surface.
|
|
20
|
+
* callback callable — THE OMP-SPECIFIC ONE. `tools.xdev` defaults to TRUE, which mounts MCP
|
|
21
|
+
* tools as `xd://` devices whose schemas never reach the prompt. The tool
|
|
22
|
+
* would be configured, the bridge would be running, and the model still
|
|
23
|
+
* could not call `mcp__entwurf_bridge_entwurf_v`. Step 9 clause 5 makes the
|
|
24
|
+
* callback the FIRST action, so a fresh call onto a default-config host
|
|
25
|
+
* opens a window that can never name itself. `[측정]` #87 A-lane: the
|
|
26
|
+
* vendor default wrapped the send tool and produced a false delivery report.
|
|
27
|
+
*
|
|
28
|
+
* Copilot's fourth axis is a statusline COMMAND that must resolve on PATH. OMP has no such
|
|
29
|
+
* surface: `ctx.ui.setStatus` inside the birth extension is the only thing that renders
|
|
30
|
+
* extension-owned text on a v18 TUI (`pi-extensions/meta-bridge-omp.ts:163-169`), and the
|
|
31
|
+
* vendor gates it on `statusLine.showHookStatus` (default true). So visible identity here is
|
|
32
|
+
* "the birth extension is installed AND the operator has not turned hook status off" — a
|
|
33
|
+
* different predicate for the same clause, derived rather than copied.
|
|
34
|
+
*
|
|
35
|
+
* ── What this deliberately does NOT claim ──
|
|
36
|
+
*
|
|
37
|
+
* Ownership/configuration truth only. It does NOT prove omp loaded the extensions, connected
|
|
38
|
+
* the MCP server, or rendered a garden id — that is runtime truth and belongs to
|
|
39
|
+
* `doctor-omp-bridge` / `doctor-omp-receive` / `doctor-omp-mcp` and to the clause 7 LIVE
|
|
40
|
+
* receipt. A green preflight is a statement about this filesystem, not a prediction about the
|
|
41
|
+
* next process.
|
|
42
|
+
*
|
|
43
|
+
* No vendor spawn, no network, no await, no mutation.
|
|
44
|
+
*/
|
|
45
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
46
|
+
import * as path from "node:path";
|
|
47
|
+
const BIRTH_UNIT = "entwurf-meta-omp";
|
|
48
|
+
const RECEIVE_UNIT = "entwurf-receive-omp";
|
|
49
|
+
const MCP_SERVER_KEY = "entwurf-bridge";
|
|
50
|
+
/** Vendor load order, `utils/src/dirs.ts` MAIN_CONFIG_FILENAMES. */
|
|
51
|
+
const MAIN_CONFIG_FILENAMES = ["config.yml", "config.yaml"];
|
|
52
|
+
/** `utils/src/dirs.ts` PROFILE_NAME_RE, reproduced from `scripts/omp-bridge-oracle.sh`. */
|
|
53
|
+
const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
54
|
+
/**
|
|
55
|
+
* The agent directory omp itself would read, or `null` for REFUSE.
|
|
56
|
+
*
|
|
57
|
+
* This is `omp_agent_dir` from `scripts/omp-bridge-oracle.sh`, expressed in the language this
|
|
58
|
+
* half is written in. It is a reproduction on purpose and not a spawn of that script: the fresh
|
|
59
|
+
* lane runs inside the pi extension and inside the bundled MCP child, and resolving a sibling
|
|
60
|
+
* shell script by relative path from two different emit depths is the exact arithmetic
|
|
61
|
+
* `check-capability-bundle-reach` exists to catch. The AGREEMENT of the two implementations is
|
|
62
|
+
* pinned by `check-omp-fresh-preflight`, which drives both over the same environments.
|
|
63
|
+
*
|
|
64
|
+
* A `PI_*` knob is a refusal and never a lookup (#87 ledger M6): omp is a pi fork that kept pi's
|
|
65
|
+
* env vocabulary, so `PI_CODING_AGENT_DIR` steers TWO harnesses and a value in the environment
|
|
66
|
+
* does not say which one it is addressing. Guessing here would aim the preflight at a directory
|
|
67
|
+
* no live omp reads, and it would report green off an empty one.
|
|
68
|
+
*/
|
|
69
|
+
export function ompAgentDir(env) {
|
|
70
|
+
const explicit = env.ENTWURF_OMP_AGENT_DIR;
|
|
71
|
+
if (typeof explicit === "string" && explicit.length > 0) {
|
|
72
|
+
const home = env.HOME;
|
|
73
|
+
const expanded = explicit === "~" || explicit.startsWith("~/")
|
|
74
|
+
? typeof home === "string" && home.length > 0
|
|
75
|
+
? path.join(home, explicit.slice(1))
|
|
76
|
+
: null
|
|
77
|
+
: explicit;
|
|
78
|
+
return expanded === null ? null : path.resolve(expanded);
|
|
79
|
+
}
|
|
80
|
+
if (typeof env.PI_CODING_AGENT_DIR === "string" && env.PI_CODING_AGENT_DIR.length > 0)
|
|
81
|
+
return null;
|
|
82
|
+
if (typeof env.PI_CONFIG_DIR === "string" && env.PI_CONFIG_DIR.length > 0)
|
|
83
|
+
return null;
|
|
84
|
+
const ompProfile = env.OMP_PROFILE;
|
|
85
|
+
const hasOmpProfile = typeof ompProfile === "string" && ompProfile.length > 0;
|
|
86
|
+
if (typeof env.PI_PROFILE === "string" && env.PI_PROFILE.length > 0 && !hasOmpProfile)
|
|
87
|
+
return null;
|
|
88
|
+
const home = env.HOME;
|
|
89
|
+
if (typeof home !== "string" || home.length === 0)
|
|
90
|
+
return null;
|
|
91
|
+
if (hasOmpProfile) {
|
|
92
|
+
if (!PROFILE_NAME_RE.test(ompProfile))
|
|
93
|
+
return null;
|
|
94
|
+
return path.join(home, ".omp", "profiles", ompProfile, "agent");
|
|
95
|
+
}
|
|
96
|
+
return path.join(home, ".omp", "agent");
|
|
97
|
+
}
|
|
98
|
+
function isDir(p) {
|
|
99
|
+
try {
|
|
100
|
+
return statSync(p).isDirectory();
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Bounded environment probing, Hard Rule 15's stated exception.
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** A JSON object or nothing. Unreadable, unparseable and not-an-object are the SAME answer,
|
|
108
|
+
* because the caller's next move is identical in all three: run the installer. */
|
|
109
|
+
function readJsonObject(file) {
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
112
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
113
|
+
? parsed
|
|
114
|
+
: null;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The effective value of one TOP-LEVEL-then-one-key path in omp's config, as a tri-state:
|
|
122
|
+
* `true` / `false` / `null` for "not stated, or not readable with confidence".
|
|
123
|
+
*
|
|
124
|
+
* A deliberately NARROW block-YAML reader — it understands exactly the shape these two settings
|
|
125
|
+
* are written in (`tools:` newline, two-space `xdev: false`) and answers `null` for everything
|
|
126
|
+
* else. It mirrors `scripts/omp-tool-surface.py`'s scalar vocabulary for booleans, including its
|
|
127
|
+
* `true/yes/on/y` word set and its comment stripping, but it does NOT try to be that parser: the
|
|
128
|
+
* Python leaf reports a full verdict for a doctor, this one answers one question for a refusal.
|
|
129
|
+
*
|
|
130
|
+
* The tri-state is what makes both callers able to fail in the direction their axis needs, so
|
|
131
|
+
* neither has to invent a default here.
|
|
132
|
+
*/
|
|
133
|
+
export function readOmpConfigFlag(agentDir, section, key) {
|
|
134
|
+
let text = null;
|
|
135
|
+
for (const name of MAIN_CONFIG_FILENAMES) {
|
|
136
|
+
const candidate = path.join(agentDir, name);
|
|
137
|
+
if (!existsSync(candidate))
|
|
138
|
+
continue;
|
|
139
|
+
try {
|
|
140
|
+
if (statSync(candidate).isDirectory())
|
|
141
|
+
return null;
|
|
142
|
+
text = readFileSync(candidate, "utf8");
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
if (text === null)
|
|
150
|
+
return null;
|
|
151
|
+
// A tab anywhere in the body makes this not-YAML for the vendor's own reader; refuse rather
|
|
152
|
+
// than guess which indentation the vendor would have seen.
|
|
153
|
+
let inSection = false;
|
|
154
|
+
// The indent of the section's IMMEDIATE children, learned from the first one. Matching a key
|
|
155
|
+
// at any deeper indent would read `tools.nested.xdev` as `tools.xdev` — a fail-OPEN misread
|
|
156
|
+
// that would preflight green off a config which never set the flag. Caught by
|
|
157
|
+
// `check-omp-fresh-preflight` when this reader was first compared against the python leaf.
|
|
158
|
+
let childIndent = -1;
|
|
159
|
+
for (const raw of text.split("\n")) {
|
|
160
|
+
const body = stripComment(raw);
|
|
161
|
+
if (body.trim() === "")
|
|
162
|
+
continue;
|
|
163
|
+
if (body.includes("\t"))
|
|
164
|
+
return null;
|
|
165
|
+
const indent = body.length - body.trimStart().length;
|
|
166
|
+
const trimmed = body.trim();
|
|
167
|
+
if (!inSection) {
|
|
168
|
+
if (indent === 0 && trimmed === `${section}:`)
|
|
169
|
+
inSection = true;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (indent === 0)
|
|
173
|
+
break; // the section ended at the next top-level key
|
|
174
|
+
if (childIndent === -1)
|
|
175
|
+
childIndent = indent;
|
|
176
|
+
if (indent !== childIndent)
|
|
177
|
+
continue; // a grandchild, or a mis-indented line: not our key
|
|
178
|
+
const colon = trimmed.indexOf(":");
|
|
179
|
+
if (colon <= 0)
|
|
180
|
+
continue;
|
|
181
|
+
if (trimmed.slice(0, colon).trim() !== key)
|
|
182
|
+
continue;
|
|
183
|
+
return parseBool(trimmed.slice(colon + 1).trim());
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
/** Comment stripping with the same quote awareness as `omp-tool-surface.py:strip_comment`. */
|
|
188
|
+
function stripComment(raw) {
|
|
189
|
+
let inSingle = false;
|
|
190
|
+
let inDouble = false;
|
|
191
|
+
let escaped = false;
|
|
192
|
+
for (let i = 0; i < raw.length; i++) {
|
|
193
|
+
const ch = raw[i];
|
|
194
|
+
if (escaped) {
|
|
195
|
+
escaped = false;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (ch === "\\" && inDouble) {
|
|
199
|
+
escaped = true;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (ch === "'" && !inDouble)
|
|
203
|
+
inSingle = !inSingle;
|
|
204
|
+
else if (ch === '"' && !inSingle)
|
|
205
|
+
inDouble = !inDouble;
|
|
206
|
+
else if (ch === "#" && !inSingle && !inDouble)
|
|
207
|
+
return raw.slice(0, i).trimEnd();
|
|
208
|
+
}
|
|
209
|
+
return raw.trimEnd();
|
|
210
|
+
}
|
|
211
|
+
/** `omp-tool-surface.py:parse_scalar`'s boolean vocabulary; anything else is "not a boolean". */
|
|
212
|
+
function parseBool(text) {
|
|
213
|
+
const folded = text.toLowerCase();
|
|
214
|
+
if (["true", "yes", "on", "y"].includes(folded))
|
|
215
|
+
return true;
|
|
216
|
+
if (["false", "no", "off", "n"].includes(folded))
|
|
217
|
+
return false;
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The five axes, decided in the order an operator should repair them. Returns the FIRST missing
|
|
222
|
+
* capability, or `null` when every one is in place.
|
|
223
|
+
*
|
|
224
|
+
* Order is not cosmetic. The agent dir comes first because every other predicate is a path under
|
|
225
|
+
* it — reporting "birth unit missing" while the directory itself is ambiguous would send the
|
|
226
|
+
* operator to reinstall into a directory omp may never read. Birth precedes the rest because a
|
|
227
|
+
* host with no birth unit has nothing else worth checking, and visible identity depends on the
|
|
228
|
+
* same unit.
|
|
229
|
+
*/
|
|
230
|
+
export function ompFreshPreflight(env) {
|
|
231
|
+
const agentDir = ompAgentDir(env);
|
|
232
|
+
if (agentDir === null)
|
|
233
|
+
return "omp-agent-dir-ambiguous";
|
|
234
|
+
const extensions = path.join(agentDir, "extensions");
|
|
235
|
+
if (!isDir(path.join(extensions, BIRTH_UNIT)))
|
|
236
|
+
return "omp-birth-unit-missing";
|
|
237
|
+
// The native MCP entry omp reads, at the ONE non-configurable path the installer owns
|
|
238
|
+
// (`scripts/omp-mcp-bridge.sh`: `<resolved omp agent dir>/mcp.json`).
|
|
239
|
+
const mcp = readJsonObject(path.join(agentDir, "mcp.json"));
|
|
240
|
+
const servers = mcp?.mcpServers;
|
|
241
|
+
const hand = typeof servers === "object" && servers !== null && !Array.isArray(servers)
|
|
242
|
+
? servers[MCP_SERVER_KEY]
|
|
243
|
+
: undefined;
|
|
244
|
+
if (typeof hand !== "object" || hand === null || Array.isArray(hand))
|
|
245
|
+
return "omp-mcp-hand-missing";
|
|
246
|
+
if (!isDir(path.join(extensions, RECEIVE_UNIT)))
|
|
247
|
+
return "omp-receive-unit-missing";
|
|
248
|
+
// Visible identity: the vendor default is TRUE, so only an explicit false refuses. An
|
|
249
|
+
// unreadable config is not a refusal HERE — it is one on the axis below, which needs proof
|
|
250
|
+
// rather than absence, and reporting the same file twice under two names would send an
|
|
251
|
+
// operator looking for two problems.
|
|
252
|
+
if (readOmpConfigFlag(agentDir, "statusLine", "showHookStatus") === false)
|
|
253
|
+
return "omp-visible-identity-missing";
|
|
254
|
+
// Callback callable: the vendor default is TRUE and true is the BROKEN state, so this axis
|
|
255
|
+
// requires positive proof of `false`. Absent file, absent key and unparseable config all
|
|
256
|
+
// refuse — that is the fail-closed direction, and it is the opposite of the axis above for
|
|
257
|
+
// the same reason: each fails toward the value the vendor would actually apply.
|
|
258
|
+
if (readOmpConfigFlag(agentDir, "tools", "xdev") !== false)
|
|
259
|
+
return "omp-callback-tool-uncallable";
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
/** Repair text lives on the leaf that decides the predicate, so the sentence an operator reads
|
|
263
|
+
* cannot drift away from the check that produced it. */
|
|
264
|
+
export const OMP_PREFLIGHT_HINT = {
|
|
265
|
+
"omp-agent-dir-ambiguous": "an inherited PI_CODING_AGENT_DIR / PI_CONFIG_DIR / PI_PROFILE makes it ambiguous which agent directory omp would read (omp is a pi fork and shares those names) — unset it, or set ENTWURF_OMP_AGENT_DIR explicitly; guessing would preflight a directory no live omp reads",
|
|
266
|
+
"omp-birth-unit-missing": "this host has no OMP birth extension, so the sibling would mint no record and its callback would carry no garden id — run `entwurf install-omp-bridge`",
|
|
267
|
+
"omp-mcp-hand-missing": "this host has no entwurf-bridge server in omp's own mcp.json, so the callback tool would not exist in that session — run `entwurf install-omp-mcp`",
|
|
268
|
+
"omp-receive-unit-missing": "this host has no OMP receiver extension, so the sibling could call home and nothing could ever be delivered to it — run `entwurf install-omp-receive`",
|
|
269
|
+
"omp-visible-identity-missing": "omp's statusLine.showHookStatus is set to false, so the citizen's garden id would render nowhere on its own TUI — remove that setting from the omp config",
|
|
270
|
+
"omp-callback-tool-uncallable": "omp's tools.xdev is not set to false, so MCP tools mount as xd:// devices whose schemas never reach the prompt and the model cannot call the callback tool — set `tools: xdev: false` in the omp agent config",
|
|
271
|
+
};
|