@junghanacs/entwurf 0.12.6 → 0.12.7

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 (92) hide show
  1. package/AGENTS.md +31 -17
  2. package/BASELINE.md +42 -8
  3. package/CHANGELOG.md +22 -0
  4. package/DELIVERY.md +71 -14
  5. package/README.md +77 -45
  6. package/VERIFY.md +18 -13
  7. package/docs/setup-clean-host.md +80 -22
  8. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +90 -66
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +54 -0
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +153 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +436 -0
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +157 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +105 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +90 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +194 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +153 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +42 -9
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +49 -13
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +104 -11
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +30 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +57 -0
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +10 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +9 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +21 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -0
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +17 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +125 -0
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +158 -0
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +61 -0
  30. package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +166 -0
  31. package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +130 -0
  32. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +178 -0
  33. package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +24 -0
  34. package/mcp/entwurf-bridge/src/index.ts +101 -67
  35. package/mcp/entwurf-bridge/test.sh +1 -1
  36. package/mcp/entwurf-bridge/tsconfig.build.json +23 -3
  37. package/package.json +10 -5
  38. package/pi-extensions/lib/entwurf-deliverability.ts +62 -9
  39. package/pi-extensions/lib/entwurf-self-address.ts +58 -15
  40. package/pi-extensions/lib/entwurf-v2-contract.ts +120 -12
  41. package/pi-extensions/lib/entwurf-v2-decider.ts +60 -0
  42. package/pi-extensions/lib/entwurf-v2-native-push.ts +86 -0
  43. package/pi-extensions/lib/entwurf-v2-production.ts +20 -0
  44. package/pi-extensions/lib/entwurf-v2-release.ts +9 -0
  45. package/pi-extensions/lib/entwurf-v2-runner.ts +29 -1
  46. package/pi-extensions/lib/entwurf-v2-send.ts +7 -0
  47. package/pi-extensions/lib/entwurf-v2-surface.ts +17 -0
  48. package/pi-extensions/lib/meta-sender-identity.ts +154 -0
  49. package/pi-extensions/lib/native-push/adapter.ts +255 -0
  50. package/pi-extensions/lib/native-push/register.ts +99 -0
  51. package/run.sh +756 -214
  52. package/scripts/agy-bridge-config.py +446 -0
  53. package/scripts/agy-bridge.sh +359 -0
  54. package/scripts/agy-hooks-bridge.sh +193 -0
  55. package/scripts/agy-hooks-config.py +257 -0
  56. package/scripts/agy-imprint.sh +28 -0
  57. package/scripts/agy-imprint.ts +193 -0
  58. package/scripts/agy-statusline-bridge.sh +176 -0
  59. package/scripts/agy-statusline-config.py +213 -0
  60. package/scripts/agy-statusline.sh +256 -0
  61. package/scripts/build-bridge.sh +20 -0
  62. package/scripts/check-agy-sender-identity.ts +364 -0
  63. package/scripts/check-entwurf-bridge-boot.ts +8 -2
  64. package/scripts/check-entwurf-deliverability.ts +34 -0
  65. package/scripts/check-entwurf-self-address.ts +78 -11
  66. package/scripts/check-entwurf-v2-contract.ts +136 -1
  67. package/scripts/check-entwurf-v2-decider.ts +95 -1
  68. package/scripts/check-entwurf-v2-matrix.ts +14 -3
  69. package/scripts/check-entwurf-v2-native-push.ts +193 -0
  70. package/scripts/check-entwurf-v2-production.ts +68 -1
  71. package/scripts/check-entwurf-v2-runner.ts +58 -0
  72. package/scripts/check-entwurf-v2-surface.ts +35 -0
  73. package/scripts/check-install-surface.ts +357 -0
  74. package/scripts/check-native-push-adapter.ts +319 -0
  75. package/scripts/check-native-push-register.ts +130 -0
  76. package/scripts/dev-bin.sh +195 -0
  77. package/scripts/doctor-pi-provider.ts +140 -0
  78. package/scripts/meta-bridge-doctor.sh +36 -2
  79. package/scripts/register-pi-package.py +37 -3
  80. package/scripts/register-pi-provider.py +287 -0
  81. package/scripts/smoke-agy-hooks-state.sh +172 -0
  82. package/scripts/smoke-agy-install-state.sh +660 -0
  83. package/scripts/smoke-agy-native-push-live.ts +243 -0
  84. package/scripts/smoke-agy-statusline-state.sh +300 -0
  85. package/scripts/smoke-meta-async-drift.sh +9 -2
  86. package/scripts/smoke-meta-install-state.sh +20 -0
  87. package/scripts/smoke-pi-provider-state.sh +182 -0
  88. package/scripts/smoke-user-scope-citizen.sh +62 -0
  89. package/scripts/with-dist-lock.sh +81 -0
  90. package/scripts/__pycache__/meta-bridge-state.cpython-312.pyc +0 -0
  91. package/scripts/__pycache__/meta-bridge-state.cpython-313.pyc +0 -0
  92. package/scripts/__pycache__/register-pi-package.cpython-313.pyc +0 -0
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * meta-bridge-prune — LISTING-ONLY hygiene surface for the meta-session store.
4
+ *
5
+ * 1.0.0 policy (GLG + GPT힣, 2026-06-06): list candidates, NEVER delete. The
6
+ * store is the native→garden lookup authority, so this surface is deliberately
7
+ * conservative:
8
+ * - orphan (transcript gone) / stale (recordUpdatedAt older than ttl) are
9
+ * SUGGESTED for manual prune with the exact rm command printed — but removed
10
+ * by no one here. transcript-gone is a strong abandonment signal, not proof; a
11
+ * backend path migration / cleanup / config-dir change can also vacate it. A
12
+ * NULL transcriptPath (v2 nullable-at-birth, e.g. pi) is "unknown", NOT gone —
13
+ * never orphan on null; only a non-empty string path that no longer exists.
14
+ * - corrupt JSON / body↔filename drift / duplicate nativeSessionId are
15
+ * AMBIGUOUS / manual-only: the operator decides which authority survives.
16
+ * Never blindly rm a duplicate pair.
17
+ * No --apply in 1.0.0. The script prints, the operator removes. doctor stays the
18
+ * fail-loud surface (store-doctor reds on corrupt/duplicate/drift); prune is the
19
+ * separate janitor LISTING that keeps a green store from silently bloating with
20
+ * transcript-gone records — which doctor intentionally does NOT fail on.
21
+ */
22
+ import fs from "node:fs";
23
+ import path from "node:path";
24
+ import { defaultMetaSessionsDir, parseMetaIdentity } from "../pi-extensions/lib/meta-session.js";
25
+ /** POSIX single-quote a path so the printed rm command survives spaces/specials. */
26
+ function shellQuote(s) {
27
+ return `'${s.replace(/'/g, "'\\''")}'`;
28
+ }
29
+ function usage(code) {
30
+ console.error("usage: node --experimental-strip-types scripts/meta-bridge-prune.ts [meta-sessions-dir] [--ttl-days N]");
31
+ process.exit(code);
32
+ }
33
+ let dirArg;
34
+ let ttlDays = 30;
35
+ for (let i = 2; i < process.argv.length; i++) {
36
+ const a = process.argv[i];
37
+ if (a === "--ttl-days") {
38
+ const v = process.argv[++i];
39
+ const n = Number(v);
40
+ if (!Number.isFinite(n) || n < 0)
41
+ usage(2);
42
+ ttlDays = n;
43
+ }
44
+ else if (a.startsWith("--ttl-days=")) {
45
+ const n = Number(a.slice("--ttl-days=".length));
46
+ if (!Number.isFinite(n) || n < 0)
47
+ usage(2);
48
+ ttlDays = n;
49
+ }
50
+ else if (a === "-h" || a === "--help") {
51
+ usage(0);
52
+ }
53
+ else if (a.startsWith("-")) {
54
+ console.error(`unknown flag: ${a}`);
55
+ usage(2);
56
+ }
57
+ else if (dirArg === undefined) {
58
+ dirArg = a;
59
+ }
60
+ else {
61
+ usage(2);
62
+ }
63
+ }
64
+ const dir = dirArg ?? defaultMetaSessionsDir();
65
+ const ttlMs = ttlDays * 24 * 60 * 60 * 1000;
66
+ const now = Date.now();
67
+ console.log("meta-bridge prune candidates");
68
+ console.log(`store: ${dir}`);
69
+ console.log(`ttl: ${ttlDays}d (stale threshold)`);
70
+ console.log("");
71
+ if (!fs.existsSync(dir)) {
72
+ console.log(`store does not exist yet (0 records).`);
73
+ console.log("");
74
+ console.log("No files removed.");
75
+ process.exit(0);
76
+ }
77
+ const goodByNative = new Map();
78
+ const ambiguous = [];
79
+ let scanned = 0;
80
+ for (const filename of fs.readdirSync(dir).sort()) {
81
+ if (!filename.endsWith(".meta.json"))
82
+ continue;
83
+ scanned += 1;
84
+ const file = path.join(dir, filename);
85
+ // dual-read (3D-4 commit1): parseMetaIdentity reads v1 AND v2, normalizing to
86
+ // identity (transcriptPath nullable, lastSeen → recordUpdatedAt).
87
+ let id;
88
+ try {
89
+ id = parseMetaIdentity(fs.readFileSync(file, "utf8"));
90
+ }
91
+ catch (err) {
92
+ ambiguous.push(`${filename}: corrupt — ${err instanceof Error ? err.message : String(err)}`);
93
+ continue;
94
+ }
95
+ const expectedFilename = `${id.gardenId}.meta.json`;
96
+ if (filename !== expectedFilename) {
97
+ ambiguous.push(`${filename}: body/filename drift — body gardenId=${id.gardenId}, expected ${expectedFilename}`);
98
+ continue;
99
+ }
100
+ const list = goodByNative.get(id.nativeSessionId) ?? [];
101
+ list.push({
102
+ file: filename,
103
+ gardenId: id.gardenId,
104
+ backend: id.backend,
105
+ nativeSessionId: id.nativeSessionId,
106
+ transcriptPath: id.transcriptPath,
107
+ recordUpdatedAt: id.recordUpdatedAt,
108
+ });
109
+ goodByNative.set(id.nativeSessionId, list);
110
+ }
111
+ const orphan = [];
112
+ const stale = [];
113
+ let keep = 0;
114
+ for (const [nativeSessionId, list] of goodByNative.entries()) {
115
+ if (list.length > 1) {
116
+ // Authority ambiguity — operator must choose which survives. Never auto-pick.
117
+ ambiguous.push(`duplicate nativeSessionId ${JSON.stringify(nativeSessionId)} in ${list.map((g) => g.file).join(", ")} — manual-only, do not blindly remove`);
118
+ continue;
119
+ }
120
+ const g = list[0];
121
+ // G2 (3D-4): null transcriptPath is NOT orphan. v2 (pi nullable-at-birth) leaves
122
+ // it null = "unknown / not yet known", which is not "the file went away". Orphan
123
+ // is ONLY a non-empty string path that no longer exists on disk. A null path
124
+ // falls through to the recordUpdatedAt staleness check like any live record.
125
+ const tp = g.transcriptPath;
126
+ if (typeof tp === "string" && tp !== "" && !fs.existsSync(tp)) {
127
+ orphan.push(g);
128
+ continue;
129
+ }
130
+ // recordUpdatedAt is only proven a non-empty string by the parser, not a real
131
+ // date. KEEP means live + RECENT; an unparseable recordUpdatedAt cannot prove
132
+ // recency, so it is not a silent keep — it goes manual-only like any defect.
133
+ const updatedMs = Date.parse(g.recordUpdatedAt);
134
+ if (!Number.isFinite(updatedMs)) {
135
+ ambiguous.push(`${g.file}: unparseable recordUpdatedAt ${JSON.stringify(g.recordUpdatedAt)} — cannot prove recent, manual-only`);
136
+ continue;
137
+ }
138
+ if (now - updatedMs > ttlMs)
139
+ stale.push(g);
140
+ else
141
+ keep += 1;
142
+ }
143
+ const ageDays = (iso) => {
144
+ const ms = now - Date.parse(iso);
145
+ if (!Number.isFinite(ms))
146
+ return "?";
147
+ return String(Math.floor(ms / (24 * 60 * 60 * 1000)));
148
+ };
149
+ console.log(`ORPHAN transcript-gone (${orphan.length}):`);
150
+ for (const g of orphan) {
151
+ console.log(`- ${g.gardenId} ${g.backend} native=${g.nativeSessionId} transcript=${g.transcriptPath ?? "(null)"}`);
152
+ }
153
+ console.log("");
154
+ console.log(`STALE recordUpdatedAt>${ttlDays}d (${stale.length}):`);
155
+ for (const g of stale) {
156
+ console.log(`- ${g.gardenId} ${g.backend} recordUpdatedAt=${g.recordUpdatedAt} age=${ageDays(g.recordUpdatedAt)}d`);
157
+ }
158
+ console.log("");
159
+ console.log(`AMBIGUOUS manual-only (${ambiguous.length}):`);
160
+ for (const reason of ambiguous) {
161
+ console.log(`- ${reason}`);
162
+ }
163
+ console.log("");
164
+ console.log(`KEEP (live/recent): ${keep} record(s)`);
165
+ console.log(`scanned: ${scanned} record(s)`);
166
+ console.log("");
167
+ console.log("No files removed.");
168
+ const removable = [...orphan, ...stale];
169
+ if (removable.length > 0) {
170
+ console.log("To prune an orphan/stale candidate, inspect it then remove its file manually:");
171
+ for (const g of removable) {
172
+ console.log(` rm -- ${shellQuote(path.join(dir, `${g.gardenId}.meta.json`))}`);
173
+ }
174
+ }
175
+ if (ambiguous.length > 0) {
176
+ console.log("AMBIGUOUS records are NOT listed as rm commands — inspect each and decide which authority survives.");
177
+ }
178
+ process.exit(0);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * new-session-id — print one fresh garden-native sessionId and exit.
3
+ *
4
+ * The garden launcher (operator alias / wrapper) calls this so every
5
+ * `--entwurf-control` session is born a garden citizen:
6
+ *
7
+ * pi --session-id "$(./run.sh new-session-id)" --entwurf-control \
8
+ * --emacs-agent-socket server …
9
+ *
10
+ * The id is `generateSessionId()` from entwurf-core — the single SSOT for the
11
+ * locked `YYYYMMDDTHHMMSS-[0-9a-f]{6}` grammar. Do NOT reimplement the format in
12
+ * the shell (it would drift from the validator the resident guard enforces).
13
+ *
14
+ * Stdout is the id and nothing else (no trailing prose), so `$(…)` captures a
15
+ * clean value. Errors go to stderr with a nonzero exit.
16
+ */
17
+ import { generateSessionId } from "../pi-extensions/lib/entwurf-core.js";
18
+ try {
19
+ process.stdout.write(`${generateSessionId()}\n`);
20
+ }
21
+ catch (err) {
22
+ process.stderr.write(`new-session-id failed: ${err instanceof Error ? err.message : String(err)}\n`);
23
+ process.exit(1);
24
+ }
@@ -52,18 +52,19 @@ import { receiverMarkerMatchesIdentity } from "../../../pi-extensions/lib/entwur
52
52
  import { listEntwurfFacts } from "../../../pi-extensions/lib/entwurf-fact-provider.ts";
