@junghanacs/entwurf 0.16.1 → 0.17.2
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 +4 -2
- package/CHANGELOG.md +333 -0
- package/VERIFY.md +8 -1
- package/docs/acp-backend-rail.md +25 -14
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +15 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +9 -2
- 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 +18 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +23 -7
- package/package.json +4 -4
- 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/acp/acp-client.ts +57 -4
- package/pi-extensions/lib/acp/backend-adapter.ts +59 -0
- package/pi-extensions/lib/acp/backend.ts +453 -11
- package/pi-extensions/lib/acp/event-mapper.ts +43 -6
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +9 -2
- 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 +18 -3
- package/pi-extensions/lib/meta-session.ts +32 -7
- package/run.sh +21 -0
- package/scripts/check-acp-stop-reason.ts +8 -2
- package/scripts/check-acp-usage-accounting.ts +1074 -0
- package/scripts/check-entwurf-v2-mailbox.ts +33 -0
- 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-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/lib/pi-record-discovery.ts +47 -0
- package/scripts/meta-bridge-doctor.sh +18 -3
- package/scripts/meta-bridge-state.py +23 -5
- package/scripts/meta-bridge-statusline.sh +65 -2
- package/scripts/mutants/acp-stream-hooks.json +4 -2
- package/scripts/mutants/acp-usage-accounting.json +181 -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/smoke-acp-bundled-mcp-live.ts +2 -2
- package/scripts/smoke-acp-cortex-live.ts +2 -2
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
- package/scripts/smoke-acp-v2-send-live.ts +2 -2
- package/scripts/smoke-entwurf-v2-matrix-live.ts +60 -10
- 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-lifecycle-live.ts +46 -2
|
@@ -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" \
|
|
@@ -213,6 +213,46 @@ function pidIsAlive(pid: number): boolean {
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
/**
|
|
217
|
+
* What the launched window actually shows, read at the moment a nonce callback never arrived.
|
|
218
|
+
*
|
|
219
|
+
* The launch receipt says so itself — "if it never comes, the window is visible and can be read
|
|
220
|
+
* directly" — but on a headless release-gate run nobody is there to look, and the window is torn
|
|
221
|
+
* down in teardown seconds later. So the harness looks for the operator: is the pane process even
|
|
222
|
+
* alive, and what is painted in it. That separates the three states a bare timeout collapses into
|
|
223
|
+
* one — the runtime never started (or died), it started and is sitting on an error or a prompt, or
|
|
224
|
+
* it is running fine and the model simply did not call the callback tool. A pi-native callback
|
|
225
|
+
* timeout with no window evidence is what blocked the second 0.17.0 `--cut` (and, in the same
|
|
226
|
+
* shape, a 0.16.1 run), leaving nothing to tell those three apart afterwards.
|
|
227
|
+
*
|
|
228
|
+
* Diagnostics must never become the failure: every step is best-effort, and a tmux that cannot
|
|
229
|
+
* answer is reported as its own line rather than thrown.
|
|
230
|
+
*/
|
|
231
|
+
function paneForensics(label: string, socket: string, env: NodeJS.ProcessEnv, coords: Coordinates): string {
|
|
232
|
+
const lines: string[] = [`--- ${label} window forensics (the callback never came) ---`];
|
|
233
|
+
try {
|
|
234
|
+
lines.push(`pane ${coords.paneId} pid ${coords.panePid}: ${pidIsAlive(Number(coords.panePid)) ? "ALIVE" : "GONE"}`);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
lines.push(`pane pid liveness unreadable: ${err instanceof Error ? err.message : String(err)}`);
|
|
237
|
+
}
|
|
238
|
+
const panes = tmux(
|
|
239
|
+
socket,
|
|
240
|
+
["list-panes", "-a", "-F", "#{pane_id} #{pane_pid} #{pane_dead} #{pane_current_command}"],
|
|
241
|
+
env,
|
|
242
|
+
);
|
|
243
|
+
lines.push(
|
|
244
|
+
panes.status === 0 ? `list-panes:\n${panes.stdout || "(none)"}` : `list-panes failed (status ${panes.status})`,
|
|
245
|
+
);
|
|
246
|
+
const captured = tmux(socket, ["capture-pane", "-p", "-t", coords.paneId], env);
|
|
247
|
+
if (captured.status === 0) {
|
|
248
|
+
const tail = captured.stdout.split("\n").slice(-40).join("\n");
|
|
249
|
+
lines.push(`capture-pane (last 40 lines):\n${tail || "(the pane painted nothing)"}`);
|
|
250
|
+
} else {
|
|
251
|
+
lines.push(`capture-pane failed (status ${captured.status}) — the window is already gone`);
|
|
252
|
+
}
|
|
253
|
+
return lines.join("\n");
|
|
254
|
+
}
|
|
255
|
+
|
|
216
256
|
async function waitForPidsGone(pids: ReadonlySet<number>, timeoutMs = 10_000): Promise<boolean> {
|
|
217
257
|
const deadline = Date.now() + timeoutMs;
|
|
218
258
|
while (Date.now() < deadline) {
|
|
@@ -704,7 +744,9 @@ async function main(): Promise<void> {
|
|
|
704
744
|
ok(
|
|
705
745
|
`${cell}: the nonce came back and its SENDER ENVELOPE carries a garden id — correlation without asking the sibling`,
|
|
706
746
|
gid !== null && GARDEN_ID.test(gid),
|
|
707
|
-
|
|
747
|
+
gid === null
|
|
748
|
+
? `--- launch receipt ---\n${launch.text}\n${paneForensics(cell, srv.socket, srv.env, coords)}`
|
|
749
|
+
: `--- launch receipt ---\n${launch.text}`,
|
|
708
750
|
);
|
|
709
751
|
const citizen = gid as string;
|
|
710
752
|
siblingGids.add(citizen);
|
|
@@ -936,7 +978,9 @@ async function main(): Promise<void> {
|
|
|
936
978
|
ok(
|
|
937
979
|
"claude-code: the nonce came back and its SENDER ENVELOPE carries a garden id",
|
|
938
980
|
ccGid !== null && GARDEN_ID.test(ccGid),
|
|
939
|
-
|
|
981
|
+
ccGid === null
|
|
982
|
+
? `--- launch receipt ---\n${ccLaunch.text}\n${paneForensics("claude-code", cc.socket, cc.env, ccCoords)}`
|
|
983
|
+
: `--- launch receipt ---\n${ccLaunch.text}`,
|
|
940
984
|
);
|
|
941
985
|
const ccCitizen = ccGid as string;
|
|
942
986
|
siblingGids.add(ccCitizen);
|