@junghanacs/entwurf 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +248 -0
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +14 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +76 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +9 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +11 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +66 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +10 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +8 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +9 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +25 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +14 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +24 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +61 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +64 -4
- package/mcp/entwurf-bridge/src/index.ts +16 -8
- package/package.json +3 -2
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +3 -1
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +13 -8
- package/pi-extensions/lib/entwurf-deliverability.ts +104 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +20 -6
- package/pi-extensions/lib/entwurf-facts.ts +52 -3
- package/pi-extensions/lib/entwurf-peer-observe.ts +77 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +12 -1
- package/pi-extensions/lib/entwurf-v2-decider.ts +26 -8
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +9 -2
- package/pi-extensions/lib/entwurf-v2-production.ts +32 -12
- package/pi-extensions/lib/entwurf-v2-runner.ts +23 -6
- package/pi-extensions/lib/entwurf-v2-send.ts +26 -3
- package/pi-extensions/lib/entwurf-v2-surface.ts +24 -3
- package/pi-extensions/lib/meta-session.ts +76 -7
- package/pi-extensions/meta-bridge-hook.ts +72 -3
- package/run.sh +21 -0
- package/scripts/check-bridge-delivery.ts +42 -1
- package/scripts/check-entwurf-fact-provider.ts +22 -1
- package/scripts/check-entwurf-facts.ts +17 -2
- package/scripts/check-entwurf-peers-surface.ts +20 -2
- package/scripts/check-entwurf-self-address.ts +20 -2
- package/scripts/check-entwurf-v2-decider.ts +6 -3
- package/scripts/check-entwurf-v2-mailbox.ts +33 -0
- package/scripts/check-entwurf-v2-matrix.ts +10 -5
- package/scripts/check-entwurf-v2-production.ts +72 -1
- package/scripts/check-entwurf-v2-runner.ts +18 -0
- package/scripts/check-entwurf-v2-send.ts +13 -1
- package/scripts/check-entwurf-v2-surface.ts +75 -3
- package/scripts/check-gate-qualification.ts +1 -0
- package/scripts/check-hook-launch-topology.ts +70 -1
- package/scripts/check-mailbox-receipt-state.ts +6 -0
- package/scripts/check-meta-doctor-oracle.sh +73 -0
- package/scripts/check-meta-hook-session-switch.ts +560 -0
- package/scripts/check-meta-mailbox-state-write.ts +9 -2
- package/scripts/check-meta-manifest-schema.py +19 -1
- package/scripts/check-meta-session.ts +10 -2
- package/scripts/meta-bridge-doctor.sh +47 -7
- package/scripts/meta-bridge-state.py +23 -5
- package/scripts/meta-bridge-statusline.sh +65 -2
- package/scripts/mutants/meta-hook-session-switch.json +223 -0
- package/scripts/raw-async-delivery/README.md +280 -9
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +6 -2
- package/scripts/raw-async-delivery/lab-statusline.sh +63 -0
- package/scripts/raw-async-delivery/mailbox-watch.py +230 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +3 -1
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +13 -4
- package/scripts/raw-async-delivery/probe-delivery-transparency.sh +387 -0
- package/scripts/raw-claude-session-switch/README.md +170 -0
- package/scripts/smoke-acp-cortex-live.ts +22 -2
- package/scripts/smoke-acp-v2-send-live.ts +23 -2
- package/scripts/smoke-entwurf-chain-live.ts +85 -13
- package/scripts/smoke-entwurf-v2-matrix-live.ts +23 -1
- package/scripts/smoke-meta-async-drift.sh +31 -8
- package/scripts/smoke-meta-install-state.sh +170 -11
- package/scripts/smoke-meta-keyset-guard.sh +4 -1
- package/scripts/smoke-mux-fresh-call-live.ts +13 -3
|
@@ -54,7 +54,15 @@ import fsp from "node:fs/promises";
|
|
|
54
54
|
import os from "node:os";
|
|
55
55
|
import path from "node:path";
|
|
56
56
|
import { fileURLToPath } from "node:url";
|
|
57
|
-
import {
|
|
57
|
+
import { resolveMailboxReceiverFacts } from "../pi-extensions/lib/entwurf-deliverability.ts";
|
|
58
|
+
import {
|
|
59
|
+
readMetaInbox,
|
|
60
|
+
readMetaReceiverMarker,
|
|
61
|
+
readMetaSenderMarker,
|
|
62
|
+
upsertMetaSession,
|
|
63
|
+
writeMetaReceiverMarker,
|
|
64
|
+
writeMetaSenderMarker,
|
|
65
|
+
} from "../pi-extensions/lib/meta-session.ts";
|
|
58
66
|
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
59
67
|
import { skipLive } from "./lib/live-skip.ts";
|
|
60
68
|
|
|
@@ -155,11 +163,13 @@ async function main(): Promise<void> {
|
|
|
155
163
|
const sessionsDir = path.join(world, "sessions");
|
|
156
164
|
const mailboxDir = path.join(world, "mailbox");
|
|
157
165
|
const receiversDir = path.join(world, "receivers");
|
|
158
|
-
|
|
166
|
+
const sendersDir = path.join(world, "senders");
|
|
167
|
+
for (const d of [sessionsDir, mailboxDir, receiversDir, sendersDir]) await fsp.mkdir(d, { recursive: true });
|
|
159
168
|
const worldEnv = {
|
|
160
169
|
ENTWURF_META_SESSIONS_DIR: sessionsDir,
|
|
161
170
|
ENTWURF_META_MAILBOX_DIR: mailboxDir,
|
|
162
171
|
ENTWURF_META_RECEIVERS_DIR: receiversDir,
|
|
172
|
+
ENTWURF_META_SENDERS_DIR: sendersDir,
|
|
163
173
|
};
|
|
164
174
|
// The MCP bridge prefers PI_SESSION_ID+PI_AGENT_ID over a meta-sender marker when both
|
|
165
175
|
// are present. A runner that is itself a pi --entwurf-control session (release-gate,
|
|
@@ -183,20 +193,46 @@ async function main(): Promise<void> {
|
|
|
183
193
|
dir: sessionsDir,
|
|
184
194
|
});
|
|
185
195
|
const gidD = terminus.record.gardenId;
|
|
186
|
-
writeMetaReceiverMarker({
|
|
187
|
-
gardenId: gidD,
|
|
188
|
-
backend: "claude-code",
|
|
189
|
-
nativeSessionId: terminus.record.nativeSessionId,
|
|
190
|
-
ownerPid: process.pid,
|
|
191
|
-
armProvenance: "session-start",
|
|
192
|
-
receiversDir,
|
|
193
|
-
});
|
|
194
|
-
console.error(`[smoke-entwurf-chain-live] D: ${gidD} (mailbox terminus)`);
|
|
195
|
-
|
|
196
196
|
const children: ChildProcess[] = [];
|
|
197
|
+
let terminusOwner: ChildProcess | null = null;
|
|
197
198
|
let streamB = "";
|
|
198
199
|
let streamC = "";
|
|
199
200
|
try {
|
|
201
|
+
// The receiver marker alone is no longer the whole fixture (#101): a `claude-code-cli` watch
|
|
202
|
+
// owner is deliverable only while ITS OWN sender marker still names the garden it is armed
|
|
203
|
+
// for — that join is what refuses a session which switched away in place. A receiver marker
|
|
204
|
+
// with no sender marker beside it models a state the product now treats as retired, so this
|
|
205
|
+
// smoke would fail on its own fixture instead of on the rail it exists to prove.
|
|
206
|
+
//
|
|
207
|
+
// THE OWNER IS A SPAWNED IDLE PROCESS, NOT THIS ONE, and that is specific to this smoke:
|
|
208
|
+
// hop 1 is a native `claude` child of THIS process, and the bridge resolves a sender's
|
|
209
|
+
// identity through its ancestry. A terminus marker under this pid therefore puts TWO garden
|
|
210
|
+
// citizens on one host process, which the bridge refuses outright as ambiguous sender
|
|
211
|
+
// identity — measured on oracle 2026-09-04, where hop 1 came back "refused: ambiguous sender
|
|
212
|
+
// identity (…-f34713, …-912b42)" and the chain never started. The sibling smokes keep
|
|
213
|
+
// `process.pid` because their children are pi/ACP sessions carrying their own identity, or
|
|
214
|
+
// tmux siblings that are not descendants at all.
|
|
215
|
+
terminusOwner = spawn(process.execPath, ["-e", "setTimeout(() => {}, 900000)"], { stdio: "ignore" });
|
|
216
|
+
const terminusOwnerPid = terminusOwner.pid;
|
|
217
|
+
if (typeof terminusOwnerPid !== "number") throw new Error("could not spawn the terminus owner process");
|
|
218
|
+
writeMetaReceiverMarker({
|
|
219
|
+
gardenId: gidD,
|
|
220
|
+
backend: "claude-code",
|
|
221
|
+
nativeSessionId: terminus.record.nativeSessionId,
|
|
222
|
+
ownerPid: terminusOwnerPid,
|
|
223
|
+
armProvenance: "session-start",
|
|
224
|
+
receiversDir,
|
|
225
|
+
});
|
|
226
|
+
writeMetaSenderMarker({
|
|
227
|
+
backend: "claude-code",
|
|
228
|
+
gardenId: gidD,
|
|
229
|
+
nativeSessionId: terminus.record.nativeSessionId,
|
|
230
|
+
cwd: world,
|
|
231
|
+
ownerPid: terminusOwnerPid,
|
|
232
|
+
sendersDir,
|
|
233
|
+
});
|
|
234
|
+
console.error(`[smoke-entwurf-chain-live] D: ${gidD} (mailbox terminus)`);
|
|
235
|
+
|
|
200
236
|
// ── C: the ACP Claude Sonnet citizen (spawned first so B can be told its id) ──
|
|
201
237
|
const known = new Set((await readRecords(sessionsDir)).map((r) => r.gardenId));
|
|
202
238
|
const c = spawn(
|
|
@@ -283,6 +319,21 @@ async function main(): Promise<void> {
|
|
|
283
319
|
ok("A is distinct from every other citizen", !new Set([gidB, gidC, gidD]).has(gidA as string));
|
|
284
320
|
console.error(`[smoke-entwurf-chain-live] A: ${gidA} (native Claude Code)`);
|
|
285
321
|
|
|
322
|
+
// ── the fixture must still be a deliverable citizen when the chain arrives ──
|
|
323
|
+
// The terminus is only reachable while its owner is alive AND that owner's sender marker
|
|
324
|
+
// still names it (#101). Both are fixture state this smoke owns, and both can decay while
|
|
325
|
+
// three model turns run. Asserting it here separates "the chain did not arrive" from "the
|
|
326
|
+
// target stopped being addressable", which cost a LIVE run to tell apart once.
|
|
327
|
+
const terminusFacts = resolveMailboxReceiverFacts(terminus.record, {
|
|
328
|
+
readReceiverMarker: (gardenId: string) => readMetaReceiverMarker({ gardenId, receiversDir }),
|
|
329
|
+
readSenderMarker: (backend: string, pid: number) =>
|
|
330
|
+
readMetaSenderMarker({ backend: backend as "claude-code", ownerPid: pid, sendersDir }),
|
|
331
|
+
});
|
|
332
|
+
ok(
|
|
333
|
+
"fixture: the terminus is a deliverable citizen at the moment the chain starts",
|
|
334
|
+
terminusFacts.ownerAlive && terminusFacts.watchArmed,
|
|
335
|
+
);
|
|
336
|
+
|
|
286
337
|
// ── the chain runs on its own from here; wait for the terminus ───────────
|
|
287
338
|
const boxDir = path.join(mailboxDir, gidD);
|
|
288
339
|
const until = Date.now() + CHAIN_TIMEOUT_MS;
|
|
@@ -295,7 +346,25 @@ async function main(): Promise<void> {
|
|
|
295
346
|
await sleep(POLL_MS);
|
|
296
347
|
}
|
|
297
348
|
if (msgs.length === 0) {
|
|
298
|
-
|
|
349
|
+
let ownerAlive = false;
|
|
350
|
+
try {
|
|
351
|
+
process.kill(terminusOwnerPid, 0);
|
|
352
|
+
ownerAlive = true;
|
|
353
|
+
} catch {}
|
|
354
|
+
const facts = resolveMailboxReceiverFacts(terminus.record, {
|
|
355
|
+
readReceiverMarker: (gardenId: string) => readMetaReceiverMarker({ gardenId, receiversDir }),
|
|
356
|
+
readSenderMarker: (backend: string, pid: number) =>
|
|
357
|
+
readMetaSenderMarker({ backend: backend as "claude-code", ownerPid: pid, sendersDir }),
|
|
358
|
+
});
|
|
359
|
+
console.error(
|
|
360
|
+
`[smoke-entwurf-chain-live] terminus fixture at timeout: ownerPid=${terminusOwnerPid} alive=${ownerAlive} ` +
|
|
361
|
+
`ownerAlive=${facts.ownerAlive} watchArmed=${facts.watchArmed}`,
|
|
362
|
+
);
|
|
363
|
+
// The FULL turn, not a 1500-char head: that slice cut the JSON before its `result`
|
|
364
|
+
// field, so the one line that says what A actually did — the tool outcome it was told
|
|
365
|
+
// to echo back — was the first thing this diagnostic dropped. A failure that hides its
|
|
366
|
+
// own cause costs another LIVE run to re-learn.
|
|
367
|
+
console.error(`[smoke-entwurf-chain-live] A turn output:\n${claudeOut}`);
|
|
299
368
|
console.error(`[smoke-entwurf-chain-live] B stream tail:\n${streamB.slice(-2000)}`);
|
|
300
369
|
console.error(`[smoke-entwurf-chain-live] C stream tail:\n${streamC.slice(-2000)}`);
|
|
301
370
|
}
|
|
@@ -350,6 +419,9 @@ async function main(): Promise<void> {
|
|
|
350
419
|
console.error(`[smoke-entwurf-chain-live] chain: ${gidA} → ${gidB} → ${gidC} → ${gidD}`);
|
|
351
420
|
} finally {
|
|
352
421
|
for (const ch of children) await terminateChild(ch).catch(() => undefined);
|
|
422
|
+
try {
|
|
423
|
+
terminusOwner?.kill("SIGTERM");
|
|
424
|
+
} catch {}
|
|
353
425
|
}
|
|
354
426
|
|
|
355
427
|
console.log(
|
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
metaRecordExistsByGardenId,
|
|
63
63
|
upsertMetaSession,
|
|
64
64
|
writeMetaReceiverMarker,
|
|
65
|
+
writeMetaSenderMarker,
|
|
65
66
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
66
67
|
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
67
68
|
import { skipLive } from "./lib/live-skip.ts";
|
|
@@ -187,10 +188,15 @@ async function main(): Promise<void> {
|
|
|
187
188
|
const mailboxDir = path.join(tmp, "mailbox");
|
|
188
189
|
const lockDir = path.join(tmp, "locks");
|
|
189
190
|
const receiversDir = path.join(tmp, "receivers");
|
|
190
|
-
|
|
191
|
+
const sendersDir = path.join(tmp, "senders");
|
|
192
|
+
for (const d of [sessionsDir, mailboxDir, lockDir, receiversDir, sendersDir]) await fsp.mkdir(d, { recursive: true });
|
|
191
193
|
// The receiver marker is read through defaultMetaReceiversDir() (env-overridable); point it
|
|
192
194
|
// (and the store/mailbox defaults, belt-and-suspenders to the opts dirs) at the temp world.
|
|
195
|
+
// The SENDER dir joins that list for #101: deliverability now also reads the receiver owner's
|
|
196
|
+
// sender marker, and leaving this one pointed at the operator's real root would have the
|
|
197
|
+
// fixture judged against markers this smoke never wrote.
|
|
193
198
|
process.env.ENTWURF_META_RECEIVERS_DIR = receiversDir;
|
|
199
|
+
process.env.ENTWURF_META_SENDERS_DIR = sendersDir;
|
|
194
200
|
process.env.ENTWURF_META_SESSIONS_DIR = sessionsDir;
|
|
195
201
|
process.env.ENTWURF_META_MAILBOX_DIR = mailboxDir;
|
|
196
202
|
|
|
@@ -341,6 +347,14 @@ async function main(): Promise<void> {
|
|
|
341
347
|
const gid = minted.record.gardenId;
|
|
342
348
|
artifacts["C2.gid"] = gid;
|
|
343
349
|
// armed = a presence marker owned by THIS (live) process → deliverable.
|
|
350
|
+
//
|
|
351
|
+
// The receiver marker alone is no longer the whole fixture (#101): a `claude-code-cli`
|
|
352
|
+
// watch owner is deliverable only while ITS OWN sender marker still names the garden it
|
|
353
|
+
// is armed for — that join is what refuses a session which switched away in place. A
|
|
354
|
+
// receiver marker with no sender marker beside it models a state the product now treats
|
|
355
|
+
// as retired, so this smoke would fail on its own fixture instead of on the rail it
|
|
356
|
+
// exists to prove. Same shape as smoke-mux-lifecycle-live / smoke-omp-fresh-live, which
|
|
357
|
+
// already seeded both.
|
|
344
358
|
writeMetaReceiverMarker({
|
|
345
359
|
gardenId: gid,
|
|
346
360
|
backend: "claude-code",
|
|
@@ -349,6 +363,14 @@ async function main(): Promise<void> {
|
|
|
349
363
|
armProvenance: "session-start",
|
|
350
364
|
receiversDir,
|
|
351
365
|
});
|
|
366
|
+
writeMetaSenderMarker({
|
|
367
|
+
backend: "claude-code",
|
|
368
|
+
gardenId: gid,
|
|
369
|
+
nativeSessionId: minted.record.nativeSessionId,
|
|
370
|
+
cwd: tmp,
|
|
371
|
+
ownerPid: process.pid,
|
|
372
|
+
sendersDir,
|
|
373
|
+
});
|
|
352
374
|
|
|
353
375
|
const result: EntwurfV2RunResult = await runEntwurfV2(
|
|
354
376
|
{ target: gid, intent: "fire-and-forget", message: "matrix-live C2 meta-mailbox probe" },
|
|
@@ -6,13 +6,24 @@
|
|
|
6
6
|
#
|
|
7
7
|
# WHY THIS EXISTS
|
|
8
8
|
# The whole Claude async-delivery path rides on UNDOCUMENTED Claude Code behavior:
|
|
9
|
-
# `asyncRewake`
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
9
|
+
# `asyncRewake` names the operator row and the model prefix from `rewakeSummary` /
|
|
10
|
+
# `rewakeMessage` (both read from a local plugin's hooks.json, both @internal, and
|
|
11
|
+
# the product doorbell now DEPENDS on both); the doorbell writes its notice on
|
|
12
|
+
# stderr; `watchPaths` can arm from only 3 hook events (SessionStart / CwdChanged /
|
|
13
|
+
# FileChanged). Half the Gotchas in scripts/raw-async-delivery/README.md are
|
|
14
|
+
# reverse-engineered. Claude ships ~weekly. So the path can break SILENTLY on any
|
|
15
|
+
# upgrade. This gate makes it SCREAM instead — direct lineage of the 0.8.x fail-fast
|
|
16
|
+
# tool-surface gates ("don't let a surface break quietly, make the gate cry out").
|
|
17
|
+
#
|
|
18
|
+
# THIS PARAGRAPH USED TO BE WRONG, AND THAT IS THE POINT (#98, corrected
|
|
19
|
+
# 2026-09-03). It said `asyncRewake` "force-prepends `Stop hook feedback:` and
|
|
20
|
+
# ignores `rewakeMessage`", and that the payload channel was "stderr-only". Both are
|
|
21
|
+
# FALSE — measured on 2.1.236/2.1.258/2.1.259: `rewakeMessage` REPLACES the model
|
|
22
|
+
# prefix, `rewakeSummary` REPLACES the operator row, and the model body is
|
|
23
|
+
# `stderr || stdout`. The marker strings below stayed green the whole time, because
|
|
24
|
+
# a marker pins a STRING and cannot see the sentence next to it rot. Nobody tried
|
|
25
|
+
# the two fields for months on the strength of this header. Receipts:
|
|
26
|
+
# scripts/raw-async-delivery/README.md "Inherited facts corrected".
|
|
16
27
|
#
|
|
17
28
|
# TWO TIERS (mirrors smoke-compaction-policy: deterministic default + LIVE=1 add-on)
|
|
18
29
|
# DETERMINISTIC (default; free, offline, CI/pre-commit safe):
|
|
@@ -70,7 +81,19 @@ MARKERS=(
|
|
|
70
81
|
flushPendingAsyncRewakeHooks # engine turn-boundary flush (edge-bound delivery)
|
|
71
82
|
CLAUDE_CODE_STOP_HOOK_BLOCK_CAP # native re-wake cap
|
|
72
83
|
FileChanged # idle-wake event
|
|
73
|
-
|
|
84
|
+
# rewakeMessage/rewakeSummary — CORRECTED 2026-09-03 (#98 Phase 1). The comment here used
|
|
85
|
+
# to read "the field asyncRewake IGNORES (doorbell-only proof)". That was FALSE, and it is
|
|
86
|
+
# the reason nobody tried the field for months: measured on 2.1.236/2.1.258/2.1.259, BOTH
|
|
87
|
+
# are read from a local plugin's hooks.json with no first-party gate. `rewakeMessage`
|
|
88
|
+
# REPLACES the model-facing `Stop hook blocking error from command "…":` prefix;
|
|
89
|
+
# `rewakeSummary` REPLACES the operator-visible row (default `Stop hook feedback`). Only
|
|
90
|
+
# the stdout-JSON form of rewakeSummary is first-party gated. The marker string was right
|
|
91
|
+
# while the reason next to it was dead — which is why these two are pinned HERE: the
|
|
92
|
+
# product doorbell now DEPENDS on both (pi/meta-bridge/.../hooks/hooks.json), the fields
|
|
93
|
+
# are @internal, and if either disappears the surface silently reverts to
|
|
94
|
+
# `Stop hook feedback` + "blocking error" with no other gate to notice.
|
|
95
|
+
rewakeMessage # model-facing prefix replacement (product depends on it)
|
|
96
|
+
rewakeSummary # operator-facing row replacement (product depends on it)
|
|
74
97
|
hookSpecificOutput # the watchPaths emit envelope
|
|
75
98
|
CwdChanged # the 2nd of the 3 watch-arming events
|
|
76
99
|
)
|
|
@@ -317,6 +317,69 @@ def structural_cells():
|
|
|
317
317
|
assert (broken, settings) == before
|
|
318
318
|
|
|
319
319
|
cell("", "retired/managed sets stay disjoint and a corrupt key ledger fails closed", structural_cells)
|
|
320
|
+
|
|
321
|
+
def compaction_cells():
|
|
322
|
+
# #94 — the two compaction keys join the retired set. They exercise two axes
|
|
323
|
+
# the skipDangerous cells above never touch: a NESTED path and a STRING
|
|
324
|
+
# scalar. Both matter, because relinquish compares `type(value) is
|
|
325
|
+
# type(last_managed_value)` and walks the path through get_nested.
|
|
326
|
+
for name, path, last in [
|
|
327
|
+
("autoCompactEnabled", ["autoCompactEnabled"], False),
|
|
328
|
+
("env.DISABLE_AUTOCOMPACT", ["env", "DISABLE_AUTOCOMPACT"], "1"),
|
|
329
|
+
]:
|
|
330
|
+
def build(current=MISSING, old=MISSING):
|
|
331
|
+
settings = {}
|
|
332
|
+
if current is not MISSING:
|
|
333
|
+
node = settings
|
|
334
|
+
for key in path[:-1]:
|
|
335
|
+
node = node.setdefault(key, {})
|
|
336
|
+
node[path[-1]] = current
|
|
337
|
+
keys = {} if old is MISSING else {name: copy.deepcopy(old)}
|
|
338
|
+
return {"files": {"settings": {"keys": keys}}}, settings
|
|
339
|
+
|
|
340
|
+
def led(existed, value):
|
|
341
|
+
return {"kind": "scalar", "path": path, "original": {"existed": existed, "value": value}}
|
|
342
|
+
|
|
343
|
+
def call(current=MISSING, old=MISSING):
|
|
344
|
+
state, settings = build(current, old)
|
|
345
|
+
m.relinquish_retired_scalar(state, settings, name, path, last)
|
|
346
|
+
return state, settings
|
|
347
|
+
|
|
348
|
+
# (a) still carrying what entwurf last managed, with provenance -> the
|
|
349
|
+
# pre-entwurf snapshot is restored, and the entry is consumed.
|
|
350
|
+
state, settings = call(last, led(True, last))
|
|
351
|
+
assert m.get_nested(settings, path) == (True, last), (name, settings)
|
|
352
|
+
assert name not in state["files"]["settings"]["keys"], name
|
|
353
|
+
# (a') the same, where the operator had NOTHING before entwurf: restoring
|
|
354
|
+
# the snapshot must DELETE the key rather than leave our value.
|
|
355
|
+
state, settings = call(last, led(False, None))
|
|
356
|
+
assert m.get_nested(settings, path)[0] is False, (name, settings)
|
|
357
|
+
# (b) the operator has since changed it -> already theirs, untouched.
|
|
358
|
+
other = True if last is False else "0"
|
|
359
|
+
state, settings = call(other, led(True, last))
|
|
360
|
+
assert m.get_nested(settings, path) == (True, other), (name, settings)
|
|
361
|
+
assert name not in state["files"]["settings"]["keys"], name
|
|
362
|
+
# (c) no ledger entry at all (fresh install) -> untouched, and a fresh
|
|
363
|
+
# install never creates one because the key is not managed any more.
|
|
364
|
+
for current in [last, other, MISSING]:
|
|
365
|
+
state, settings = call(current)
|
|
366
|
+
expected = (False, None) if current is MISSING else (True, current)
|
|
367
|
+
assert m.get_nested(settings, path) == expected, (name, current, settings)
|
|
368
|
+
# Type strictness, where a lookalike EXISTS. For the bool axis that is `0`
|
|
369
|
+
# (`0 == False` is True while `type(0) is not bool`) — NOT `1`, which is
|
|
370
|
+
# simply unequal to False and would pass the cell without the type guard
|
|
371
|
+
# ever being consulted. The string axis has no lookalike at all: no
|
|
372
|
+
# non-str value equals `"1"`, so equality alone already refuses and there
|
|
373
|
+
# is nothing for a type check to add. Asserted only where it can fail.
|
|
374
|
+
if last is False:
|
|
375
|
+
state, settings = call(0, led(True, last))
|
|
376
|
+
assert m.get_nested(settings, path) == (True, 0), (name, settings)
|
|
377
|
+
|
|
378
|
+
cell(
|
|
379
|
+
"",
|
|
380
|
+
"#94 compaction keys relinquish across nested/string axes: restore, delete, operator-changed, fresh, type-strict",
|
|
381
|
+
compaction_cells,
|
|
382
|
+
)
|
|
320
383
|
sys.exit(failed)
|
|
321
384
|
PY
|
|
322
385
|
then ok "#71 retirement leaf: provenance, bool strictness, malformed refusal, one-shot convergence"
|
|
@@ -339,10 +402,12 @@ assert stat.S_IMODE(os.stat(path).st_mode) == 0o600
|
|
|
339
402
|
s=json.load(open(path))
|
|
340
403
|
assert s['files']['settings']['keys']['enabledPlugins.entwurf-meta-receive@meta-bridge-local']['original']['value'] is False
|
|
341
404
|
assert s['files']['settings']['keys']['cleanupPeriodDays']['original']['value'] == 30
|
|
342
|
-
assert s['files']['settings']['keys']['env.DISABLE_AUTOCOMPACT']['original']['value'] == '0'
|
|
343
405
|
assert s['files']['settings']['keys']['statusLine']['original']['value']['command'] == '/old/user/statusline.sh'
|
|
344
406
|
assert s['files']['settings']['keys']['promptSuggestionEnabled']['original']['value'] is True
|
|
345
|
-
|
|
407
|
+
# #94 case (c) — a FRESH install never captures a retired key, so entwurf holds no
|
|
408
|
+
# ownership evidence for compaction and uninstall can never restore a suppression.
|
|
409
|
+
assert 'autoCompactEnabled' not in s['files']['settings']['keys']
|
|
410
|
+
assert 'env.DISABLE_AUTOCOMPACT' not in s['files']['settings']['keys']
|
|
346
411
|
assert 'skipDangerousModePermissionPrompt' not in s['files']['settings']['keys']
|
|
347
412
|
assert s['files']['claudeRoot']['keys']['mcpServers.entwurf-bridge']['original']['value']['command'] == 'old'
|
|
348
413
|
PY
|
|
@@ -360,6 +425,21 @@ state['files']['settings']['keys']['skipDangerousModePermissionPrompt']={
|
|
|
360
425
|
'kind':'scalar', 'path':['skipDangerousModePermissionPrompt'],
|
|
361
426
|
'original':{'existed':True, 'value':False}
|
|
362
427
|
}
|
|
428
|
+
# #94 — the same upgrade shape for the two compaction keys, planted at DRIVE level
|
|
429
|
+
# so apply() itself is the subject, not the leaf. Two branches that the unit cells
|
|
430
|
+
# cannot reach through the real ledger: a NESTED path whose original must come
|
|
431
|
+
# back, and a key whose original never existed and must therefore be DELETED
|
|
432
|
+
# rather than left carrying entwurf's value.
|
|
433
|
+
settings['autoCompactEnabled']=False
|
|
434
|
+
state['files']['settings']['keys']['autoCompactEnabled']={
|
|
435
|
+
'kind':'scalar', 'path':['autoCompactEnabled'],
|
|
436
|
+
'original':{'existed':False, 'value':None}
|
|
437
|
+
}
|
|
438
|
+
settings.setdefault('env',{})['DISABLE_AUTOCOMPACT']='1'
|
|
439
|
+
state['files']['settings']['keys']['env.DISABLE_AUTOCOMPACT']={
|
|
440
|
+
'kind':'scalar', 'path':['env','DISABLE_AUTOCOMPACT'],
|
|
441
|
+
'original':{'existed':True, 'value':'0'}
|
|
442
|
+
}
|
|
363
443
|
json.dump(settings, open(sp,'w'), indent=2); open(sp,'a').write('\n')
|
|
364
444
|
json.dump(state, open(stp,'w'), indent=2); open(stp,'a').write('\n')
|
|
365
445
|
PY
|
|
@@ -373,14 +453,26 @@ assert settings['enabledPlugins']['entwurf-meta-receive@meta-bridge-local'] is T
|
|
|
373
453
|
assert settings['enabledPlugins']['keep@user'] is True
|
|
374
454
|
assert settings['extraKnownMarketplaces']['meta-bridge-local']['source']['path'] == os.environ['ASM']
|
|
375
455
|
assert settings['cleanupPeriodDays'] == 365
|
|
376
|
-
|
|
456
|
+
# #94 - retired: apply neither writes nor rewrites it. The fixture's operator
|
|
457
|
+
# value ('0') survives untouched, and the scalar is never introduced at all.
|
|
458
|
+
assert settings['env']['DISABLE_AUTOCOMPACT'] == '0'
|
|
459
|
+
assert 'autoCompactEnabled' not in settings
|
|
377
460
|
assert settings['env']['KEEP_ME'] == 'yes'
|
|
378
461
|
assert settings['statusLine']['command'] == os.environ['REPO'] + '/scripts/meta-bridge-statusline.sh'
|
|
379
|
-
for key in ['promptSuggestionEnabled','awaySummaryEnabled','autoMemoryEnabled','verbose','
|
|
462
|
+
for key in ['promptSuggestionEnabled','awaySummaryEnabled','autoMemoryEnabled','verbose','showTurnDuration','terminalProgressBarEnabled','useAutoModeDuringPlan','enableWorkflows','workflowKeywordTriggerEnabled']:
|
|
380
463
|
assert settings[key] is False, key
|
|
381
464
|
assert settings['skipDangerousModePermissionPrompt'] is False
|
|
465
|
+
# #94 drive-level relinquishment, both branches:
|
|
466
|
+
# nested path -> the operator's pre-entwurf '0' is back, and the env map that
|
|
467
|
+
# carries it survives with the neighbour key untouched.
|
|
468
|
+
# absent original -> the key is REMOVED, not left holding entwurf's False.
|
|
469
|
+
assert settings['env']['DISABLE_AUTOCOMPACT'] == '0'
|
|
470
|
+
assert settings['env']['KEEP_ME'] == 'yes'
|
|
471
|
+
assert 'autoCompactEnabled' not in settings
|
|
382
472
|
state=json.load(open(cfg + '/entwurf.install-state.json'))
|
|
383
473
|
assert 'skipDangerousModePermissionPrompt' not in state['files']['settings']['keys']
|
|
474
|
+
assert 'autoCompactEnabled' not in state['files']['settings']['keys']
|
|
475
|
+
assert 'env.DISABLE_AUTOCOMPACT' not in state['files']['settings']['keys']
|
|
384
476
|
for item in ['Bash','Read','Write','Edit','Grep','Glob','WebFetch','WebSearch','Skill','mcp__entwurf-bridge__*']:
|
|
385
477
|
assert item in settings['permissions']['allow'], item
|
|
386
478
|
assert settings['permissions']['allow'].count('Read') == 1
|
|
@@ -455,7 +547,6 @@ import json, os
|
|
|
455
547
|
s=json.load(open(os.environ['CLAUDE_CONFIG_DIR'] + '/entwurf.install-state.json'))
|
|
456
548
|
assert s['files']['settings']['keys']['enabledPlugins.entwurf-meta-receive@meta-bridge-local']['original']['value'] is False
|
|
457
549
|
assert s['files']['settings']['keys']['cleanupPeriodDays']['original']['value'] == 30
|
|
458
|
-
assert s['files']['settings']['keys']['env.DISABLE_AUTOCOMPACT']['original']['value'] == '0'
|
|
459
550
|
assert s['files']['settings']['keys']['statusLine']['original']['value']['command'] == '/old/user/statusline.sh'
|
|
460
551
|
assert s['files']['settings']['keys']['promptSuggestionEnabled']['original']['value'] is True
|
|
461
552
|
PY
|
|
@@ -479,13 +570,18 @@ root=json.load(open(os.environ['HOME'] + '/.claude.json'))
|
|
|
479
570
|
assert settings['cleanupPeriodDays'] == 30
|
|
480
571
|
assert settings['enabledPlugins']['entwurf-meta-receive@meta-bridge-local'] is False
|
|
481
572
|
assert 'meta-bridge-local' not in settings.get('extraKnownMarketplaces', {})
|
|
573
|
+
# #94 - still the operator's '0'. NOT restored by uninstall (we never owned it):
|
|
574
|
+
# never written in the first place.
|
|
482
575
|
assert settings['env']['DISABLE_AUTOCOMPACT'] == '0'
|
|
483
576
|
assert settings['env']['KEEP_ME'] == 'yes'
|
|
484
577
|
assert settings['statusLine']['command'] == '/old/user/statusline.sh'
|
|
485
578
|
assert settings['promptSuggestionEnabled'] is True
|
|
486
579
|
assert settings['skipDangerousModePermissionPrompt'] is False
|
|
487
580
|
assert settings['showTurnDuration'] is True
|
|
488
|
-
|
|
581
|
+
# One list, one meaning: these are the keys UNINSTALL removed. `autoCompactEnabled`
|
|
582
|
+
# is not among them post-#94 — that entwurf never introduced it is a different
|
|
583
|
+
# claim, pinned on the post-install assertion above.
|
|
584
|
+
for key in ['awaySummaryEnabled','autoMemoryEnabled','verbose','terminalProgressBarEnabled','useAutoModeDuringPlan','enableWorkflows','workflowKeywordTriggerEnabled']:
|
|
489
585
|
assert key not in settings, key
|
|
490
586
|
allow=settings['permissions']['allow']
|
|
491
587
|
deny=settings['permissions']['deny']
|
|
@@ -667,7 +763,8 @@ s=json.load(open(os.environ['CLAUDE_CONFIG_DIR'] + '/entwurf.install-state.json'
|
|
|
667
763
|
assert s['files']['settings']['keys']['enabledPlugins.entwurf-meta-receive@meta-bridge-local']['original']['existed'] is False
|
|
668
764
|
assert s['files']['settings']['keys']['extraKnownMarketplaces.meta-bridge-local']['original']['existed'] is False
|
|
669
765
|
assert s['files']['claudeRoot']['keys']['mcpServers.entwurf-bridge']['original']['existed'] is False
|
|
670
|
-
|
|
766
|
+
# #94 - retired: prepare captures no ownership evidence for it any more.
|
|
767
|
+
assert 'env.DISABLE_AUTOCOMPACT' not in s['files']['settings']['keys']
|
|
671
768
|
PY
|
|
672
769
|
then ok "legacy exact plugin/marketplace/MCP migrate as pi-owned absent, policy keys remain user-owned"; else bad "legacy migration state check failed"; fi
|
|
673
770
|
py uninstall >/dev/null
|
|
@@ -706,18 +803,80 @@ valid_record "20260606T000001-bbbbbb" "native-b" > "$STORE/20260606T000001-bbbbb
|
|
|
706
803
|
STATUS_INPUT_MATCH='{"session_id":"native-a","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-sonnet-5"},"context_window":{"context_window_size":200000,"used_percentage":2,"current_usage":{"input_tokens":10}}}'
|
|
707
804
|
STATUS_INPUT_MISS='{"session_id":"native-missing","workspace":{"current_dir":"/tmp"},"model":{"id":"claude-opus-5"}}'
|
|
708
805
|
STATUS_INPUT_READY='{"workspace":{"current_dir":"/tmp"},"model":{"id":"claude-haiku-4-5"}}'
|
|
709
|
-
|
|
806
|
+
# Pin the mailbox root on EVERY statusline render below, not just the badge cells: the
|
|
807
|
+
# #98 B badge reads it, so an unpinned call would let the operator's real ~/.pi mailbox
|
|
808
|
+
# decide whether these rows carry an envelope.
|
|
809
|
+
MBX="$TMP/mbx"; mkdir -p "$MBX/20260606T000000-aaaaaa"
|
|
810
|
+
STATUS_OUT_MATCH="$(printf '%s' "$STATUS_INPUT_MATCH" | ENTWURF_META_SESSIONS_DIR="$STORE" ENTWURF_META_MAILBOX_DIR="$MBX" "$REPO/scripts/meta-bridge-statusline.sh")"
|
|
710
811
|
if [ "$(printf '%s\n' "$STATUS_OUT_MATCH" | wc -l | tr -d ' ')" = "2" ]; then ok "statusline renders exactly two rows"; else bad "statusline should render two rows: $STATUS_OUT_MATCH"; fi
|
|
711
812
|
if printf '%s\n' "$STATUS_OUT_MATCH" | sed -n '1p' | grep -q 'tmp' && printf '%s\n' "$STATUS_OUT_MATCH" | sed -n '2p' | grep -q '🪛 20260606T000000-aaaaaa cc | s'; then ok "statusline keeps row-1 work context and maps native session_id to row-2 garden-id"; else bad "statusline did not show expected two-row content for native-a: $STATUS_OUT_MATCH"; fi
|
|
712
|
-
STATUS_OUT_MISS="$(printf '%s' "$STATUS_INPUT_MISS" | ENTWURF_META_SESSIONS_DIR="$STORE" "$REPO/scripts/meta-bridge-statusline.sh")"
|
|
813
|
+
STATUS_OUT_MISS="$(printf '%s' "$STATUS_INPUT_MISS" | ENTWURF_META_SESSIONS_DIR="$STORE" ENTWURF_META_MAILBOX_DIR="$MBX" "$REPO/scripts/meta-bridge-statusline.sh")"
|
|
713
814
|
if printf '%s' "$STATUS_OUT_MISS" | grep -q '🪛 ? cc'; then ok "statusline no-record fallback is ?"; else bad "statusline no-record fallback wrong: $STATUS_OUT_MISS"; fi
|
|
714
|
-
STATUS_OUT_READY="$(printf '%s' "$STATUS_INPUT_READY" | ENTWURF_META_SESSIONS_DIR="$STORE" "$REPO/scripts/meta-bridge-statusline.sh")"
|
|
815
|
+
STATUS_OUT_READY="$(printf '%s' "$STATUS_INPUT_READY" | ENTWURF_META_SESSIONS_DIR="$STORE" ENTWURF_META_MAILBOX_DIR="$MBX" "$REPO/scripts/meta-bridge-statusline.sh")"
|
|
715
816
|
if printf '%s' "$STATUS_OUT_READY" | grep -q '🪛 ready cc'; then ok "statusline no-session_id fallback is ready"; else bad "statusline ready fallback wrong: $STATUS_OUT_READY"; fi
|
|
716
817
|
cp "$STORE/20260606T000000-aaaaaa.meta.json" "$STORE/20260606T000003-dddddd.meta.json"
|
|
717
|
-
STATUS_OUT_DUP="$(printf '%s' "$STATUS_INPUT_MATCH" | ENTWURF_META_SESSIONS_DIR="$STORE" "$REPO/scripts/meta-bridge-statusline.sh")"
|
|
818
|
+
STATUS_OUT_DUP="$(printf '%s' "$STATUS_INPUT_MATCH" | ENTWURF_META_SESSIONS_DIR="$STORE" ENTWURF_META_MAILBOX_DIR="$MBX" "$REPO/scripts/meta-bridge-statusline.sh")"
|
|
718
819
|
if printf '%s' "$STATUS_OUT_DUP" | grep -q '🪛 ! cc'; then ok "statusline duplicate nativeSessionId fallback is !"; else bad "statusline duplicate fallback wrong: $STATUS_OUT_DUP"; fi
|
|
719
820
|
rm "$STORE/20260606T000003-dddddd.meta.json"
|
|
720
821
|
|
|
822
|
+
# --- #98 B: the unread badge -------------------------------------------------
|
|
823
|
+
# Three states that must LOOK different: N letters, none, and "could not count".
|
|
824
|
+
# The last one is the whole reason the badge exists — an unread letter that the
|
|
825
|
+
# operator never saw is exactly the failure #98 opened on, and a statusline that
|
|
826
|
+
# renders a confident 0 when its read failed reproduces it one layer down.
|
|
827
|
+
badge_line() { printf '%s' "$1" | ENTWURF_META_SESSIONS_DIR="$STORE" ENTWURF_META_MAILBOX_DIR="$MBX" "$REPO/scripts/meta-bridge-statusline.sh" | sed -n '2p'; }
|
|
828
|
+
|
|
829
|
+
# empty mailbox -> NO badge. Silence is the quiet common case; the badge is for mail.
|
|
830
|
+
B_EMPTY="$(badge_line "$STATUS_INPUT_MATCH")"
|
|
831
|
+
if ! printf '%s' "$B_EMPTY" | grep -q '✉'; then ok "badge: empty mailbox draws no envelope"; else bad "badge: empty mailbox drew one: $B_EMPTY"; fi
|
|
832
|
+
|
|
833
|
+
# The counted set is EXACTLY readMetaInbox's unread union: *.msg + *.msg.delivered.
|
|
834
|
+
# The three decoys below must all be excluded: a .read (already drained — a badge that
|
|
835
|
+
# counted it would never clear), the signal file, and state.json.
|
|
836
|
+
: > "$MBX/20260606T000000-aaaaaa/one.msg"
|
|
837
|
+
: > "$MBX/20260606T000000-aaaaaa/two.msg.delivered"
|
|
838
|
+
: > "$MBX/20260606T000000-aaaaaa/three.msg.delivered.read"
|
|
839
|
+
: > "$MBX/20260606T000000-aaaaaa/inbox.signal"
|
|
840
|
+
: > "$MBX/20260606T000000-aaaaaa/state.json"
|
|
841
|
+
B_TWO="$(badge_line "$STATUS_INPUT_MATCH")"
|
|
842
|
+
if printf '%s' "$B_TWO" | grep -q '✉2'; then ok "badge: .msg + .msg.delivered counted, .read/signal/state excluded (✉2)"; else bad "badge: expected ✉2, got: $B_TWO"; fi
|
|
843
|
+
|
|
844
|
+
# The drain clears it on the NEXT render — the property acceptance criterion 4 names.
|
|
845
|
+
mv "$MBX/20260606T000000-aaaaaa/one.msg" "$MBX/20260606T000000-aaaaaa/one.msg.read"
|
|
846
|
+
mv "$MBX/20260606T000000-aaaaaa/two.msg.delivered" "$MBX/20260606T000000-aaaaaa/two.msg.delivered.read"
|
|
847
|
+
B_DRAINED="$(badge_line "$STATUS_INPUT_MATCH")"
|
|
848
|
+
if ! printf '%s' "$B_DRAINED" | grep -q '✉'; then ok "badge: clears after the read archives both bodies to .read"; else bad "badge: stayed after drain: $B_DRAINED"; fi
|
|
849
|
+
|
|
850
|
+
# Could-not-count #1: no garden id resolved, so there is no mailbox to look in. The
|
|
851
|
+
# statusline must NOT quietly claim zero for a session it could not place.
|
|
852
|
+
B_NOGARDEN="$(badge_line "$STATUS_INPUT_MISS")"
|
|
853
|
+
if printf '%s' "$B_NOGARDEN" | grep -q '✉?'; then ok "badge: unresolved garden id renders ✉? (never a false zero)"; else bad "badge: unresolved garden id should be ✉?, got: $B_NOGARDEN"; fi
|
|
854
|
+
|
|
855
|
+
# Could-not-count #2: the mailbox dir exists but cannot be read. Same rule.
|
|
856
|
+
chmod 000 "$MBX/20260606T000000-aaaaaa"
|
|
857
|
+
B_UNREADABLE="$(badge_line "$STATUS_INPUT_MATCH")"
|
|
858
|
+
chmod 755 "$MBX/20260606T000000-aaaaaa"
|
|
859
|
+
if printf '%s' "$B_UNREADABLE" | grep -q '✉?'; then ok "badge: unreadable mailbox renders ✉? (measurement failure, not zero)"; else bad "badge: unreadable mailbox should be ✉?, got: $B_UNREADABLE"; fi
|
|
860
|
+
|
|
861
|
+
# Could-not-count #3: no python3 at all. That degrade path predates the badge and used
|
|
862
|
+
# to print a bare `🪛 ? cc`; without the ✉? it would read as "counted, none".
|
|
863
|
+
# The stand-in PATH carries `cat` and nothing else: the script reads its stdin with `cat`
|
|
864
|
+
# BEFORE it tests for python3, so a truly empty PATH would kill it under `set -e` and the
|
|
865
|
+
# cell would pass for the wrong reason.
|
|
866
|
+
mkdir -p "$TMP/emptybin"
|
|
867
|
+
ln -sf "$(command -v cat)" "$TMP/emptybin/cat"
|
|
868
|
+
if [ -n "$(command -v python3 2>/dev/null)" ] && PATH="$TMP/emptybin" command -v python3 >/dev/null 2>&1; then bad "no-python3 fixture is not actually python3-less"; else ok "no-python3 fixture really hides python3"; fi
|
|
869
|
+
# `$BASH` (an absolute path) rather than the name `bash`, so the interpreter is reachable
|
|
870
|
+
# without putting a second binary on the stand-in PATH the fixture is trying to empty.
|
|
871
|
+
B_NOPY="$(printf '%s' "$STATUS_INPUT_MATCH" | PATH="$TMP/emptybin" ENTWURF_META_SESSIONS_DIR="$STORE" ENTWURF_META_MAILBOX_DIR="$MBX" "$BASH" "$REPO/scripts/meta-bridge-statusline.sh" | sed -n '2p')"
|
|
872
|
+
if printf '%s' "$B_NOPY" | grep -q '✉?'; then ok "badge: python3-less degrade line still says ✉? (not a silent zero)"; else bad "badge: python3-less degrade should carry ✉?, got: $B_NOPY"; fi
|
|
873
|
+
|
|
874
|
+
# A garden id that never renders a real mailbox: the pre-record "ready" state. Nothing
|
|
875
|
+
# has been addressed to this session yet, so an empty badge is TRUE, not a guess.
|
|
876
|
+
B_READY="$(badge_line "$STATUS_INPUT_READY")"
|
|
877
|
+
if ! printf '%s' "$B_READY" | grep -q '✉'; then ok "badge: pre-record 'ready' draws no envelope (nothing addressed yet)"; else bad "badge: ready state drew an envelope: $B_READY"; fi
|
|
878
|
+
rm -rf "$MBX"
|
|
879
|
+
|
|
721
880
|
if node --experimental-strip-types "$STORE_DOCTOR" "$STORE" >/dev/null; then ok "store doctor accepts valid records"; else bad "store doctor rejected valid records"; fi
|
|
722
881
|
cp "$STORE/20260606T000000-aaaaaa.meta.json" "$STORE/20260606T000002-cccccc.meta.json"
|
|
723
882
|
if node --experimental-strip-types "$STORE_DOCTOR" "$STORE" >/dev/null 2>&1; then bad "store doctor missed body/filename drift + duplicate"; else ok "store doctor fails on body/filename drift and duplicate nativeSessionId"; fi
|
|
@@ -31,7 +31,10 @@ d=json.load(sys.stdin)
|
|
|
31
31
|
s=d['settings']
|
|
32
32
|
assert 'statusLine' in s['map-entry'], 'statusLine missing from SSOT map-entry'
|
|
33
33
|
assert 'permissions.allow' in s['array-items'] and 'permissions.deny' in s['array-items'], 'permissions missing'
|
|
34
|
-
assert 'verbose' in s['scalar'] and '
|
|
34
|
+
assert 'verbose' in s['scalar'] and 'awaySummaryEnabled' in s['scalar'], 'scalar policy missing'
|
|
35
|
+
# #94: the compaction keys are RETIRED, so managed-keys must NOT claim them.
|
|
36
|
+
assert 'autoCompactEnabled' not in s['scalar'], 'retired compaction scalar still claimed as managed'
|
|
37
|
+
assert 'env.DISABLE_AUTOCOMPACT' not in s['scalar'], 'retired compaction env scalar still claimed as managed'
|
|
35
38
|
assert any(k.startswith('enabledPlugins.') for k in s['map-entry']), 'plugin ref missing'
|
|
36
39
|
assert d['claudeRoot']['map-entry'] == ['mcpServers.entwurf-bridge'], 'MCP key missing'
|
|
37
40
|
" && ok "managed-keys SSOT is valid JSON with the expected owned keys" \
|
|
@@ -194,7 +194,10 @@ async function main(): Promise<void> {
|
|
|
194
194
|
const callerGid = caller.record.gardenId;
|
|
195
195
|
// A record proves identity; only an ARMED receiver marker makes the mailbox deliverable —
|
|
196
196
|
// without it the callbacks would be refused as mailbox-undeliverable and this smoke would
|
|
197
|
-
// measure nothing.
|
|
197
|
+
// measure nothing. Since #101 the marker is half of that: a `claude-code-cli` watch owner
|
|
198
|
+
// is deliverable only while ITS OWN sender marker still names the garden it is armed for,
|
|
199
|
+
// which is what refuses a session that switched away in place. Both, or the fixture models
|
|
200
|
+
// a retired watch — the same shape smoke-mux-lifecycle-live already seeds.
|
|
198
201
|
meta.writeMetaReceiverMarker({
|
|
199
202
|
gardenId: callerGid,
|
|
200
203
|
backend: "claude-code",
|
|
@@ -202,9 +205,16 @@ async function main(): Promise<void> {
|
|
|
202
205
|
ownerPid: process.pid,
|
|
203
206
|
armProvenance: "session-start",
|
|
204
207
|
});
|
|
208
|
+
const callerSenderMarker = meta.writeMetaSenderMarker({
|
|
209
|
+
backend: "claude-code",
|
|
210
|
+
gardenId: callerGid,
|
|
211
|
+
nativeSessionId,
|
|
212
|
+
cwd: scratch,
|
|
213
|
+
ownerPid: process.pid,
|
|
214
|
+
});
|
|
205
215
|
ok(
|
|
206
|
-
"fixture: the caller citizen was minted INSIDE the fixture store
|
|
207
|
-
Boolean(callerGid) && caller.path.startsWith(root),
|
|
216
|
+
"fixture: the caller citizen was minted INSIDE the fixture store, with an armed mailbox and a sender marker",
|
|
217
|
+
Boolean(callerGid) && caller.path.startsWith(root) && callerSenderMarker.startsWith(root),
|
|
208
218
|
);
|
|
209
219
|
|
|
210
220
|
// ── One private tmux server per backend ──────────────────────────────────
|