53
53
  import { renderEntwurfPeers } from "../../../pi-extensions/lib/entwurf-peers-render.ts";
54
54
  import { computeSelfAddressability } from "../../../pi-extensions/lib/entwurf-self-address.ts";
55
+ import { nativePushSupported } from "../../../pi-extensions/lib/entwurf-v2-contract.ts";
55
56
  import { runAndRenderEntwurfV2FromSurface } from "../../../pi-extensions/lib/entwurf-v2-surface.ts";
57
+ import {
58
+ probeNativeSenderAlive,
59
+ resolveTrustedMetaSenderIdentity,
60
+ } from "../../../pi-extensions/lib/meta-sender-identity.ts";
56
61
  import {
57
62
  defaultMetaMailboxDir,
58
63
  defaultMetaSessionsDir,
59
- type MetaIdentity,
60
- type MetaSenderMarker,
61
- parentPid,
62
- readMetaIdentityByGardenId,
63
64
  readMetaInbox,
64
65
  readMetaReceiverMarker,
65
- readMetaSenderMarker,
66
66
  } from "../../../pi-extensions/lib/meta-session.ts";
67
+ import { registerNativeConversation } from "../../../pi-extensions/lib/native-push/register.ts";
67
68
 
68
69
  const HOME = os.homedir();
