@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/DELIVERY.md +3 -2
  3. package/README.md +70 -87
  4. package/VERIFY.md +4 -1
  5. package/docs/acp-backend-rail.md +1 -1
  6. package/docs/external-mcp-host.md +147 -9
  7. package/docs/setup-clean-host.md +105 -0
  8. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +65 -20
  9. package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +17 -6
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +1 -1
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +172 -5
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +152 -17
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/omp-fresh-preflight.js +271 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +988 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-receive-omp.js +574 -0
  17. package/mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js +84 -0
  18. package/mcp/entwurf-bridge/src/index.ts +65 -19
  19. package/mcp/entwurf-bridge/tsconfig.build.json +19 -3
  20. package/mcp/tsconfig.json +10 -0
  21. package/package.json +13 -3
  22. package/pi/entwurf-capabilities.json +1 -0
  23. package/pi/meta-bridge-omp/entwurf-meta-omp/package.json +7 -0
  24. package/pi/omp-receive/entwurf-receive-omp/package.json +7 -0
  25. package/pi-extensions/entwurf-control.ts +12 -9
  26. package/pi-extensions/lib/entwurf-self-address.ts +18 -7
  27. package/pi-extensions/lib/meta-sender-identity.ts +1 -1
  28. package/pi-extensions/lib/meta-session.ts +219 -5
  29. package/pi-extensions/lib/mux-fresh-call.ts +171 -17
  30. package/pi-extensions/lib/omp-fresh-preflight.ts +275 -0
  31. package/pi-extensions/meta-bridge-omp.ts +1244 -0
  32. package/pi-extensions/meta-bridge-receive-omp.ts +666 -0
  33. package/run.sh +296 -4
  34. package/scripts/check-entwurf-self-address.ts +31 -0
  35. package/scripts/check-gate-qualification.ts +4 -1
  36. package/scripts/check-harness-admission-parity.ts +143 -0
  37. package/scripts/check-meta-doctor-oracle.sh +20 -0
  38. package/scripts/check-omp-birth-hook.ts +1049 -0
  39. package/scripts/check-omp-fresh-preflight.ts +181 -0
  40. package/scripts/check-omp-receive-arm.ts +516 -0
  41. package/scripts/inventory-verification-surface.ts +1 -1
  42. package/scripts/meta-bridge-hook-log.sh +9 -1
  43. package/scripts/mutants/copilot-birth.json +7 -5
  44. package/scripts/mutants/mux-fresh-call.json +22 -22
  45. package/scripts/mutants/omp-birth.json +173 -0
  46. package/scripts/mutants/omp-fresh.json +300 -0
  47. package/scripts/mutants/omp-receive.json +135 -0
  48. package/scripts/mutants/self-address.json +34 -0
  49. package/scripts/omp-bridge-doctor.sh +315 -0
  50. package/scripts/omp-bridge-install.sh +221 -0
  51. package/scripts/omp-bridge-oracle.sh +154 -0
  52. package/scripts/omp-bridge-uninstall.sh +57 -0
  53. package/scripts/omp-mcp-bridge.sh +320 -0
  54. package/scripts/omp-mcp-config.py +392 -0
  55. package/scripts/omp-receive-doctor.sh +246 -0
  56. package/scripts/omp-receive-facts.ts +106 -0
  57. package/scripts/omp-receive-install.sh +228 -0
  58. package/scripts/omp-receive-uninstall.sh +60 -0
  59. package/scripts/omp-tool-surface.py +349 -0
  60. package/scripts/raw-omp-measure/README.md +420 -0
  61. package/scripts/raw-omp-measure/probe-extension.ts +76 -0
  62. package/scripts/raw-omp-measure/probe-receive-surface.ts +250 -0
  63. package/scripts/raw-omp-measure/source-audit.md +414 -0
  64. package/scripts/smoke-omp-bridge-state.sh +221 -0
  65. package/scripts/smoke-omp-fresh-live.ts +497 -0
  66. package/scripts/smoke-omp-mcp-state.sh +327 -0
  67. package/scripts/smoke-omp-receive-live.ts +340 -0
  68. package/scripts/smoke-omp-receive-state.sh +196 -0
  69. 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
+ };