@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.
Files changed (60) hide show
  1. package/AGENTS.md +4 -2
  2. package/CHANGELOG.md +333 -0
  3. package/VERIFY.md +8 -1
  4. package/docs/acp-backend-rail.md +25 -14
  5. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +1 -1
  6. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +15 -0
  7. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +9 -2
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +14 -2
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -2
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +18 -3
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +23 -7
  12. package/package.json +4 -4
  13. package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +3 -1
  14. package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +13 -8
  15. package/pi-extensions/lib/acp/acp-client.ts +57 -4
  16. package/pi-extensions/lib/acp/backend-adapter.ts +59 -0
  17. package/pi-extensions/lib/acp/backend.ts +453 -11
  18. package/pi-extensions/lib/acp/event-mapper.ts +43 -6
  19. package/pi-extensions/lib/entwurf-v2-mailbox.ts +9 -2
  20. package/pi-extensions/lib/entwurf-v2-runner.ts +23 -6
  21. package/pi-extensions/lib/entwurf-v2-send.ts +26 -3
  22. package/pi-extensions/lib/entwurf-v2-surface.ts +18 -3
  23. package/pi-extensions/lib/meta-session.ts +32 -7
  24. package/run.sh +21 -0
  25. package/scripts/check-acp-stop-reason.ts +8 -2
  26. package/scripts/check-acp-usage-accounting.ts +1074 -0
  27. package/scripts/check-entwurf-v2-mailbox.ts +33 -0
  28. package/scripts/check-entwurf-v2-runner.ts +18 -0
  29. package/scripts/check-entwurf-v2-send.ts +13 -1
  30. package/scripts/check-entwurf-v2-surface.ts +75 -3
  31. package/scripts/check-gate-qualification.ts +1 -0
  32. package/scripts/check-hook-launch-topology.ts +70 -1
  33. package/scripts/check-mailbox-receipt-state.ts +6 -0
  34. package/scripts/check-meta-doctor-oracle.sh +73 -0
  35. package/scripts/check-meta-mailbox-state-write.ts +9 -2
  36. package/scripts/check-meta-manifest-schema.py +19 -1
  37. package/scripts/check-meta-session.ts +10 -2
  38. package/scripts/lib/pi-record-discovery.ts +47 -0
  39. package/scripts/meta-bridge-doctor.sh +18 -3
  40. package/scripts/meta-bridge-state.py +23 -5
  41. package/scripts/meta-bridge-statusline.sh +65 -2
  42. package/scripts/mutants/acp-stream-hooks.json +4 -2
  43. package/scripts/mutants/acp-usage-accounting.json +181 -0
  44. package/scripts/raw-async-delivery/README.md +280 -9
  45. package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +6 -2
  46. package/scripts/raw-async-delivery/lab-statusline.sh +63 -0
  47. package/scripts/raw-async-delivery/mailbox-watch.py +230 -0
  48. package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +3 -1
  49. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +13 -4
  50. package/scripts/raw-async-delivery/probe-delivery-transparency.sh +387 -0
  51. package/scripts/smoke-acp-bundled-mcp-live.ts +2 -2
  52. package/scripts/smoke-acp-cortex-live.ts +2 -2
  53. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  54. package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
  55. package/scripts/smoke-acp-v2-send-live.ts +2 -2
  56. package/scripts/smoke-entwurf-v2-matrix-live.ts +60 -10
  57. package/scripts/smoke-meta-async-drift.sh +31 -8
  58. package/scripts/smoke-meta-install-state.sh +170 -11
  59. package/scripts/smoke-meta-keyset-guard.sh +4 -1
  60. 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
- assert s['files']['settings']['keys']['autoCompactEnabled']['original']['existed'] is False
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
- assert settings['env']['DISABLE_AUTOCOMPACT'] == '1'
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','autoCompactEnabled','showTurnDuration','terminalProgressBarEnabled','useAutoModeDuringPlan','enableWorkflows','workflowKeywordTriggerEnabled']:
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
- for key in ['awaySummaryEnabled','autoMemoryEnabled','verbose','autoCompactEnabled','terminalProgressBarEnabled','useAutoModeDuringPlan','enableWorkflows','workflowKeywordTriggerEnabled']:
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
- assert s['files']['settings']['keys']['env.DISABLE_AUTOCOMPACT']['original']['existed'] is True
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
- STATUS_OUT_MATCH="$(printf '%s' "$STATUS_INPUT_MATCH" | ENTWURF_META_SESSIONS_DIR="$STORE" "$REPO/scripts/meta-bridge-statusline.sh")"
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 'autoCompactEnabled' in s['scalar'], 'scalar policy missing'
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
- `--- launch receipt ---\n${launch.text}`,
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
- `--- launch receipt ---\n${ccLaunch.text}`,
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);