69
70
  const DEFAULT_ENTWURF_DIR = path.join(HOME, ".pi", "entwurf-control");
@@ -150,22 +151,6 @@ class EntwurfSenderIdentityError extends Error {
150
151
  }
151
152
  }
152
153
 
153
- // Resolve the meta-sender marker for THIS MCP process. ENTWURF_META_SENDER_MARKER is an
154
- // explicit override (test / wiring). Otherwise try the shared ancestor: process.ppid
155
- // first, then one step up (Claude may run the hook through a shell wrapper, shifting
156
- // the shared ancestor). readMetaSenderMarker's pid+start-key guard rejects a
157
- // dead/reused owner, so a wrong marker is never trusted on any candidate.
158
- function resolveMetaSenderMarker(): MetaSenderMarker | null {
159
- const explicit = process.env.ENTWURF_META_SENDER_MARKER?.trim();
160
- if (explicit) return readMetaSenderMarker({ markerPath: explicit });
161
- const candidates = [process.ppid, parentPid(process.ppid)].filter((p): p is number => typeof p === "number" && p > 0);
162
- for (const ownerPid of candidates) {
163
- const marker = readMetaSenderMarker({ backend: "claude-code", ownerPid });
164
- if (marker) return marker;
165
- }
166
- return null;
167
- }
168
-
169
154
  function buildStrictPiSenderEnvelope(): SenderEnvelope {
170
155
  const sessionId = process.env.PI_SESSION_ID?.trim();
171
156
  const agentId = process.env.PI_AGENT_ID?.trim();
@@ -195,47 +180,49 @@ function buildStrictPiSenderEnvelope(): SenderEnvelope {
195
180
  };
196
181
  }
