@junghanacs/entwurf 0.12.4 → 0.12.5
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 +14 -0
- package/README.md +9 -1
- package/docs/setup-clean-host.md +27 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +218 -0
- package/mcp/entwurf-bridge/tsconfig.build.json +18 -1
- package/package.json +3 -2
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +3 -3
- package/pi-extensions/meta-bridge-hook.ts +8 -3
- package/run.sh +127 -2
- package/scripts/__pycache__/meta-bridge-state.cpython-312.pyc +0 -0
- package/scripts/meta-bridge-doctor.sh +50 -9
- package/scripts/meta-bridge-install.sh +46 -12
- package/scripts/meta-bridge-state.py +20 -1
- package/scripts/meta-bridge-uninstall.sh +13 -0
- package/scripts/smoke-meta-install-state.sh +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to this project will be documented here. Format follows [Kee
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.12.5 — 2026-07-01
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **The plugin hook now runs as compiled JS when installed — the strip-types class is closed for good.** The SessionStart/UserPromptSubmit hook ran as a raw `meta-bridge-hook.ts`. When the marketplace source sits below `node_modules` (a global npm/pnpm install), Claude executes that `.ts` directly and Node refuses strip-types there (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`), so every session failed. The hook was the last `.ts`-at-runtime surface; `build-bridge` now emits `dist/pi-extensions/meta-bridge-hook.js` and the installer copies that compiled closure (dev clones still run the `.ts` for transparent editing), mirroring the same installed-vs-dev split `start.sh` (0.12.1) and the store-doctor (0.12.4) already use. `check-pack-install` now runs the installed compiled hook **from under `node_modules` with plain `node`** and proves a raw `.ts` at the same location is refused — the exact fence that broke real hosts is a green regression. The doctor's writer-version parity compares `meta-session.<js|ts>` by install mode so a compiled bundle no longer false-STALEs against the `.ts` source.
|
|
12
|
+
- **Installed meta-bridge paths are now stable across package upgrades.** The installed statusline uses a new `entwurf-statusline` bin shim (matching the existing `entwurf-bridge` MCP shim), and installed meta-bridge marketplace assemblies move out of versioned pnpm store paths into a version-stable operator data directory. Package upgrades still require `entwurf install-meta-bridge` to refresh the Claude plugin bundle/cache, but old-store dead links no longer strand `statusLine` or marketplace source paths. The doctor now also executes the cached SessionStart hook once in an isolated temp agent dir, catching stale plugin-cache / hook-syntax failures before the next real Claude Code session.
|
|
13
|
+
|
|
14
|
+
### Verification
|
|
15
|
+
|
|
16
|
+
- Oracle and local review both reproduced the installed hook fence: compiled `meta-bridge-hook.js` runs from under `node_modules` with plain `node`, while the raw `.ts` at the same location fails specifically with `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`.
|
|
17
|
+
- `./run.sh check-pack` and `./run.sh check-pack-install` passed on 2026-07-01 with the hook-JS tarball requirements, baked `hooks.json` `.js` assertion, and raw-`.ts` fence reproduction.
|
|
18
|
+
- `pnpm check` passed on 2026-07-01 after the hook-JS + stable installed-path changes.
|
|
19
|
+
- `./run.sh install-meta-bridge && ./run.sh doctor-meta-bridge` passed from the development clone after re-materializing the gitignored `.assembled` bundle.
|
|
20
|
+
|
|
7
21
|
## 0.12.4 — 2026-07-01
|
|
8
22
|
|
|
9
23
|
### Fixed
|
package/README.md
CHANGED
|
@@ -80,10 +80,11 @@ A few words that look unusual for a coding tool.
|
|
|
80
80
|
adapter that hosts the ACP plugin and live control-socket surface, but the base
|
|
81
81
|
install is **not** `pi install npm:...` anymore.
|
|
82
82
|
|
|
83
|
-
The package exposes
|
|
83
|
+
The package exposes three bins:
|
|
84
84
|
|
|
85
85
|
- `entwurf` → `run.sh` (installer, checks, meta-bridge doctor/install)
|
|
86
86
|
- `entwurf-bridge` → the MCP stdio launcher (`mcp/entwurf-bridge/start.sh`)
|
|
87
|
+
- `entwurf-statusline` → the Claude Code statusline renderer (`scripts/meta-bridge-statusline.sh`); a stable bin shim so a package upgrade tracks the current version without rewriting Claude settings
|
|
87
88
|
|
|
88
89
|
The bridge does not provide Claude credentials, tokens, or subscription access,
|
|
89
90
|
and does not bypass any backend auth. Whatever the operator's local `claude` /
|
|
@@ -187,6 +188,13 @@ raw `.ts` helpers under `node_modules`. If the doctor reports
|
|
|
187
188
|
`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`, reinstall a current package before
|
|
188
189
|
trusting the floor result.
|
|
189
190
|
|
|
191
|
+
After upgrading a globally installed package, run `entwurf install-meta-bridge`
|
|
192
|
+
again before trusting native Claude Code sessions. The installed statusline and
|
|
193
|
+
MCP entries use stable bin shims, and the marketplace source uses a version-stable
|
|
194
|
+
operator data dir, but the Claude plugin bundle/cache still has to be
|
|
195
|
+
re-materialized for the new package writer. Existing Claude Code sessions should
|
|
196
|
+
be restarted after reinstall.
|
|
197
|
+
|
|
190
198
|
For manual configuration, [`pi/settings.reference.json`](./pi/settings.reference.json)
|
|
191
199
|
shows the pi adapter settings shape, and the external-host examples below show
|
|
192
200
|
plain MCP registrations.
|
package/docs/setup-clean-host.md
CHANGED
|
@@ -4,11 +4,11 @@ End-to-end install of **entwurf** on a host with only `git` available — no
|
|
|
4
4
|
node, no npm package, no pi binary, no dotfiles. The point is to validate the
|
|
5
5
|
public install surface as an outside user would experience it.
|
|
6
6
|
|
|
7
|
-
> **Scope.** This is the entwurf 0.12.
|
|
7
|
+
> **Scope.** This is the entwurf 0.12.5 install recipe. The base package install
|
|
8
8
|
> is **neutral npm**, not `pi install npm:...`. Pi is an optional adapter lane for
|
|
9
|
-
> the ACP provider / control-socket runtime. The 0.12.
|
|
9
|
+
> the ACP provider / control-socket runtime. The 0.12.5 floor fix is explicitly
|
|
10
10
|
> covered here: installed packages under `node_modules` must not run raw `.ts`
|
|
11
|
-
> doctor helpers through Node strip-types.
|
|
11
|
+
> bridge, doctor, or plugin-hook helpers through Node strip-types.
|
|
12
12
|
|
|
13
13
|
`entwurf` is a garden-citizen dispatch substrate and meta-bridge. It does not
|
|
14
14
|
provide, copy, or mediate backend credentials — it lets the official backend CLI
|
|
@@ -31,7 +31,7 @@ ssh cleanhost 'uname -a; whoami; which git node npm pi claude 2>/dev/null'
|
|
|
31
31
|
|---|---|---|
|
|
32
32
|
| Node | **24** recommended; `>=22.6.0` minimum | `engines.node` (Node strip-types / ESM runtime) |
|
|
33
33
|
| npm | bundled with Node 24 | public package install path |
|
|
34
|
-
| entwurf | `@junghanacs/entwurf` | neutral npm package; exposes `entwurf` and `entwurf-
|
|
34
|
+
| entwurf | `@junghanacs/entwurf` | neutral npm package; exposes `entwurf`, `entwurf-bridge`, and `entwurf-statusline` bins |
|
|
35
35
|
| pi binary | **optional**, `@earendil-works/pi-coding-agent >=0.80.3 <0.81` | needed only for the pi adapter / ACP provider / spawn-bg resume lane |
|
|
36
36
|
|
|
37
37
|
## Stage 0 — Node 24 via nvm
|
|
@@ -64,6 +64,7 @@ npm install -g @junghanacs/entwurf
|
|
|
64
64
|
|
|
65
65
|
which entwurf
|
|
66
66
|
which entwurf-bridge
|
|
67
|
+
which entwurf-statusline
|
|
67
68
|
entwurf --help | head -5
|
|
68
69
|
```
|
|
69
70
|
|
|
@@ -77,8 +78,11 @@ Prove the installed MCP server answers `tools/list` from inside `node_modules`.
|
|
|
77
78
|
This is the first `node_modules` strip-types regression fixed in 0.12.0: Node
|
|
78
79
|
refuses `--experimental-strip-types` for `.ts` under `node_modules`, so the
|
|
79
80
|
installed package must boot the prebuilt JS under `mcp/entwurf-bridge/dist/`.
|
|
80
|
-
The
|
|
81
|
-
`doctor-meta-bridge` helper
|
|
81
|
+
The same installed-vs-dev split then closes the other two `.ts`-at-runtime
|
|
82
|
+
surfaces on that same fence: the `doctor-meta-bridge` store-scan helper (0.12.4)
|
|
83
|
+
and the plugin `SessionStart`/`UserPromptSubmit` hook (0.12.5, runs the compiled
|
|
84
|
+
`dist/pi-extensions/meta-bridge-hook.js` when installed). Installed packages run
|
|
85
|
+
tsc-emitted JS on every one of these; dev clones keep the `.ts` source.
|
|
82
86
|
|
|
83
87
|
```bash
|
|
84
88
|
node --input-type=module <<'JS'
|
|
@@ -146,17 +150,29 @@ entwurf doctor-meta-bridge
|
|
|
146
150
|
```
|
|
147
151
|
|
|
148
152
|
On an installed package (`.../node_modules/@junghanacs/entwurf`), the doctor must
|
|
149
|
-
not try to strip-types-run raw `.ts` helpers. In the output, check for
|
|
150
|
-
0.12.
|
|
153
|
+
not try to strip-types-run raw `.ts` helpers or hooks. In the output, check for
|
|
154
|
+
these 0.12.5 floor-regression signals:
|
|
151
155
|
|
|
152
156
|
```text
|
|
157
|
+
ok cached SessionStart hook executes cleanly in an isolated temp agent dir
|
|
153
158
|
ok full store scan: no corrupt records, duplicate nativeSessionId, body/filename drift, or backend↔wakeMode contradiction
|
|
154
159
|
ok check-entwurf-v2-surface: shipped surface source present; exhaustive source-shape gate is a repo/release invariant (not run under node_modules)
|
|
155
160
|
```
|
|
156
161
|
|
|
157
|
-
If
|
|
158
|
-
is still running a pre-0.12.
|
|
159
|
-
package and re-run `entwurf install-meta-bridge && entwurf doctor-meta-bridge`.
|
|
162
|
+
If any of those sections reports `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`,
|
|
163
|
+
the host is still running a pre-0.12.5 package or a broken tarball. Reinstall the
|
|
164
|
+
current package and re-run `entwurf install-meta-bridge && entwurf doctor-meta-bridge`.
|
|
165
|
+
|
|
166
|
+
Upgrade invariant: every global npm/pnpm package upgrade must be followed by
|
|
167
|
+
`entwurf install-meta-bridge` from that same installed binary and then
|
|
168
|
+
`entwurf doctor-meta-bridge`. Installed statusline/MCP entries use stable bin
|
|
169
|
+
shims and the marketplace source lives in a version-stable operator data dir, but
|
|
170
|
+
package managers still do not re-materialize Claude's plugin bundle/cache. If a
|
|
171
|
+
dev checkout's `./run.sh doctor-meta-bridge` expects repo-owned paths while the
|
|
172
|
+
global install intentionally owns the meta-bridge (or the reverse), that is an
|
|
173
|
+
ownership mismatch — run the doctor from the surface that intentionally owns the
|
|
174
|
+
install, or reinstall from the other surface. Restart already-open Claude Code
|
|
175
|
+
sessions after changing the meta-bridge install.
|
|
160
176
|
|
|
161
177
|
A plain external MCP host can call tools but is non-replyable. A garden-native
|
|
162
178
|
meta-session has a garden id, a mailbox, and a trusted sender marker; it can call
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* meta-bridge-hook — the Claude Code native-session entry shell (1.0.0 step 4).
|
|
3
|
+
*
|
|
4
|
+
* THE load-bearing hook. Shipped inside a plugin bundle whose `hooks/hooks.json`
|
|
5
|
+
* wires it to `SessionStart` (and `CwdChanged` / `UserPromptSubmit`). Because a
|
|
6
|
+
* plugin's hooks load at STARTUP (a bare skill's load only on invocation, after
|
|
7
|
+
* SessionStart has passed), this fires on every native Claude Code open and makes
|
|
8
|
+
* that session a garden citizen WITHOUT any pi JSONL of its own:
|
|
9
|
+
*
|
|
10
|
+
* stdin {session_id, transcript_path, cwd} (Claude hook envelope)
|
|
11
|
+
* -> upsertMetaSession(claude-code) idempotent create/attach the record
|
|
12
|
+
* -> gardenId the session's garden address
|
|
13
|
+
* -> arm watchPath <mailbox>/<gardenId>/inbox.signal idle-wake doorbell
|
|
14
|
+
* -> stdout hookSpecificOutput.watchPaths (SessionStart / CwdChanged only)
|
|
15
|
+
*
|
|
16
|
+
* The record is keyed/looked-up by `native_session_id` (THE authority,
|
|
17
|
+
* scanByNativeId); the idle-wake mailbox is keyed by GARDEN id — that is the
|
|
18
|
+
* whole point of the meta-bridge over the raw per-session prototype: one garden
|
|
19
|
+
* address a sender can target, decoupled from the backend's native id grammar.
|
|
20
|
+
*
|
|
21
|
+
* FAILURE POLICY (decided, see NEXT.md step 4): the runtime hook is
|
|
22
|
+
* BEST-EFFORT + LOG. It never screams into the user's terminal and never blocks
|
|
23
|
+
* startup — on any error it appends a line to `<pi-agent-dir>/meta-bridge-hook.log`
|
|
24
|
+
* and emits an empty `{}` (no arm). The fail-LOUD surface is the doctor
|
|
25
|
+
* (step 5), which reads that log + the meta-record dir to catch a silent miss.
|
|
26
|
+
*
|
|
27
|
+
* watchPaths can be emitted only from SessionStart / CwdChanged / FileChanged, so
|
|
28
|
+
* a UserPromptSubmit fire does a degraded RECORD backfill (upsert) but cannot
|
|
29
|
+
* re-arm the idle watch — the record's address is restored, the wake is not.
|
|
30
|
+
*
|
|
31
|
+
* Run (dev clone): `<node> --experimental-strip-types <plugin-root>/meta-bridge-hook.ts`.
|
|
32
|
+
* Run (installed): `<node> <plugin-root>/meta-bridge-hook.js` — the tsc-emitted
|
|
33
|
+
* closure (build-bridge → dist), because Node REFUSES strip-types on a `.ts` below
|
|
34
|
+
* node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING). The installer picks
|
|
35
|
+
* the artifact by layout (0.12.5), mirroring start.sh / store-doctor.
|
|
36
|
+
* Imports `./lib/meta-session.ts` (+ `./lib/session-id.js`); `rewriteRelativeImportExtensions`
|
|
37
|
+
* rewrites that to `./lib/meta-session.js` in the emitted closure. The installer
|
|
38
|
+
* copies the lib dir alongside this file so `${CLAUDE_PLUGIN_ROOT}` self-locates it.
|
|
39
|
+
*/
|
|
40
|
+
import * as fs from "node:fs";
|
|
41
|
+
import * as path from "node:path";
|
|
42
|
+
import { defaultMetaMailboxDir, defaultMetaSessionsDir, upsertMetaSession, writeMetaReceiverMarker, writeMetaSenderMarker, } from "./lib/meta-session.js";
|
|
43
|
+
function logLine(level, message) {
|
|
44
|
+
try {
|
|
45
|
+
// dirname(meta-sessions) == the pi agent dir — no extra resolver export needed.
|
|
46
|
+
const file = path.join(path.dirname(defaultMetaSessionsDir()), "meta-bridge-hook.log");
|
|
47
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
48
|
+
fs.appendFileSync(file, `${new Date().toISOString()} ${level} ${message}\n`);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* logging is best-effort; a broken log must not break the session */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Emit a hook response on stdout and exit 0. `{}` means "did nothing, do not block startup". */
|
|
55
|
+
function emit(payload) {
|
|
56
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Map the hook event to a receiver-marker arm provenance. ONLY the genuinely
|
|
61
|
+
* arm-capable events map; any other (a future/unknown hook event) returns null so we
|
|
62
|
+
* never mint an "active receiver" presence we cannot back — fail-closed, not an
|
|
63
|
+
* optimistic session-start. UserPromptSubmit never reaches here (it early-returns
|
|
64
|
+
* before the arm block).
|
|
65
|
+
*/
|
|
66
|
+
function armProvenanceFor(eventName) {
|
|
67
|
+
if (eventName === "SessionStart")
|
|
68
|
+
return "session-start";
|
|
69
|
+
if (eventName === "CwdChanged")
|
|
70
|
+
return "cwd-changed";
|
|
71
|
+
if (eventName === "FileChanged")
|
|
72
|
+
return "file-changed";
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function main() {
|
|
76
|
+
let raw;
|
|
77
|
+
try {
|
|
78
|
+
raw = fs.readFileSync(0, "utf8"); // fd 0 = stdin (the Claude hook envelope)
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
logLine("ERROR", `stdin read failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
82
|
+
emit({});
|
|
83
|
+
}
|
|
84
|
+
let env;
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(raw);
|
|
87
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
88
|
+
throw new Error("envelope is not an object");
|
|
89
|
+
env = parsed;
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
logLine("ERROR", `envelope parse failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
93
|
+
emit({});
|
|
94
|
+
}
|
|
95
|
+
const sessionId = typeof env.session_id === "string" ? env.session_id : "";
|
|
96
|
+
const transcriptPath = typeof env.transcript_path === "string" ? env.transcript_path : "";
|
|
97
|
+
// cwd: prefer the envelope's, fall back to the process cwd (the hook runs in the session's cwd).
|
|
98
|
+
const cwd = typeof env.cwd === "string" && env.cwd.length > 0 ? env.cwd : process.cwd();
|
|
99
|
+
const modelEnvelope = env.model;
|
|
100
|
+
const model = typeof modelEnvelope === "object" &&
|
|
101
|
+
modelEnvelope !== null &&
|
|
102
|
+
typeof modelEnvelope.id === "string"
|
|
103
|
+
? modelEnvelope.id
|
|
104
|
+
: typeof env.model_id === "string"
|
|
105
|
+
? env.model_id
|
|
106
|
+
: undefined;
|
|
107
|
+
const eventName = typeof env.hook_event_name === "string" ? env.hook_event_name : "SessionStart";
|
|
108
|
+
if (!sessionId || !transcriptPath) {
|
|
109
|
+
// A degraded envelope: cannot mint an honest reference record. Log + no-op
|
|
110
|
+
// rather than write a half-record or guess a transcript path. LEVEL depends
|
|
111
|
+
// on the event: a degraded SessionStart / CwdChanged means the session FAILED
|
|
112
|
+
// to become (or refresh) a garden citizen — that is the silent registration
|
|
113
|
+
// miss the doctor must catch (blocker #2), so ERROR. UserPromptSubmit only
|
|
114
|
+
// ever does a best-effort record backfill, so a degraded one is just WARN.
|
|
115
|
+
const degradedLevel = eventName === "UserPromptSubmit" ? "WARN" : "ERROR";
|
|
116
|
+
logLine(degradedLevel, `degraded envelope (event=${eventName}, session_id=${sessionId ? "set" : "MISSING"}, transcript_path=${transcriptPath ? "set" : "MISSING"})`);
|
|
117
|
+
emit({});
|
|
118
|
+
}
|
|
119
|
+
let gardenId;
|
|
120
|
+
try {
|
|
121
|
+
const result = upsertMetaSession({
|
|
122
|
+
input: { backend: "claude-code", nativeSessionId: sessionId, transcriptPath, cwd, model },
|
|
123
|
+
onSkip: (filename, e) => logLine("WARN", `scan skipped ${filename}: ${e.message}`),
|
|
124
|
+
});
|
|
125
|
+
gardenId = result.record.gardenId;
|
|
126
|
+
logLine("INFO", `${result.action} record ${path.basename(result.path)} (event=${eventName}, native=${sessionId})`);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
// Best-effort: a broken record store must surface via the doctor, not by
|
|
130
|
+
// breaking the user's session open. Log and continue with no arm. This is
|
|
131
|
+
// the silent-registration-miss (blocker #2): the session opened fine but is
|
|
132
|
+
// NOT a garden citizen — the doctor catches it via this ERROR line.
|
|
133
|
+
logLine("ERROR", `upsert failed (event=${eventName}, native=${sessionId}): ${err instanceof Error ? err.message : String(err)}`);
|
|
134
|
+
emit({});
|
|
135
|
+
}
|
|
136
|
+
// Sender marker, keyed by the shared Claude Code parent pid: the user-scope
|
|
137
|
+
// MCP child (same parent) reads it at entwurf_v2 send time to promote this
|
|
138
|
+
// session from anonymous external-mcp to a REPLYABLE meta-session sender —
|
|
139
|
+
// process ancestry, not cwd inference (same repo + multiple sessions would be
|
|
140
|
+
// ambiguous). Best-effort: a failed marker only costs reply-addressability
|
|
141
|
+
// (WARN), it does not break the session or the receiver path.
|
|
142
|
+
//
|
|
143
|
+
// SE-1/SE-2 (dual-owner fix): write ONLY for the direct parent (process.ppid =
|
|
144
|
+
// the Claude CLI that ran this hook, verified the native tree is direct — the
|
|
145
|
+
// plugin host is not in between). The old code ALSO wrote a marker for the
|
|
146
|
+
// grandparent. That grandparent is the login shell (e.g. bash under ghostty/i3),
|
|
147
|
+
// which OUTLIVES the Claude session: when Claude exits, the grandparent marker's
|
|
148
|
+
// ownerStartKey still matches a live pid, so it passes readMetaSenderMarker's
|
|
149
|
+
// reuse guard and the dead session keeps looking like a live, replyable receiver
|
|
150
|
+
// — a false-positive "active receiver" leak. The owner must be the watchPaths
|
|
151
|
+
// subscriber (the Claude CLI), nothing higher. If a stray topology means the MCP
|
|
152
|
+
// child's shared ancestor is not process.ppid, that resolves to "no marker"
|
|
153
|
+
// (fail-closed, honest) rather than a wrong-but-live grandparent identity.
|
|
154
|
+
const ownerPid = process.ppid;
|
|
155
|
+
if (typeof ownerPid === "number" && ownerPid > 0) {
|
|
156
|
+
try {
|
|
157
|
+
writeMetaSenderMarker({ backend: "claude-code", gardenId, nativeSessionId: sessionId, cwd, ownerPid });
|
|
158
|
+
logLine("INFO", `sender marker ${ownerPid} -> ${gardenId} (event=${eventName})`);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
logLine("WARN", `sender marker write failed (event=${eventName}, pid=${ownerPid}, garden=${gardenId}): ${err instanceof Error ? err.message : String(err)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// watchPaths is emittable only from SessionStart / CwdChanged / FileChanged.
|
|
165
|
+
// UserPromptSubmit reaches here only to backfill the record above; it must NOT
|
|
166
|
+
// claim to arm a watch it cannot (decided).
|
|
167
|
+
if (eventName === "UserPromptSubmit") {
|
|
168
|
+
emit({});
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const mailbox = path.join(defaultMetaMailboxDir(), gardenId);
|
|
172
|
+
fs.mkdirSync(mailbox, { recursive: true });
|
|
173
|
+
const signal = path.join(mailbox, "inbox.signal");
|
|
174
|
+
if (!fs.existsSync(signal))
|
|
175
|
+
fs.writeFileSync(signal, "", { mode: 0o600 });
|
|
176
|
+
logLine("INFO", `armed watch ${signal}`);
|
|
177
|
+
// Receiver presence marker (SE-2): written on the arm-capable hook path that
|
|
178
|
+
// emits watchPaths, keyed by garden id with the watch owner pid (= the Claude
|
|
179
|
+
// CLI, process.ppid — same single owner as the sender marker, never the
|
|
180
|
+
// grandparent). It records that a LIVE owner reached the watch-arm emit; it is
|
|
181
|
+
// not proof the host ack'd the watch registration. This is what lets a sender
|
|
182
|
+
// tell a live receiver from a terminated one whose record still lingers.
|
|
183
|
+
// Best-effort: a failed/skipped marker only costs deliverability detection
|
|
184
|
+
// (WARN), it does not break the arm. An unknown event maps to null provenance →
|
|
185
|
+
// no marker (fail-closed: never claim an active receiver we cannot back).
|
|
186
|
+
const ownerPid = process.ppid;
|
|
187
|
+
const armProvenance = armProvenanceFor(eventName);
|
|
188
|
+
if (armProvenance === null) {
|
|
189
|
+
logLine("WARN", `receiver marker skipped — non-arm event ${eventName} (garden=${gardenId})`);
|
|
190
|
+
}
|
|
191
|
+
else if (typeof ownerPid === "number" && ownerPid > 0) {
|
|
192
|
+
try {
|
|
193
|
+
writeMetaReceiverMarker({
|
|
194
|
+
gardenId,
|
|
195
|
+
backend: "claude-code",
|
|
196
|
+
nativeSessionId: sessionId,
|
|
197
|
+
ownerPid,
|
|
198
|
+
armProvenance,
|
|
199
|
+
});
|
|
200
|
+
logLine("INFO", `receiver marker ${gardenId} owner=${ownerPid} arm=${eventName}`);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
logLine("WARN", `receiver marker write failed (event=${eventName}, garden=${gardenId}): ${err instanceof Error ? err.message : String(err)}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
emit({
|
|
207
|
+
hookSpecificOutput: {
|
|
208
|
+
hookEventName: eventName,
|
|
209
|
+
watchPaths: [signal],
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
logLine("ERROR", `arm failed (event=${eventName}, garden=${gardenId}): ${err instanceof Error ? err.message : String(err)}`);
|
|
215
|
+
emit({}); // record landed; only the arm failed — the doctor will flag the missing watch.
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
main();
|
|
@@ -46,5 +46,22 @@
|
|
|
46
46
|
// emits here, so it adds ONE leaf JS (dist/scripts/meta-bridge-store-doctor.js)
|
|
47
47
|
// and ZERO new deps. It is NOT in index.ts's boot closure, so
|
|
48
48
|
// check-entwurf-bridge-pi-free (which walks the index eager closure) is unaffected.
|
|
49
|
-
|
|
49
|
+
//
|
|
50
|
+
// WHY meta-bridge-hook rides THIS build (0.12.5): the SessionStart/UserPromptSubmit
|
|
51
|
+
// plugin hook ran as raw `.ts` (`node ${CLAUDE_PLUGIN_ROOT}/meta-bridge-hook.ts`).
|
|
52
|
+
// Under an installed marketplace source below node_modules that is
|
|
53
|
+
// ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING — the SAME strip-types fence
|
|
54
|
+
// start.sh (0.12.1) and the store-doctor (0.12.4) already cross by shipping JS.
|
|
55
|
+
// The hook was the last `.ts`-at-runtime surface; emitting it here closes that
|
|
56
|
+
// class for good (installer copies the dist JS when installed, keeps `.ts` for a
|
|
57
|
+
// dev clone). Its only repo import is meta-session.ts, ALREADY emitted for the
|
|
58
|
+
// store-doctor, so this adds ONE leaf JS (dist/pi-extensions/meta-bridge-hook.js)
|
|
59
|
+
// and ZERO new deps. It is a plugin hook, never in index.ts's boot closure, so
|
|
60
|
+
// check-entwurf-bridge-pi-free is unaffected.
|
|
61
|
+
"include": [
|
|
62
|
+
"./src/index.ts",
|
|
63
|
+
"../../protocol.js",
|
|
64
|
+
"../../scripts/meta-bridge-store-doctor.ts",
|
|
65
|
+
"../../pi-extensions/meta-bridge-hook.ts"
|
|
66
|
+
]
|
|
50
67
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junghanacs/entwurf",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.5",
|
|
4
4
|
"description": "Garden-citizen dispatch substrate and meta-bridge for Claude Code, Codex, Antigravity, and pi harnesses.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -132,7 +132,8 @@
|
|
|
132
132
|
},
|
|
133
133
|
"bin": {
|
|
134
134
|
"entwurf": "./run.sh",
|
|
135
|
-
"entwurf-bridge": "./mcp/entwurf-bridge/start.sh"
|
|
135
|
+
"entwurf-bridge": "./mcp/entwurf-bridge/start.sh",
|
|
136
|
+
"entwurf-statusline": "./scripts/meta-bridge-statusline.sh"
|
|
136
137
|
},
|
|
137
138
|
"peerDependenciesMeta": {
|
|
138
139
|
"@earendil-works/pi-ai": {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "__NODE_BIN__ ${CLAUDE_PLUGIN_ROOT}/
|
|
9
|
+
"command": "__NODE_BIN__ ${CLAUDE_PLUGIN_ROOT}/__HOOK_ENTRY__"
|
|
10
10
|
}
|
|
11
11
|
]
|
|
12
12
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"hooks": [
|
|
18
18
|
{
|
|
19
19
|
"type": "command",
|
|
20
|
-
"command": "__NODE_BIN__ ${CLAUDE_PLUGIN_ROOT}/
|
|
20
|
+
"command": "__NODE_BIN__ ${CLAUDE_PLUGIN_ROOT}/__HOOK_ENTRY__"
|
|
21
21
|
}
|
|
22
22
|
]
|
|
23
23
|
}
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"hooks": [
|
|
29
29
|
{
|
|
30
30
|
"type": "command",
|
|
31
|
-
"command": "__NODE_BIN__ ${CLAUDE_PLUGIN_ROOT}/
|
|
31
|
+
"command": "__NODE_BIN__ ${CLAUDE_PLUGIN_ROOT}/__HOOK_ENTRY__"
|
|
32
32
|
}
|
|
33
33
|
]
|
|
34
34
|
}
|
|
@@ -28,9 +28,14 @@
|
|
|
28
28
|
* a UserPromptSubmit fire does a degraded RECORD backfill (upsert) but cannot
|
|
29
29
|
* re-arm the idle watch — the record's address is restored, the wake is not.
|
|
30
30
|
*
|
|
31
|
-
* Run: `<node> --experimental-strip-types <plugin-root>/meta-bridge-hook.ts`.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* Run (dev clone): `<node> --experimental-strip-types <plugin-root>/meta-bridge-hook.ts`.
|
|
32
|
+
* Run (installed): `<node> <plugin-root>/meta-bridge-hook.js` — the tsc-emitted
|
|
33
|
+
* closure (build-bridge → dist), because Node REFUSES strip-types on a `.ts` below
|
|
34
|
+
* node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING). The installer picks
|
|
35
|
+
* the artifact by layout (0.12.5), mirroring start.sh / store-doctor.
|
|
36
|
+
* Imports `./lib/meta-session.ts` (+ `./lib/session-id.js`); `rewriteRelativeImportExtensions`
|
|
37
|
+
* rewrites that to `./lib/meta-session.js` in the emitted closure. The installer
|
|
38
|
+
* copies the lib dir alongside this file so `${CLAUDE_PLUGIN_ROOT}` self-locates it.
|
|
34
39
|
*/
|
|
35
40
|
|
|
36
41
|
import * as fs from "node:fs";
|
package/run.sh
CHANGED
|
@@ -1735,6 +1735,12 @@ check_pack() {
|
|
|
1735
1735
|
# doctor.sh runs this prebuilt JS when installed under node_modules (strip-types
|
|
1736
1736
|
# refuses the .ts there), same boundary start.sh crosses. build-bridge emits it.
|
|
1737
1737
|
"mcp/entwurf-bridge/dist/scripts/meta-bridge-store-doctor.js"
|
|
1738
|
+
# 0.12.5 — the node_modules-safe plugin hook + its lib. install-meta-bridge copies
|
|
1739
|
+
# these compiled JS into the assembled plugin when installed (raw .ts can't
|
|
1740
|
+
# strip-types under node_modules). meta-session.js is shared with the store-doctor
|
|
1741
|
+
# above; listed here too so the hook axis fails loud if the emit graph drops it.
|
|
1742
|
+
"mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js"
|
|
1743
|
+
"mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
|
|
1738
1744
|
"scripts/postinstall-chmod.cjs"
|
|
1739
1745
|
"pi/entwurf-capabilities.json"
|
|
1740
1746
|
"pi/entwurf-targets.json"
|
|
@@ -1887,6 +1893,10 @@ check_pack_install() {
|
|
|
1887
1893
|
# 0.12.4 — prebuilt node_modules-safe store-scan artifact for the doctor
|
|
1888
1894
|
# (see check-pack). The installed-scan smoke below runs exactly this file.
|
|
1889
1895
|
"mcp/entwurf-bridge/dist/scripts/meta-bridge-store-doctor.js"
|
|
1896
|
+
# 0.12.5 — node_modules-safe plugin hook + lib (see check-pack). The installed
|
|
1897
|
+
# hook regression below runs exactly this compiled JS from under node_modules.
|
|
1898
|
+
"mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js"
|
|
1899
|
+
"mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
|
|
1890
1900
|
"scripts/postinstall-chmod.cjs"
|
|
1891
1901
|
"pi/entwurf-capabilities.json"
|
|
1892
1902
|
"pi/entwurf-targets.json"
|
|
@@ -2043,8 +2053,8 @@ check_pack_install() {
|
|
|
2043
2053
|
fail "[check-pack-install] npm-managed layout missing executable run.sh (postinstall-chmod did not run?) at $npm_pkg"
|
|
2044
2054
|
return 1
|
|
2045
2055
|
fi
|
|
2046
|
-
if [ ! -x "$npmroot/node_modules/.bin/entwurf" ] || [ ! -x "$npmroot/node_modules/.bin/entwurf-bridge" ]; then
|
|
2047
|
-
fail "[check-pack-install] npm-managed neutral install missing package bins (entwurf / entwurf-bridge)"
|
|
2056
|
+
if [ ! -x "$npmroot/node_modules/.bin/entwurf" ] || [ ! -x "$npmroot/node_modules/.bin/entwurf-bridge" ] || [ ! -x "$npmroot/node_modules/.bin/entwurf-statusline" ]; then
|
|
2057
|
+
fail "[check-pack-install] npm-managed neutral install missing package bins (entwurf / entwurf-bridge / entwurf-statusline)"
|
|
2048
2058
|
ls -l "$npmroot/node_modules/.bin" 2>/dev/null | sed 's/^/ /' >&2 || true
|
|
2049
2059
|
return 1
|
|
2050
2060
|
fi
|
|
@@ -2065,6 +2075,121 @@ check_pack_install() {
|
|
|
2065
2075
|
fi
|
|
2066
2076
|
echo "[check-pack-install] npm-managed install regression pass (hoisted-dep run.sh install wrote settings)"
|
|
2067
2077
|
|
|
2078
|
+
# Installed meta-bridge ownership regression (0.12.5): package upgrades must not
|
|
2079
|
+
# bake versioned pnpm-store paths into Claude settings. MCP already uses the
|
|
2080
|
+
# stable `entwurf-bridge` bin; statusLine must now use `entwurf-statusline`, and
|
|
2081
|
+
# the plugin marketplace source must be the version-stable operator data dir
|
|
2082
|
+
# rather than <node_modules>/pi/meta-bridge/.assembled. Use a fake claude CLI so
|
|
2083
|
+
# this stays deterministic/offline while running the REAL installed
|
|
2084
|
+
# install-meta-bridge path and meta-bridge-state apply.
|
|
2085
|
+
local fake_claude_dir="$npm_tmp/fake-claude-bin" fake_claude_log="$npm_tmp/fake-claude.log"
|
|
2086
|
+
mkdir -p "$fake_claude_dir"
|
|
2087
|
+
cat > "$fake_claude_dir/claude" <<'SH'
|
|
2088
|
+
#!/usr/bin/env bash
|
|
2089
|
+
printf '%s\n' "$*" >> "${FAKE_CLAUDE_LOG:?}"
|
|
2090
|
+
case "$1${2:+ $2}" in
|
|
2091
|
+
"--version") echo "2.1.197 (Claude Code)" ;;
|
|
2092
|
+
"plugin validate") : ;;
|
|
2093
|
+
"plugin uninstall") : ;;
|
|
2094
|
+
"plugin marketplace") : ;;
|
|
2095
|
+
"plugin install") : ;;
|
|
2096
|
+
"plugin list") printf '%s\n' "entwurf-meta-receive@meta-bridge-local" " Status: enabled" ;;
|
|
2097
|
+
"mcp remove") : ;;
|
|
2098
|
+
"mcp add") : ;;
|
|
2099
|
+
"mcp get") printf '%s\n' "Scope: User config" "Status: ✔ Connected" ;;
|
|
2100
|
+
*) : ;;
|
|
2101
|
+
esac
|
|
2102
|
+
exit 0
|
|
2103
|
+
SH
|
|
2104
|
+
chmod +x "$fake_claude_dir/claude"
|
|
2105
|
+
local mb_home="$npm_tmp/meta-home" mb_cfg="$npm_tmp/meta-claude" mb_log
|
|
2106
|
+
mkdir -p "$mb_home" "$mb_cfg"
|
|
2107
|
+
mb_log=$(HOME="$mb_home" XDG_DATA_HOME="$mb_home/.local/share" CLAUDE_CONFIG_DIR="$mb_cfg" FAKE_CLAUDE_LOG="$fake_claude_log" PATH="$fake_claude_dir:$npmroot/node_modules/.bin:$PATH" "$npm_pkg/run.sh" install-meta-bridge 2>&1) || {
|
|
2108
|
+
fail "[check-pack-install] installed install-meta-bridge failed under fake claude:"
|
|
2109
|
+
echo "$mb_log" | tail -20 | sed 's/^/ /' >&2
|
|
2110
|
+
return 1
|
|
2111
|
+
}
|
|
2112
|
+
local stable_asm="$mb_home/.local/share/entwurf/meta-bridge/.assembled"
|
|
2113
|
+
local installed_hook="$stable_asm/entwurf-meta-receive/meta-bridge-hook.js"
|
|
2114
|
+
if [ ! -f "$installed_hook" ]; then
|
|
2115
|
+
fail "[check-pack-install] installed meta-bridge did not assemble the compiled hook JS into the stable operator data dir: $installed_hook"
|
|
2116
|
+
return 1
|
|
2117
|
+
fi
|
|
2118
|
+
if [ -f "$stable_asm/entwurf-meta-receive/meta-bridge-hook.ts" ]; then
|
|
2119
|
+
fail "[check-pack-install] installed meta-bridge shipped a raw .ts hook — installed packages must run compiled JS (strip-types is refused under node_modules)"
|
|
2120
|
+
return 1
|
|
2121
|
+
fi
|
|
2122
|
+
# The artifact existing is not enough (review BLOCKER 1): Claude runs the hooks.json
|
|
2123
|
+
# COMMAND, so assert the baked command actually targets the compiled .js with both
|
|
2124
|
+
# placeholders resolved. A bake that mis-targeted .ts (or left a placeholder) would
|
|
2125
|
+
# still pass the direct-JS smoke below, then fail live.
|
|
2126
|
+
local installed_hooks_json="$stable_asm/entwurf-meta-receive/hooks/hooks.json"
|
|
2127
|
+
if ! grep -q 'meta-bridge-hook\.js' "$installed_hooks_json"; then
|
|
2128
|
+
fail "[check-pack-install] installed hooks.json does not point at the compiled meta-bridge-hook.js: $installed_hooks_json"
|
|
2129
|
+
return 1
|
|
2130
|
+
fi
|
|
2131
|
+
if grep -qE 'meta-bridge-hook\.ts|__HOOK_ENTRY__|__NODE_BIN__' "$installed_hooks_json"; then
|
|
2132
|
+
fail "[check-pack-install] installed hooks.json references a raw .ts entry or an unbaked placeholder (__HOOK_ENTRY__/__NODE_BIN__): $installed_hooks_json"
|
|
2133
|
+
return 1
|
|
2134
|
+
fi
|
|
2135
|
+
if [ -e "$npm_pkg/pi/meta-bridge/.assembled/entwurf-meta-receive" ]; then
|
|
2136
|
+
fail "[check-pack-install] installed meta-bridge still assembled inside the versioned package store: $npm_pkg/pi/meta-bridge/.assembled"
|
|
2137
|
+
return 1
|
|
2138
|
+
fi
|
|
2139
|
+
# 0.12.5 strip-types-fence regression (GPT safety pin). The compiled hook must run
|
|
2140
|
+
# FROM UNDER node_modules with plain node — the exact fence that broke oracle's
|
|
2141
|
+
# 0.12.4 raw-.ts hook (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING). L2 relocates
|
|
2142
|
+
# the runtime marketplace source to XDG, but this hardens the ARTIFACT itself so
|
|
2143
|
+
# it is safe even if a future cache/marketplace path lands under node_modules.
|
|
2144
|
+
local nm_probe="$npmroot/node_modules/@junghanacs/entwurf/pi/meta-bridge/.assembled-packprobe/entwurf-meta-receive"
|
|
2145
|
+
mkdir -p "$nm_probe/lib"
|
|
2146
|
+
cp "$installed_hook" "$nm_probe/meta-bridge-hook.js"
|
|
2147
|
+
cp "$stable_asm/entwurf-meta-receive/lib/meta-session.js" "$nm_probe/lib/meta-session.js"
|
|
2148
|
+
cp "$stable_asm/entwurf-meta-receive/lib/session-id.js" "$nm_probe/lib/session-id.js"
|
|
2149
|
+
cp "$stable_asm/entwurf-meta-receive/entwurf-capabilities.json" "$nm_probe/entwurf-capabilities.json"
|
|
2150
|
+
local probe_env='{"session_id":"pack-probe","transcript_path":"/tmp/x.jsonl","cwd":"/tmp","hook_event_name":"SessionStart","model":{"id":"probe"}}'
|
|
2151
|
+
local probe_out
|
|
2152
|
+
if probe_out="$(printf '%s' "$probe_env" | env PI_CODING_AGENT_DIR="$(mktemp -d)" CLAUDE_PLUGIN_ROOT="$nm_probe" node "$nm_probe/meta-bridge-hook.js" 2>&1)" && printf '%s' "$probe_out" | grep -q hookSpecificOutput; then
|
|
2153
|
+
: # compiled hook crosses the node_modules strip-types fence safely
|
|
2154
|
+
else
|
|
2155
|
+
fail "[check-pack-install] compiled hook failed to run under node_modules with plain node: $(printf '%s' "$probe_out" | tr '\n' ' ' | cut -c1-200)"
|
|
2156
|
+
rm -rf "$nm_probe"; return 1
|
|
2157
|
+
fi
|
|
2158
|
+
# FAIL-reproduction (review BLOCKER 2): a raw .ts at the SAME node_modules location
|
|
2159
|
+
# must be refused SPECIFICALLY by the strip-types fence — not by some unrelated
|
|
2160
|
+
# failure. A missing lib would also exit nonzero and hollow out the proof, so
|
|
2161
|
+
# capture stderr and require the exact ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING.
|
|
2162
|
+
cp "$npm_pkg/pi-extensions/meta-bridge-hook.ts" "$nm_probe/meta-bridge-hook.ts"
|
|
2163
|
+
local ts_out ts_rc
|
|
2164
|
+
ts_out="$(printf '%s' "$probe_env" | env PI_CODING_AGENT_DIR="$(mktemp -d)" CLAUDE_PLUGIN_ROOT="$nm_probe" node "$nm_probe/meta-bridge-hook.ts" 2>&1)" && ts_rc=0 || ts_rc=$?
|
|
2165
|
+
if [ "${ts_rc:-0}" -eq 0 ]; then
|
|
2166
|
+
fail "[check-pack-install] raw .ts hook UNEXPECTEDLY ran under node_modules — strip-types fence moved; the compiled-hook rationale must be revisited"
|
|
2167
|
+
rm -rf "$nm_probe"; return 1
|
|
2168
|
+
fi
|
|
2169
|
+
if ! printf '%s' "$ts_out" | grep -q 'ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING'; then
|
|
2170
|
+
fail "[check-pack-install] raw .ts hook failed under node_modules but NOT via the strip-types fence (expected ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING); got: $(printf '%s' "$ts_out" | tr '\n' ' ' | cut -c1-200) — the regression no longer proves the compiled-hook rationale"
|
|
2171
|
+
rm -rf "$nm_probe"; return 1
|
|
2172
|
+
fi
|
|
2173
|
+
rm -rf "$nm_probe"
|
|
2174
|
+
echo "[check-pack-install] installed hook is node_modules-safe compiled JS (hooks.json points at .js; runs under node_modules with plain node; raw .ts refused there by the strip-types fence)"
|
|
2175
|
+
python3 - "$mb_cfg/settings.json" "$mb_home/.claude.json" "$stable_asm" <<'PY'
|
|
2176
|
+
import json, sys
|
|
2177
|
+
settings = json.load(open(sys.argv[1]))
|
|
2178
|
+
root = json.load(open(sys.argv[2]))
|
|
2179
|
+
stable_asm = sys.argv[3]
|
|
2180
|
+
market = settings.get("extraKnownMarketplaces", {}).get("meta-bridge-local", {})
|
|
2181
|
+
assert market == {"source": {"source": "directory", "path": stable_asm}}, market
|
|
2182
|
+
assert settings.get("statusLine") == {"type": "command", "command": "entwurf-statusline"}, settings.get("statusLine")
|
|
2183
|
+
mcp = root.get("mcpServers", {}).get("entwurf-bridge", {})
|
|
2184
|
+
assert mcp.get("command") == "entwurf-bridge" and mcp.get("args") == [], mcp
|
|
2185
|
+
PY
|
|
2186
|
+
if ! grep -q "$stable_asm" "$fake_claude_log"; then
|
|
2187
|
+
fail "[check-pack-install] fake claude did not receive the stable marketplace path during install-meta-bridge"
|
|
2188
|
+
sed 's/^/ /' "$fake_claude_log" >&2 || true
|
|
2189
|
+
return 1
|
|
2190
|
+
fi
|
|
2191
|
+
echo "[check-pack-install] installed meta-bridge ownership pass (stable statusline bin + stable marketplace dir + stable MCP bin)"
|
|
2192
|
+
|
|
2068
2193
|
# 0.12.1 C — installed bridge BOOT regression. This is the test whose absence
|
|
2069
2194
|
# let the 0.12.0 install bug ship: the README's bridge launcher was
|
|
2070
2195
|
# `node --experimental-strip-types src/index.ts`, which Node REFUSES under
|
|
Binary file
|
|
@@ -17,6 +17,19 @@ MKT_NAME="meta-bridge-local"
|
|
|
17
17
|
PLUGIN="entwurf-meta-receive"
|
|
18
18
|
CLAUDE_CFG="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
19
19
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
20
|
+
# LIB_EXT tracks the hook artifact mode (0.12.5): installed packages ship the
|
|
21
|
+
# tsc-emitted `.js` closure (node_modules-safe), dev clones run the `.ts` source.
|
|
22
|
+
# The writer-version parity below hashes meta-session.<LIB_EXT> so an installed
|
|
23
|
+
# `.js` bundle is compared against the SAME-pipeline dist `.js`, never against the
|
|
24
|
+
# `.ts` source (which would hash-mismatch and false-STALE).
|
|
25
|
+
case "$REPO" in
|
|
26
|
+
*/node_modules/@junghanacs/entwurf)
|
|
27
|
+
ASM="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/meta-bridge/.assembled"
|
|
28
|
+
LIB_EXT="js" ;;
|
|
29
|
+
*)
|
|
30
|
+
ASM="$REPO/pi/meta-bridge/.assembled"
|
|
31
|
+
LIB_EXT="ts" ;;
|
|
32
|
+
esac
|
|
20
33
|
# shellcheck source=scripts/meta-bridge-hook-log.sh
|
|
21
34
|
source "$REPO/scripts/meta-bridge-hook-log.sh"
|
|
22
35
|
|
|
@@ -65,7 +78,7 @@ if command -v python3 >/dev/null; then
|
|
|
65
78
|
# which key drifted — silent instead of fail-loud. As an `if` condition the
|
|
66
79
|
# substitution's nonzero status is consumed (set -e suspended), so we always
|
|
67
80
|
# reach the detail. stderr (drift detail) is captured via `2>&1 >/dev/null`.
|
|
68
|
-
if CHECK_ERR="$(python3 "$REPO/scripts/meta-bridge-state.py" check --repo "$REPO" --asm "$
|
|
81
|
+
if CHECK_ERR="$(python3 "$REPO/scripts/meta-bridge-state.py" check --repo "$REPO" --asm "$ASM" 2>&1 >/dev/null)"; then
|
|
69
82
|
ok "state file present and managed settings/MCP keyset survives intact"
|
|
70
83
|
else
|
|
71
84
|
bad "managed settings/MCP keyset missing or overwritten — another consumer may have clobbered a pi-owned key. Re-run ./run.sh install-meta-bridge. Drift detail:"
|
|
@@ -93,8 +106,23 @@ if [ -n "$CACHE_HOOKS" ]; then
|
|
|
93
106
|
# line 158) so BAKED="" routes to that bad(...): an installed hooks.json exists but
|
|
94
107
|
# the doctor cannot read its load-bearing hook command, so the store-churn guard is
|
|
95
108
|
# blind — a verify-impossible state must fail loud, not pass as a soft warn (A3b).
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
# Match either hook artifact (0.12.5): installed ships meta-bridge-hook.js, dev
|
|
110
|
+
# clones meta-bridge-hook.ts. Capture the whole command so we run the SAME entry
|
|
111
|
+
# file the live hook runs (never a hardcoded extension that drifts from install).
|
|
112
|
+
HOOK_CMD="$({ grep -oE '"command": "[^ ]+ \$\{CLAUDE_PLUGIN_ROOT\}/meta-bridge-hook\.(ts|js)"' "$CACHE_HOOKS" || true; } | head -1)"
|
|
113
|
+
BAKED="$(printf '%s' "$HOOK_CMD" | sed -E 's/.*"command": "([^ ]+) .*/\1/')"
|
|
114
|
+
HOOK_FILE="$(printf '%s' "$HOOK_CMD" | sed -E 's#.*/([^/"]+)"$#\1#')"
|
|
115
|
+
if [ -n "$BAKED" ] && [ -x "$BAKED" ]; then
|
|
116
|
+
ok "baked node exists + executable: $BAKED"
|
|
117
|
+
CACHE_ROOT="$(cd "$(dirname "$CACHE_HOOKS")/.." && pwd)"
|
|
118
|
+
TMP_AGENT="$(mktemp -d 2>/dev/null || mktemp -d -t entwurf-doctor-hook)"
|
|
119
|
+
HOOK_ENV='{"session_id":"doctor-synthetic-native","transcript_path":"/tmp/entwurf-doctor-synthetic-transcript.jsonl","cwd":"/tmp","hook_event_name":"SessionStart","model":{"id":"doctor-model"}}'
|
|
120
|
+
if HOOK_OUT="$(printf '%s' "$HOOK_ENV" | env PI_CODING_AGENT_DIR="$TMP_AGENT" CLAUDE_PLUGIN_ROOT="$CACHE_ROOT" "$BAKED" "$CACHE_ROOT/$HOOK_FILE" 2>&1)" && printf '%s' "$HOOK_OUT" | grep -q 'hookSpecificOutput'; then
|
|
121
|
+
ok "cached SessionStart hook executes cleanly in an isolated temp agent dir"
|
|
122
|
+
else
|
|
123
|
+
bad "cached SessionStart hook failed to execute — stale plugin cache / unsupported TS syntax / broken bundle. Re-run install-meta-bridge from the intended surface. Detail: $(printf '%s' "$HOOK_OUT" | tr '\n' ' ' | cut -c1-300)"
|
|
124
|
+
fi
|
|
125
|
+
rm -rf "$TMP_AGENT" 2>/dev/null || true
|
|
98
126
|
elif [ -n "$BAKED" ]; then bad "baked node path is DEAD (nix GC / version bump?): $BAKED — re-run ./run.sh install-meta-bridge"
|
|
99
127
|
else bad "could not parse baked node path from $CACHE_HOOKS — hook command format drift; doctor cannot verify the NixOS store-churn guard. Re-run ./run.sh install-meta-bridge or update the doctor parser."; fi
|
|
100
128
|
else
|
|
@@ -114,9 +142,15 @@ except Exception:
|
|
|
114
142
|
print('')
|
|
115
143
|
PY
|
|
116
144
|
)"
|
|
117
|
-
EXPECTED_STATUSLINE="$REPO/scripts/meta-bridge-statusline.
|
|
118
|
-
if [ "$STATUSLINE_CMD" = "$EXPECTED_STATUSLINE" ]; then ok "statusLine.command
|
|
119
|
-
if [
|
|
145
|
+
EXPECTED_STATUSLINE="$(python3 "$REPO/scripts/meta-bridge-state.py" desired-statusline --repo "$REPO" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("command", ""))')"
|
|
146
|
+
if [ "$STATUSLINE_CMD" = "$EXPECTED_STATUSLINE" ]; then ok "statusLine.command matches expected (dev: checkout path; installed: entwurf-statusline bin shim): $STATUSLINE_CMD"; else bad "statusLine.command drifted (got '$STATUSLINE_CMD', expected '$EXPECTED_STATUSLINE')"; fi
|
|
147
|
+
if [[ "$EXPECTED_STATUSLINE" == */* ]]; then
|
|
148
|
+
if [ -x "$EXPECTED_STATUSLINE" ]; then ok "statusline executable"; else bad "statusline script not executable: $EXPECTED_STATUSLINE"; fi
|
|
149
|
+
elif command -v "$EXPECTED_STATUSLINE" >/dev/null 2>&1; then
|
|
150
|
+
ok "statusline bin resolves on PATH: $(command -v "$EXPECTED_STATUSLINE")"
|
|
151
|
+
else
|
|
152
|
+
bad "statusline bin not on PATH: $EXPECTED_STATUSLINE"
|
|
153
|
+
fi
|
|
120
154
|
SAMPLE_STATUSLINE_OUT="$(printf '%s' '{"session_id":"doctor-no-record","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-sonnet-5"},"context_window":{"context_window_size":200000,"used_percentage":1,"current_usage":{"input_tokens":1}}}' | "$EXPECTED_STATUSLINE" 2>/dev/null || true)"
|
|
121
155
|
if [ "$(printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | wc -l | tr -d ' ')" = "2" ] && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '1p' | grep -q 'tmp' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q '🪛' && printf '%s\n' "$SAMPLE_STATUSLINE_OUT" | sed -n '2p' | grep -q ' cc | s'; then ok "statusline synthetic execution emits two rows (work context + identity)"; else bad "statusline synthetic execution failed or omitted two-row work/identity marker"; fi
|
|
122
156
|
else
|
|
@@ -241,10 +275,17 @@ registry_for_ms() { # $1=bundle meta-session.ts → sibling plugin-root registry
|
|
|
241
275
|
dirname "$(dirname "$1")"
|
|
242
276
|
}
|
|
243
277
|
|
|
244
|
-
|
|
278
|
+
# Source authority for parity depends on the mode: an installed bundle carries the
|
|
279
|
+
# dist `.js`, so compare it against the dist `.js` (same tsc pipeline → hash-equal);
|
|
280
|
+
# a dev clone carries the `.ts`, so compare against the `.ts` source.
|
|
281
|
+
if [ "$LIB_EXT" = "js" ]; then
|
|
282
|
+
SRC_MS="$REPO/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
|
|
283
|
+
else
|
|
284
|
+
SRC_MS="$REPO/pi-extensions/lib/meta-session.ts"
|
|
285
|
+
fi
|
|
245
286
|
SRC_REG="$REPO/pi/entwurf-capabilities.json"
|
|
246
|
-
ASM_MS="$
|
|
247
|
-
INST_MS="$(ls "$CLAUDE_CFG"/plugins/cache/"$MKT_NAME"/"$PLUGIN"/*/lib/meta-session.
|
|
287
|
+
ASM_MS="$ASM/$PLUGIN/lib/meta-session.$LIB_EXT"
|
|
288
|
+
INST_MS="$(ls "$CLAUDE_CFG"/plugins/cache/"$MKT_NAME"/"$PLUGIN"/*/lib/meta-session."$LIB_EXT" 2>/dev/null | head -1 || true)"
|
|
248
289
|
|
|
249
290
|
ASM_ROOT="$(registry_for_ms "$ASM_MS")"
|
|
250
291
|
INST_ROOT="$(registry_for_ms "${INST_MS:-}")"
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
# entwurf_inbox_read tool comes from USER-scope entwurf-bridge MCP wiring
|
|
14
14
|
# (`claude mcp add -s user ...`). Project-scoped .mcp.json is deliberately
|
|
15
15
|
# not enough: a /tmp native session would wake without a receipt tool.
|
|
16
|
-
# 2. marketplace add <
|
|
17
|
-
#
|
|
16
|
+
# 2. marketplace add <stable .assembled> (dev: repo-local; installed package:
|
|
17
|
+
# version-stable XDG data dir; NOT /tmp — ephemeral source would break
|
|
18
|
+
# `claude plugin marketplace update`).
|
|
18
19
|
# 3. install entwurf-meta-receive@meta-bridge-local --scope user (= global:
|
|
19
20
|
# every native session auto-loads it; no manual --plugin-dir).
|
|
20
21
|
# 4. install/update USER-scope entwurf-bridge MCP, so every native session has
|
|
@@ -30,7 +31,31 @@ REPO="$(cd "$HERE/.." && pwd)"
|
|
|
30
31
|
MKT_NAME="meta-bridge-local"
|
|
31
32
|
PLUGIN="entwurf-meta-receive"
|
|
32
33
|
SRC="$REPO/pi/meta-bridge"
|
|
33
|
-
|
|
34
|
+
# Dev clone: keep the marketplace source inside the checkout for transparent
|
|
35
|
+
# inspection. Installed package: assemble into a version-stable operator path;
|
|
36
|
+
# Claude settings store this directory path and package-manager upgrades do not
|
|
37
|
+
# rewrite it, so a pnpm-store path would go stale on version/peer churn.
|
|
38
|
+
#
|
|
39
|
+
# The SAME installed-vs-dev split also picks the hook artifact (0.12.5): an
|
|
40
|
+
# installed marketplace source lives below node_modules, where Node REFUSES
|
|
41
|
+
# `--experimental-strip-types` on `.ts` (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING),
|
|
42
|
+
# so installed packages run the tsc-emitted `meta-bridge-hook.js` closure (mirrors
|
|
43
|
+
# start.sh/store-doctor). Dev clones live outside node_modules and run the `.ts`
|
|
44
|
+
# source directly for transparent editing. HOOK_ENTRY is baked into hooks.json.
|
|
45
|
+
case "$REPO" in
|
|
46
|
+
*/node_modules/@junghanacs/entwurf)
|
|
47
|
+
ASM="${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/meta-bridge/.assembled"
|
|
48
|
+
HOOK_ENTRY="meta-bridge-hook.js"
|
|
49
|
+
HOOK_SRC="$REPO/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js"
|
|
50
|
+
LIB_SRC="$REPO/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js"
|
|
51
|
+
LIB_EXT="js" ;;
|
|
52
|
+
*)
|
|
53
|
+
ASM="$SRC/.assembled"
|
|
54
|
+
HOOK_ENTRY="meta-bridge-hook.ts"
|
|
55
|
+
HOOK_SRC="$REPO/pi-extensions/meta-bridge-hook.ts"
|
|
56
|
+
LIB_SRC="$REPO/pi-extensions/lib/meta-session.ts"
|
|
57
|
+
LIB_EXT="ts" ;;
|
|
58
|
+
esac
|
|
34
59
|
|
|
35
60
|
die() { echo "meta-bridge-install: $*" >&2; exit 1; }
|
|
36
61
|
|
|
@@ -65,9 +90,12 @@ mkdir -p "$ASM"
|
|
|
65
90
|
cp -r "$SRC/.claude-plugin" "$ASM/.claude-plugin"
|
|
66
91
|
cp -r "$SRC/$PLUGIN" "$ASM/$PLUGIN"
|
|
67
92
|
# entry shell + its lib travel WITH the plugin so the install copy is self-contained.
|
|
68
|
-
|
|
93
|
+
# Installed → tsc-emitted JS (dist, node_modules-safe); dev clone → strip-types .ts.
|
|
94
|
+
[ -f "$HOOK_SRC" ] || die "hook artifact missing: $HOOK_SRC (installed package ships it via prepack build-bridge → dist; reinstall @junghanacs/entwurf, or run 'pnpm run build-bridge' in a dev clone)."
|
|
95
|
+
[ -f "$LIB_SRC" ] || die "hook lib artifact missing: $LIB_SRC (same build-bridge dist closure)."
|
|
96
|
+
cp "$HOOK_SRC" "$ASM/$PLUGIN/$HOOK_ENTRY"
|
|
69
97
|
mkdir -p "$ASM/$PLUGIN/lib"
|
|
70
|
-
cp "$
|
|
98
|
+
cp "$LIB_SRC" "$ASM/$PLUGIN/lib/meta-session.$LIB_EXT"
|
|
71
99
|
cp "$REPO/pi-extensions/lib/session-id.js" "$ASM/$PLUGIN/lib/session-id.js"
|
|
72
100
|
# v2 writer (3D-3+) reads the capability registry at runtime
|
|
73
101
|
# (loadMetaCapabilityRegistry). It MUST travel at the plugin ROOT — meta-session's
|
|
@@ -76,23 +104,29 @@ cp "$REPO/pi-extensions/lib/session-id.js" "$ASM/$PLUGIN/lib/session-id.js"
|
|
|
76
104
|
# Without this, a v2 writer throws on every mint/parse. doctor-meta-bridge asserts it.
|
|
77
105
|
cp "$REPO/pi/entwurf-capabilities.json" "$ASM/$PLUGIN/entwurf-capabilities.json"
|
|
78
106
|
chmod +x "$ASM/$PLUGIN/scripts/doorbell.sh"
|
|
79
|
-
# Bake the node abspath into hooks.json — the
|
|
80
|
-
#
|
|
107
|
+
# Bake the node abspath AND the hook entry filename into hooks.json — the two
|
|
108
|
+
# templated surfaces (0.12.5: HOOK_ENTRY is meta-bridge-hook.js when installed,
|
|
109
|
+
# .ts in a dev clone). mailbox / meta-record dirs resolve at runtime inside the
|
|
110
|
+
# hook itself (<pi-agent-dir>, fixed ~/).
|
|
81
111
|
# The plugin owns ONLY the wake/record hooks; the receiver-side entwurf_inbox_read
|
|
82
112
|
# tool is NOT the plugin's job. It comes from USER-scope entwurf-bridge MCP
|
|
83
113
|
# wiring (`claude mcp add -s user ...`), never a plugin .mcp.json duplicate.
|
|
84
114
|
HOOKS="$ASM/$PLUGIN/hooks/hooks.json"
|
|
85
|
-
HOOKS_PATH="$HOOKS" NODE_PATH_TO_BAKE="$NODE_BIN" python3 - <<'PY'
|
|
115
|
+
HOOKS_PATH="$HOOKS" NODE_PATH_TO_BAKE="$NODE_BIN" HOOK_ENTRY_TO_BAKE="$HOOK_ENTRY" python3 - <<'PY'
|
|
86
116
|
from pathlib import Path
|
|
87
117
|
import os
|
|
88
118
|
hooks = Path(os.environ["HOOKS_PATH"])
|
|
89
119
|
node = os.environ["NODE_PATH_TO_BAKE"]
|
|
120
|
+
hook_entry = os.environ["HOOK_ENTRY_TO_BAKE"]
|
|
90
121
|
text = hooks.read_text(encoding="utf-8")
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
122
|
+
for placeholder in ("__NODE_BIN__", "__HOOK_ENTRY__"):
|
|
123
|
+
if placeholder not in text:
|
|
124
|
+
raise SystemExit(f"hooks bake failed before replacement ({placeholder} absent in {hooks})")
|
|
125
|
+
hooks.write_text(text.replace("__NODE_BIN__", node).replace("__HOOK_ENTRY__", hook_entry), encoding="utf-8")
|
|
94
126
|
PY
|
|
95
|
-
|
|
127
|
+
for placeholder in "__NODE_BIN__" "__HOOK_ENTRY__"; do
|
|
128
|
+
grep -q "$placeholder" "$HOOKS" && die "hooks bake failed ($placeholder still present in $HOOKS)."
|
|
129
|
+
done
|
|
96
130
|
echo "[meta-bridge-install] assembled $ASM (node baked, entry+lib bundled; MCP wiring is NOT plugin-owned)"
|
|
97
131
|
|
|
98
132
|
# --- validate the manifests before touching user config ---------------------
|
|
@@ -306,6 +306,12 @@ def desired_mcp(repo: Path) -> dict[str, Any]:
|
|
|
306
306
|
|
|
307
307
|
|
|
308
308
|
def desired_statusline(repo: Path) -> dict[str, Any]:
|
|
309
|
+
if is_installed_package(repo):
|
|
310
|
+
# Installed package: mirror the MCP stable-bin pattern. The statusLine is
|
|
311
|
+
# executed at render time, so a bare bin shim lets npm/pnpm update the
|
|
312
|
+
# target on package upgrade without rewriting Claude settings. Dev clones
|
|
313
|
+
# stay pinned to their checkout below.
|
|
314
|
+
return {"type": "command", "command": "entwurf-statusline"}
|
|
309
315
|
return {"type": "command", "command": str((repo / "scripts" / "meta-bridge-statusline.sh").resolve())}
|
|
310
316
|
|
|
311
317
|
|
|
@@ -543,7 +549,16 @@ def main() -> int:
|
|
|
543
549
|
parser = argparse.ArgumentParser(description="entwurf meta-bridge state manager")
|
|
544
550
|
parser.add_argument(
|
|
545
551
|
"command",
|
|
546
|
-
choices=[
|
|
552
|
+
choices=[
|
|
553
|
+
"prepare",
|
|
554
|
+
"apply",
|
|
555
|
+
"preflight-uninstall",
|
|
556
|
+
"uninstall",
|
|
557
|
+
"check",
|
|
558
|
+
"managed-keys",
|
|
559
|
+
"desired-mcp",
|
|
560
|
+
"desired-statusline",
|
|
561
|
+
],
|
|
547
562
|
)
|
|
548
563
|
parser.add_argument("--repo", default=Path(__file__).resolve().parents[1], type=Path)
|
|
549
564
|
parser.add_argument("--asm", default=None, type=Path)
|
|
@@ -568,6 +583,10 @@ def main() -> int:
|
|
|
568
583
|
# write for --repo. Lets a deterministic guard assert the installed-vs-clone
|
|
569
584
|
# dual-mode without spinning up a real `claude` CLI. --repo need not exist.
|
|
570
585
|
print(json.dumps(desired_mcp(repo), indent=2))
|
|
586
|
+
elif args.command == "desired-statusline":
|
|
587
|
+
# Same guard surface for the statusLine dual-mode: dev clones pin the
|
|
588
|
+
# checkout path; installed packages use the stable bin shim.
|
|
589
|
+
print(json.dumps(desired_statusline(repo), indent=2))
|
|
571
590
|
except StateError as exc:
|
|
572
591
|
print(f"meta-bridge-state: {exc}", file=sys.stderr)
|
|
573
592
|
return 1
|
|
@@ -36,4 +36,17 @@ else
|
|
|
36
36
|
fi
|
|
37
37
|
|
|
38
38
|
python3 "$REPO/scripts/meta-bridge-state.py" uninstall --repo "$REPO"
|
|
39
|
+
|
|
40
|
+
# Remove the assembled marketplace source too — an honest inverse must not orphan
|
|
41
|
+
# it. Installed packages assemble into a version-stable operator data dir
|
|
42
|
+
# (~/.local/share/entwurf/meta-bridge/.assembled); leaving that tree behind is the
|
|
43
|
+
# only meta-bridge footprint uninstall would otherwise leak. Dev clones drop their
|
|
44
|
+
# gitignored in-checkout .assembled. Mirror install-meta-bridge's ASM resolution.
|
|
45
|
+
case "$REPO" in
|
|
46
|
+
*/node_modules/@junghanacs/entwurf)
|
|
47
|
+
rm -rf "${XDG_DATA_HOME:-$HOME/.local/share}/entwurf/meta-bridge"
|
|
48
|
+
rmdir "${XDG_DATA_HOME:-$HOME/.local/share}/entwurf" 2>/dev/null || true ;;
|
|
49
|
+
*)
|
|
50
|
+
rm -rf "$REPO/pi/meta-bridge/.assembled" ;;
|
|
51
|
+
esac
|
|
39
52
|
echo "[meta-bridge-uninstall] DONE"
|
|
@@ -71,6 +71,12 @@ JSON
|
|
|
71
71
|
|
|
72
72
|
py() { python3 "$STATE" "$@" --repo "$REPO" --asm "$ASM"; }
|
|
73
73
|
|
|
74
|
+
DEV_STATUSLINE="$(python3 "$STATE" desired-statusline --repo "$REPO" | python3 -c 'import json,sys; print(json.load(sys.stdin)["command"])')"
|
|
75
|
+
if [ "$DEV_STATUSLINE" = "$REPO/scripts/meta-bridge-statusline.sh" ]; then ok "dev statusLine pins the checkout script"; else bad "dev statusLine command drifted: $DEV_STATUSLINE"; fi
|
|
76
|
+
FAKE_INSTALLED_REPO="$TMP/npmroot/node_modules/@junghanacs/entwurf"
|
|
77
|
+
INSTALLED_STATUSLINE="$(python3 "$STATE" desired-statusline --repo "$FAKE_INSTALLED_REPO" | python3 -c 'import json,sys; print(json.load(sys.stdin)["command"])')"
|
|
78
|
+
if [ "$INSTALLED_STATUSLINE" = "entwurf-statusline" ]; then ok "installed statusLine uses the stable bin shim"; else bad "installed statusLine command drifted: $INSTALLED_STATUSLINE"; fi
|
|
79
|
+
|
|
74
80
|
py prepare >/dev/null
|
|
75
81
|
STATE_FILE="$CLAUDE_CONFIG_DIR/entwurf.install-state.json"
|
|
76
82
|
[ -f "$STATE_FILE" ] && ok "prepare writes install-state before any merge" || bad "state file missing after prepare"
|