197
182
 
198
- function buildTrustedMetaSenderEnvelope(cwd: string = process.cwd()): SenderEnvelope | null {
199
- // No pi-session identity. Try the meta-sender marker: a native backend that
200
- // minted a garden-id via its SessionStart hook. The marker is keyed by the
201
- // shared parent pid — this MCP child's process.ppid IS the Claude Code process
202
- // the hook ran under (NOT cwd inference). ENTWURF_META_SENDER_MARKER overrides the
203
- // lookup for explicit wiring / tests. A trusted marker promotes this process to
204
- // a REPLYABLE meta-session sender addressed by its garden-id.
205
- const marker = resolveMetaSenderMarker();
206
- if (!marker) return null;
207
-
208
- // Validate the marker against its backing meta-record: a stale marker (record
209
- // deleted, or backend/nativeSessionId drift) must NOT grant a replyable
210
- // identity. The record store is the authority; the marker is only a pid→garden
211
- // hint.
212
- let identity: MetaIdentity | null = null;
213
- try {
214
- // dual-read (3D-4 commit1): identity-only check (backend/nativeSessionId), so
215
- // it survives the v2 cut. Reads both v1 and v2 records.
216
- const id = readMetaIdentityByGardenId(marker.gardenId);
217
- if (id.backend === marker.backend && id.nativeSessionId === marker.nativeSessionId) identity = id;
218
- } catch {
219
- identity = null;
220
- }
221
- if (!identity) return null;
222
-
223
- // SE-2 slice 2e-b: identity is trusted, but `replyable` is a SEPARATE fact — can THIS
224
- // session's own receiver inbox actually wake? Read the receiver presence marker (slice
225
- // 2b) and require it to match the identity (the same SSOT helper the mailbox guard uses).
226
- // recordBacked is true here by construction; ownerAlive+watchArmed BOTH come from the
227
- // matched receiver marker (readMetaReceiverMarker's verifyOwner folds a dead/reused owner
228
- // to null, so a match means a live, armed receiver — the sender marker only proves
229
- // identity, not an armed watch). Inactive → the meta identity is STILL returned (who-sent
230
- // must survive; degrading to null would erase the sender) but with replyable:false.
231
- const receiver = readMetaReceiverMarker({ gardenId: identity.gardenId });
232
- const active = receiverMarkerMatchesIdentity(receiver, identity);
233
- const self = computeSelfAddressability({
234
- origin: "meta-session",
235
- recordBacked: true,
236
- ownerAlive: active,
237
- watchArmed: active,
183
+ async function buildTrustedMetaSenderEnvelope(cwd: string = process.cwd()): Promise<SenderEnvelope | null> {
184
+ // No pi-session identity. Try the meta-sender marker: a native backend that minted a
185
+ // garden-id from its own hook (Claude SessionStart / agy PreInvocation). The marker is
186
+ // keyed by the shared parent pid — this MCP child's process.ppid IS the native host the
187
+ // hook ran under (NOT cwd inference). A trusted marker promotes this process from
188
+ // anonymous external-mcp to a meta-session sender addressed by its garden-id.
189
+ const trusted = resolveTrustedMetaSenderIdentity({
190
+ markerPath: process.env.ENTWURF_META_SENDER_MARKER?.trim() || undefined,
238
191
  });
192
+ if (!trusted) return null;
193
+ const { marker, identity } = trusted;
194
+
195
+ // Identity is trusted — but `replyable` is a SEPARATE fact, and WHICH fact depends on the
196
+ // rail a reply would ride (보정①). The domain comes from nativePushSupported(backend), not
197
+ // from wakeMode: `direct-inject` also covers codex/pi, which have no native-push adapter.
198
+ // self-fetch (claude-code): can this citizen's own inbox wake? → the receiver presence
199
+ // marker (readMetaReceiverMarker folds a dead/reused owner to null, so a match means a
200
+ // live, ARMED receiver — the sender marker proves identity, never an armed watch).
201
+ // native-push (antigravity): there is no inbox and no watch. A reply is injected into a
202
+ // live app-server conversation, so only an adapter probe can answer. Composing the
203
+ // receiver atom here would demand `watchArmed` from a backend that never arms one, and
204
+ // every agy citizen would report replyable:false forever.
205
+ // Either way an inactive/unreachable citizen STILL returns its identity (who-sent must
206
+ // survive; degrading to null would erase the sender) — only with replyable:false.
207
+ const facts = nativePushSupported(identity.backend)
208
+ ? {
209
+ origin: "meta-session" as const,
210
+ metaDeliveryDomain: "native-push" as const,
211
+ recordBacked: true,
212
+ probeAlive: await probeNativeSenderAlive(identity),
213
+ }
214
+ : (() => {
215
+ const receiver = readMetaReceiverMarker({ gardenId: identity.gardenId });
216
+ const active = receiverMarkerMatchesIdentity(receiver, identity);
217
+ return {
218
+ origin: "meta-session" as const,
219
+ metaDeliveryDomain: "self-fetch" as const,
220
+ recordBacked: true,
221
+ ownerAlive: active,
222
+ watchArmed: active,
223
+ };
224
+ })();
225
+ const self = computeSelfAddressability(facts);
239
226
 
240
227
  return {
241
228
  sessionId: identity.gardenId,
@@ -247,13 +234,15 @@ function buildTrustedMetaSenderEnvelope(cwd: string = process.cwd()): SenderEnve
247
234
  };
248
235
  }
249
236
 
250
- function buildAuthoritativeSelfEnvelope(): SenderEnvelope {
237
+ // async only for the native-push branch's adapter probe: a pi sender and a claude-code
238
+ // sender still resolve from files alone, so their cost is unchanged.
239
+ async function buildAuthoritativeSelfEnvelope(): Promise<SenderEnvelope> {
251
240
  const sessionId = process.env.PI_SESSION_ID?.trim();
252
241
  const agentId = process.env.PI_AGENT_ID?.trim();
253
242
  const cwd = process.cwd();
254
243
  if (sessionId && agentId && cwd) return buildStrictPiSenderEnvelope();
255
244
 
256
- const meta = buildTrustedMetaSenderEnvelope(cwd);
245
+ const meta = await buildTrustedMetaSenderEnvelope(cwd);
257
246
  if (meta) return meta;
258
247
 
259
248
  const missing: string[] = [];
@@ -263,13 +252,13 @@ function buildAuthoritativeSelfEnvelope(): SenderEnvelope {
263
252
  throw new EntwurfEnvelopeWiringError(missing);
264
253
  }
265
254
 
266
- function buildSendSenderEnvelope(): SenderEnvelope {
255
+ async function buildSendSenderEnvelope(): Promise<SenderEnvelope> {
267
256
  const sessionId = process.env.PI_SESSION_ID?.trim();
268
257
  const agentId = process.env.PI_AGENT_ID?.trim();
269
258
  const cwd = process.cwd();
270
259
  if (sessionId && agentId && cwd) return buildStrictPiSenderEnvelope();
271
260
 
272
- const meta = buildTrustedMetaSenderEnvelope(cwd);
261
+ const meta = await buildTrustedMetaSenderEnvelope(cwd);
273
262
  if (meta) return meta;
274
263
 
275
264
  // No marker. Anonymous external is allowed ONLY when not explicitly forbidden.
@@ -364,7 +353,7 @@ server.tool(
364
353
  try {
365
354
  // Resolved ONCE so the dispatch-moment timestamp is fixed and the control RPC sender
366
355
  // + the mailbox body sender share one envelope. No replyability gate (see above).
367
- const sender = buildSendSenderEnvelope();
356
+ const sender = await buildSendSenderEnvelope();
368
357
  const rendered = await runAndRenderEntwurfV2FromSurface(
369
358
  { target, intent, message, mode, wants_reply },
370
359
  // agentDir / prefixRoots intentionally omitted: runAndRenderEntwurfV2FromSurface falls
@@ -389,7 +378,7 @@ server.tool(
389
378
  {},
390
379
  async () => {
391
380
  try {
392
- const sender = buildAuthoritativeSelfEnvelope();
381
+ const sender = await buildAuthoritativeSelfEnvelope();
393
382
  const kst = formatKstTimestamp(sender.timestamp);
394
383
  const extra: Record<string, string> = {};
395
384
  const lines = [
@@ -498,6 +487,51 @@ server.tool(
498
487
  },
499
488
  );
500
489
 
490
+ server.tool(
491
+ "entwurf_register_native",
492
+ "Register an ALREADY-RUNNING native conversation as a garden citizen — it does NOT spawn " +
493
+ "a new one (that is a separate, deferred capability; do not use this to create a sibling). " +
494
+ "Give the backend + its native conversation id + the cwd, and this binds them to a garden id " +
495
+ "so entwurf_v2 can reach the conversation (fire-and-forget → native-push). The conversation " +
496
+ "must be LIVE: it is probed first, and a dead/indeterminate probe is refused (no garden id is " +
497
+ "minted for a pointer that does not resolve to a real host). Re-registering the same " +
498
+ "conversation attaches to the SAME garden id and refreshes the cwd. Only 'antigravity' is " +
499
+ "registerable on this lane. No mailbox receiver marker is written — native-push has no " +
500
+ "idle-wake watch; the returned garden id is the reply handle.",
501
+ {
502
+ backend: z
503
+ .enum(["antigravity"])
504
+ .describe("The native backend hosting the conversation. Only 'antigravity' — codex is a separate lane."),
505
+ nativeSessionId: z
506
+ .string()
507
+ .min(1)
508
+ .describe("The backend's native conversation id (antigravity conversationId) to bind to a garden id."),
509
+ cwd: z
510
+ .string()
511
+ .min(1)
512
+ .describe(
513
+ "The working directory to record for this citizen — REQUIRED (a native conversation's metadata cannot confirm it, so you must state it).",
514
+ ),
515
+ },
516
+ async ({ backend, nativeSessionId, cwd }) => {
517
+ try {
518
+ const result = await registerNativeConversation({ backend, nativeSessionId, cwd });
519
+ return textOk(
520
+ `[entwurf register native ⟶]\n` +
521
+ ` backend: ${result.backend}\n` +
522
+ ` conversation: ${result.nativeSessionId}\n` +
523
+ ` action: ${result.action}\n` +
524
+ ` gardenId: ${result.gardenId}\n` +
525
+ ` cwd: ${result.cwd}\n\n` +
526
+ `Reach it with entwurf_v2 (target=${result.gardenId}, intent=fire-and-forget). No receiver ` +
527
+ `marker was written (native-push has no idle-wake mailbox watch).`,
528
+ );
529
+ } catch (err) {
530
+ return textErr(`entwurf_register_native error: ${err instanceof Error ? err.message : String(err)}`);
531
+ }
532
+ },
533
+ );
534
+
501
535
  // ============================================================================
502
536
  // Main
503
537
  // ============================================================================
@@ -44,7 +44,7 @@ python3 - <<'PY' <<<"$TOOLS_JSON"
44
44
  import json, sys
45
45
  obj=json.load(sys.stdin)
46
46
  names={t['name'] for t in obj['result']['tools']}
47
- expected={'entwurf_v2','entwurf_self','entwurf_peers','entwurf_inbox_read'}
47
+ expected={'entwurf_v2','entwurf_self','entwurf_peers','entwurf_inbox_read','entwurf_register_native'}
48
48
  missing=expected-names
49
49
  legacy={'entwurf','entwurf_resume','entwurf_send'} & names
50
50
  if missing:
@@ -52,16 +52,36 @@
52
52
  // Under an installed marketplace source below node_modules that is
53
53
  // ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING — the SAME strip-types fence
54
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
55
+ // At 0.12.5 this was the last known `.ts`-at-runtime surface; emitting it here
56
+ // closed that release's set (installer copies dist JS when installed, keeps
57
+ // `.ts` for a dev clone). Its only repo import is meta-session.ts, ALREADY emitted for the
58
58
  // store-doctor, so this adds ONE leaf JS (dist/pi-extensions/meta-bridge-hook.js)
59
59
  // and ZERO new deps. It is a plugin hook, never in index.ts's boot closure, so
60
60
  // check-entwurf-bridge-pi-free is unaffected.
61
+ //
62
+ // WHY agy-imprint rides THIS build (0.12.7): the npm bin resolves to
63
+ // scripts/agy-imprint.sh under node_modules. Running its raw .ts there hits the
64
+ // same ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING fence. Emit the hook leaf as
65
+ // dist/scripts/agy-imprint.js; the stable bin chooses it only when installed and
66
+ // keeps transparent strip-types source execution in a dev clone.
67
+ //
68
+ // WHY the three OPERATOR commands ride this build (0.12.7): `entwurf` dispatches
69
+ // them through run.sh, whose REPO_DIR is under node_modules once installed — so
70
+ // they hit the same fence the imprint hook did, and shipped dead. They are not
71
+ // dev gates: doctor-pi-provider is the pi-ownership verdict, new-session-id is
72
+ // the operator alias that mints a garden citizen (docs/setup-clean-host.md tells
73
+ // operators to run it from the installed bin), and meta-bridge-prune is the store
74
+ // maintenance verb. check-*/smoke-* stay OUT on purpose — they are dev-clone-only
75
+ // and run_ts refuses them under an installed package rather than emitting 70+
76
+ // gate leaves into the tarball.
61
77
  "include": [
62
78
  "./src/index.ts",
63
79
  "../../protocol.js",
64
80
  "../../scripts/meta-bridge-store-doctor.ts",
81
+ "../../scripts/agy-imprint.ts",
82
+ "../../scripts/doctor-pi-provider.ts",
83
+ "../../scripts/new-session-id.ts",
84
+ "../../scripts/meta-bridge-prune.ts",
65
85
  "../../pi-extensions/meta-bridge-hook.ts"
66
86
  ]
67
87
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.12.6",
3
+ "version": "0.12.7",
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": {
@@ -60,7 +60,9 @@
60
60
  "VERIFY.md",
61
61
  "DELIVERY.md",
62
62
  "CONTRIBUTING.md",
63
- "CHANGELOG.md"
63
+ "CHANGELOG.md",
64
+ "!**/__pycache__",
65
+ "!**/*.pyc"
64
66
  ],
65
67
  "engines": {
66
68
  "node": ">=22.6.0"
@@ -90,11 +92,12 @@
90
92
  "scripts": {
91
93
  "prepare": "husky 2>/dev/null || true; npm run --silent build-bridge",
92
94
  "postinstall": "node scripts/postinstall-chmod.cjs",
93
- "build-bridge": "rm -rf mcp/entwurf-bridge/dist && tsc -p mcp/entwurf-bridge/tsconfig.build.json",
95
+ "build-bridge": "bash scripts/with-dist-lock.sh bash scripts/build-bridge.sh",
94
96
  "prepack": "pnpm --silent run build-bridge",
95
97
  "prepublishOnly": "pnpm run check && pnpm run check-pack-install",
96
98
  "setup": "./run.sh setup",
97
99
  "check-shell-quote": "./run.sh check-shell-quote",
100
+ "check-install-surface": "./run.sh check-install-surface",
98
101
  "check-entwurf-session-identity": "./run.sh check-entwurf-session-identity",
99
102
  "check-meta-session": "./run.sh check-meta-session",
100
103
  "check-meta-record-v2": "./run.sh check-meta-record-v2",
@@ -119,7 +122,7 @@
119
122
  "format": "biome check --write .",
120
123
  "check-meta-manifest-schema": "./run.sh check-meta-manifest-schema",
121
124
  "check-package-source-routing": "./run.sh check-package-source-routing",
122
- "check": "pnpm lint && pnpm typecheck && ./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-record-v2 && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-meta-dual-read && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-migration && ./run.sh check-meta-dual-consumers && ./run.sh check-meta-capability-source && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh smoke-meta-install-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-package-source-routing && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-install-preflight && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-lock && ./run.sh check-entwurf-v2-decider && ./run.sh check-entwurf-v2-matrix && ./run.sh check-entwurf-v2-release && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-send-fallback && ./run.sh check-entwurf-v2-mailbox && ./run.sh check-entwurf-v2-runner && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-boot && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-entwurf-v2-spawn && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-spawn-production && ./run.sh check-entwurf-facts && ./run.sh check-socket-discovery && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-entwurf-mailbox-guard && ./run.sh check-auth-boundary && ./run.sh check-acp-provider-surface && ./run.sh check-acp-sdk-surface && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-acp-carrier-augment && ./run.sh check-pack"
125
+ "check": "pnpm lint && pnpm typecheck && ./run.sh check-shell-quote && ./run.sh check-install-surface && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-record-v2 && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-meta-dual-read && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-migration && ./run.sh check-meta-dual-consumers && ./run.sh check-meta-capability-source && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh smoke-meta-install-state && ./run.sh smoke-agy-install-state && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-package-source-routing && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-install-preflight && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-lock && ./run.sh check-entwurf-v2-decider && ./run.sh check-entwurf-v2-matrix && ./run.sh check-entwurf-v2-release && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-send-fallback && ./run.sh check-entwurf-v2-mailbox && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-runner && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-boot && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-entwurf-v2-spawn && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-spawn-production && ./run.sh check-entwurf-facts && ./run.sh check-socket-discovery && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-entwurf-mailbox-guard && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-agy-sender-identity && ./run.sh check-auth-boundary && ./run.sh check-acp-provider-surface && ./run.sh check-acp-sdk-surface && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-acp-carrier-augment && ./run.sh check-pack"
123
126
  },
124
127
  "pi": {
125
128
  "extensions": [
@@ -132,7 +135,9 @@
132
135
  "bin": {
133
136
  "entwurf": "./run.sh",
134
137
  "entwurf-bridge": "./mcp/entwurf-bridge/start.sh",
135
- "entwurf-statusline": "./scripts/meta-bridge-statusline.sh"
138
+ "entwurf-statusline": "./scripts/meta-bridge-statusline.sh",
139
+ "entwurf-agy-statusline": "./scripts/agy-statusline.sh",
140
+ "entwurf-agy-imprint": "./scripts/agy-imprint.sh"
136
141
  },
137
142
  "peerDependenciesMeta": {
138
143
  "@earendil-works/pi-ai": {
@@ -3,20 +3,34 @@
3
3
  * (SE-1/SE-2 slice 2c). "If I enqueue a conversational reply to this target's mailbox
4
4
  * right now, will a model actually see it — or will it rot as garbage?"
5
5
  *
6
- * Two layers, both pure (facts injected, no IO):
6
+ * Three predicates, all pure (facts injected, no IO) — but on TWO different delivery
7
+ * axes that must never be collapsed into one (보정①):
7
8
  *
8
9
  * - computeMetaReceiverActive(facts): the shared "is this receiver active?" atom —
9
10
  * recordBacked AND ownerAlive AND watchArmed. This is the SAME conjunction the
10
11
  * self-addressability predicate uses for its meta branch; both import it so the
11
12
  * "active receiver" definition has ONE source of truth (concept shared, API split).
13
+ * `watchArmed` is the MAILBOX-ONLY "idle-wake watch armed" signal (meta-session
14
+ * receiver marker) — it belongs to the mailbox axis and nothing else.
12
15
  *
13
16
  * - mailboxConversationalDeliverable(facts): the enqueue gate. A mailbox enqueue +
14
- * doorbell only delivers for a SELF-FETCH backend (Claude Code / Codex / agy): the
15
- * receiver drains its own inbox on wake. A DIRECT-INJECT backend (pi) has no
16
- * mailbox drain at all — enqueuing for it is the SE-1 false success ("✓ delivered"
17
- * into a void). So deliverable = wakeMode === "self-fetch" AND the receiver is
18
- * active. This is the guard that the v1 fallback, MCP v1, pi-native v1, and the v2
19
- * decider/send-fallback enqueue sites must all pass before writing a .msg (slice 2d).
17
+ * doorbell only delivers for a SELF-FETCH backend (Claude Code): the receiver
18
+ * drains its own inbox on wake. A DIRECT-INJECT backend (pi / codex / antigravity)
19
+ * has no mailbox drain at all — enqueuing for it is the SE-1 false success
20
+ * ("✓ delivered" into a void). So deliverable = wakeMode === "self-fetch" AND the
21
+ * receiver is active. This is the guard that the v1 fallback, MCP v1, pi-native v1,
22
+ * and the v2 decider/send-fallback enqueue sites must all pass before writing a
23
+ * .msg (slice 2d).
24
+ *
25
+ * - nativePushDeliverable(facts): the SEPARATE deliverability predicate for a
26
+ * NATIVE-PUSH backend (antigravity). A native-push citizen has no mailbox and no
27
+ * idle-wake watch — delivery is a direct injection into a LIVE app-server
28
+ * conversation the adapter probe located. So deliverable = recordBacked AND
29
+ * probeAlive. It MUST NOT reuse computeMetaReceiverActive: that atom folds in
30
+ * `watchArmed`, so composing it here would smuggle a mailbox liveness fact into a
31
+ * domain that has no mailbox (보정① — native-push replyable ≠ mailbox receiver).
32
+ * The two axes are pinned apart in code so a future replyable-sender path cannot
33
+ * quietly collapse them.
20
34
  *
21
35
  * The contract is "mailboxConversationalDeliverable", NOT a broad "deliverable": it is
22
36
  * specifically about a conversational reply that needs a live doorbell wake, NOT about
@@ -103,8 +117,10 @@ export interface MailboxDeliverabilityResult {
103
117
 
104
118
  /**
105
119
  * The conversational-mailbox enqueue gate. False (no enqueue) unless the backend is
106
- * self-fetch AND the receiver is active. A direct-inject backend (pi) is refused
107
- * outright — it has no mailbox drain, so an enqueue would be a silent false success.
120
+ * self-fetch AND the receiver is active. A direct-inject backend (pi / codex /
121
+ * antigravity) is refused outright — it has no mailbox drain, so an enqueue would be a
122
+ * silent false success. A native-push backend's live delivery goes through
123
+ * nativePushDeliverable, not this gate.
108
124
  */
109
125
  export function mailboxConversationalDeliverable(facts: MailboxDeliverabilityFacts): MailboxDeliverabilityResult {
110
126
  if (facts.wakeMode !== "self-fetch") {
@@ -121,3 +137,40 @@ export function mailboxConversationalDeliverable(facts: MailboxDeliverabilityFac
121
137
  : `self-fetch receiver inactive — ${recv.reason}`,
122
138
  };
123
139
  }
140
+
141
+ // ── native-push deliverability (봉인 6) ──────────────────────────────────────
142
+ // A DISTINCT axis from the mailbox one above. Native-push (antigravity) has no
143
+ // mailbox and no idle-wake watch: delivery is a direct injection into a live
144
+ // app-server conversation the adapter probe found. Kept in this file so the two
145
+ // deliverability predicates sit side by side and their axis separation is visible,
146
+ // but sharing NOTHING with computeMetaReceiverActive (which requires watchArmed).
147
+
148
+ export interface NativePushDeliverabilityFacts {
149
+ /** The target garden id is backed by a live meta-record (identity exists). */
150
+ recordBacked?: boolean;
151
+ /** An adapter probe found the target's live native conversation (route resolved). */
152
+ probeAlive?: boolean;
153
+ }
154
+
155
+ export interface NativePushDeliverabilityResult {
156
+ deliverable: boolean;
157
+ reason: string;
158
+ }
159
+
160
+ /**
161
+ * The native-push deliverability predicate (봉인 6). deliverable ⟺ recordBacked ∧
162
+ * probeAlive. This DELIBERATELY does NOT reuse computeMetaReceiverActive — that atom
163
+ * requires `watchArmed`, the mailbox-only "idle-wake watch armed" signal, which is
164
+ * meaningless for a backend with no mailbox (보정①). Fail-closed: an undefined fact
165
+ * is treated as false, never optimistic; each failure names its own cause so a
166
+ * record-less target is never conflated with a probe that found no live conversation.
167
+ */
168
+ export function nativePushDeliverable(facts: NativePushDeliverabilityFacts): NativePushDeliverabilityResult {
169
+ if (facts.recordBacked !== true) {
170
+ return { deliverable: false, reason: "no backing meta-record" };
171
+ }
172
+ if (facts.probeAlive !== true) {
173
+ return { deliverable: false, reason: "adapter probe found no live native conversation" };
174
+ }
175
+ return { deliverable: true, reason: "record backed, native conversation probed alive" };
176
+ }