@junghanacs/entwurf 0.12.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.
Files changed (183) hide show
  1. package/AGENTS.md +240 -0
  2. package/BASELINE.md +227 -0
  3. package/CHANGELOG.md +1210 -0
  4. package/CONTRIBUTING.md +63 -0
  5. package/DELIVERY.md +209 -0
  6. package/LICENSE +21 -0
  7. package/README.md +504 -0
  8. package/VERIFY.md +260 -0
  9. package/demo/README.md +188 -0
  10. package/demo/demo-baseline.sh +156 -0
  11. package/demo/demo.sh +183 -0
  12. package/docs/assets/entwurf-demo.gif +0 -0
  13. package/docs/assets/entwurf-doomemacs.gif +0 -0
  14. package/docs/assets/entwurf-entwurf.gif +0 -0
  15. package/docs/assets/entwurf-hero.jpg +0 -0
  16. package/docs/setup-clean-host.md +305 -0
  17. package/mcp/entwurf-bridge/src/index.ts +513 -0
  18. package/mcp/entwurf-bridge/start.sh +25 -0
  19. package/mcp/entwurf-bridge/test.sh +54 -0
  20. package/mcp/tsconfig.json +29 -0
  21. package/package.json +130 -0
  22. package/pi/entwurf-capabilities.json +9 -0
  23. package/pi/entwurf-targets.json +20 -0
  24. package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
  25. package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
  26. package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
  27. package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
  28. package/pi/settings.reference.json +43 -0
  29. package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
  30. package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
  31. package/pi-extensions/acp-provider.ts +63 -0
  32. package/pi-extensions/entwurf-control.ts +1692 -0
  33. package/pi-extensions/lib/acp/acp-client.ts +90 -0
  34. package/pi-extensions/lib/acp/augment.ts +238 -0
  35. package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
  36. package/pi-extensions/lib/acp/backend.ts +873 -0
  37. package/pi-extensions/lib/acp/config.ts +552 -0
  38. package/pi-extensions/lib/acp/context.ts +177 -0
  39. package/pi-extensions/lib/acp/engraving.ts +123 -0
  40. package/pi-extensions/lib/acp/event-mapper.ts +339 -0
  41. package/pi-extensions/lib/acp/models.ts +102 -0
  42. package/pi-extensions/lib/acp/overlay.ts +220 -0
  43. package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
  44. package/pi-extensions/lib/acp/session-store.ts +418 -0
  45. package/pi-extensions/lib/acp/tool-surface.ts +184 -0
  46. package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
  47. package/pi-extensions/lib/entwurf-core.ts +2033 -0
  48. package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
  49. package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
  50. package/pi-extensions/lib/entwurf-facts.ts +251 -0
  51. package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
  52. package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
  53. package/pi-extensions/lib/entwurf-preflight.ts +247 -0
  54. package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
  55. package/pi-extensions/lib/entwurf-self-address.ts +117 -0
  56. package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
  57. package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
  58. package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
  59. package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
  60. package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
  61. package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
  62. package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
  63. package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
  64. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
  65. package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
  66. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
  67. package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
  68. package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
  69. package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
  70. package/pi-extensions/lib/meta-session.ts +1951 -0
  71. package/pi-extensions/lib/project-trust-handler.ts +154 -0
  72. package/pi-extensions/lib/session-id.js +57 -0
  73. package/pi-extensions/lib/socket-discovery.ts +346 -0
  74. package/pi-extensions/lib/socket-probe.ts +92 -0
  75. package/pi-extensions/meta-bridge-hook.ts +250 -0
  76. package/pi-extensions/model-lock.ts +235 -0
  77. package/prompts/engraving.md +27 -0
  78. package/protocol.js +31 -0
  79. package/run.sh +2832 -0
  80. package/scripts/check-acp-backend-preflight.ts +130 -0
  81. package/scripts/check-acp-carrier-augment.ts +297 -0
  82. package/scripts/check-acp-config.ts +322 -0
  83. package/scripts/check-acp-event-mapper.ts +253 -0
  84. package/scripts/check-acp-overlay.ts +154 -0
  85. package/scripts/check-acp-prompt-builder.ts +191 -0
  86. package/scripts/check-acp-provider-surface.ts +159 -0
  87. package/scripts/check-acp-sdk-surface.ts +211 -0
  88. package/scripts/check-acp-session-reuse.ts +756 -0
  89. package/scripts/check-acp-session-store.ts +387 -0
  90. package/scripts/check-acp-tool-surface.ts +159 -0
  91. package/scripts/check-entwurf-bridge-boot.ts +199 -0
  92. package/scripts/check-entwurf-capabilities.ts +123 -0
  93. package/scripts/check-entwurf-control-rpc.ts +187 -0
  94. package/scripts/check-entwurf-deliverability.ts +158 -0
  95. package/scripts/check-entwurf-fact-provider.ts +316 -0
  96. package/scripts/check-entwurf-facts.ts +358 -0
  97. package/scripts/check-entwurf-mailbox-guard.ts +264 -0
  98. package/scripts/check-entwurf-peers-surface.ts +235 -0
  99. package/scripts/check-entwurf-resume-args.ts +149 -0
  100. package/scripts/check-entwurf-self-address.ts +209 -0
  101. package/scripts/check-entwurf-session-identity.ts +703 -0
  102. package/scripts/check-entwurf-v2-contract.ts +536 -0
  103. package/scripts/check-entwurf-v2-decider.ts +795 -0
  104. package/scripts/check-entwurf-v2-lock.ts +368 -0
  105. package/scripts/check-entwurf-v2-mailbox.ts +228 -0
  106. package/scripts/check-entwurf-v2-matrix.ts +437 -0
  107. package/scripts/check-entwurf-v2-production.ts +406 -0
  108. package/scripts/check-entwurf-v2-release.ts +245 -0
  109. package/scripts/check-entwurf-v2-runner.ts +452 -0
  110. package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
  111. package/scripts/check-entwurf-v2-send.ts +454 -0
  112. package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
  113. package/scripts/check-entwurf-v2-spawn.ts +398 -0
  114. package/scripts/check-entwurf-v2-surface.ts +352 -0
  115. package/scripts/check-keyset-overlap.py +120 -0
  116. package/scripts/check-mailbox-receipt-state.ts +170 -0
  117. package/scripts/check-meta-capability-source.ts +112 -0
  118. package/scripts/check-meta-dual-consumers.ts +154 -0
  119. package/scripts/check-meta-dual-read.ts +158 -0
  120. package/scripts/check-meta-listing.ts +138 -0
  121. package/scripts/check-meta-mailbox-state-write.ts +135 -0
  122. package/scripts/check-meta-migration.ts +212 -0
  123. package/scripts/check-meta-receiver-marker.ts +185 -0
  124. package/scripts/check-meta-record-v2.ts +191 -0
  125. package/scripts/check-meta-session.ts +673 -0
  126. package/scripts/check-model-lock.ts +408 -0
  127. package/scripts/check-package-source-routing.ts +253 -0
  128. package/scripts/check-pi-preflight.ts +304 -0
  129. package/scripts/check-project-trust-handler.ts +265 -0
  130. package/scripts/check-shell-quote.ts +121 -0
  131. package/scripts/check-socket-discovery.ts +428 -0
  132. package/scripts/check-socket-probe.ts +106 -0
  133. package/scripts/fixtures/probe-mcp-server.ts +33 -0
  134. package/scripts/gnew-rpc-drive.ts +211 -0
  135. package/scripts/lib/acp-child-cleanup.ts +116 -0
  136. package/scripts/meta-bridge-doctor.sh +315 -0
  137. package/scripts/meta-bridge-hook-log.sh +26 -0
  138. package/scripts/meta-bridge-install.sh +135 -0
  139. package/scripts/meta-bridge-prune.ts +199 -0
  140. package/scripts/meta-bridge-state.py +549 -0
  141. package/scripts/meta-bridge-statusline.sh +192 -0
  142. package/scripts/meta-bridge-store-doctor.ts +64 -0
  143. package/scripts/meta-bridge-uninstall.sh +39 -0
  144. package/scripts/new-session-id.ts +25 -0
  145. package/scripts/postinstall-chmod.cjs +58 -0
  146. package/scripts/raw-async-delivery/README.md +258 -0
  147. package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
  148. package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
  149. package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
  150. package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
  151. package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
  152. package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
  153. package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
  154. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
  155. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
  156. package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
  157. package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
  158. package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
  159. package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
  160. package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
  161. package/scripts/resolve-acp-bridge.ts +25 -0
  162. package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
  163. package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
  164. package/scripts/smoke-acp-mcp-live.ts +129 -0
  165. package/scripts/smoke-acp-memory-containment-live.ts +389 -0
  166. package/scripts/smoke-acp-overlay-live.ts +314 -0
  167. package/scripts/smoke-acp-provider-live.ts +162 -0
  168. package/scripts/smoke-acp-raw-turn-live.ts +261 -0
  169. package/scripts/smoke-acp-session-reuse-live.ts +172 -0
  170. package/scripts/smoke-acp-skill-live.ts +144 -0
  171. package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
  172. package/scripts/smoke-claude-native-resume-live.sh +198 -0
  173. package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
  174. package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
  175. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
  176. package/scripts/smoke-meta-async-drift.sh +171 -0
  177. package/scripts/smoke-meta-honesty.sh +147 -0
  178. package/scripts/smoke-meta-install-state.sh +403 -0
  179. package/scripts/smoke-meta-keyset-guard.sh +111 -0
  180. package/scripts/smoke-meta-prune.sh +174 -0
  181. package/scripts/smoke-resident-garden-guard.sh +433 -0
  182. package/scripts/smoke-session-id-name.ts +187 -0
  183. package/scripts/tsconfig.json +34 -0
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-meta-keyset-guard — deterministic regression for the PREVENTIVE keyset
3
+ # guard (check-keyset-overlap) + the managed-keys SSOT it reads. Proves a
4
+ # disjoint consumer fragment passes, and that exact / array / parent-child key
5
+ # collisions fail loud, while unrelated sibling keys (permissions.defaultMode,
6
+ # language) stay clean. Offline / hermetic (synthetic fragments). Deps: bash +
7
+ # python3. The effect-based survival half (doctor's state.py check catching an
8
+ # overwritten pi key) is regression-tested in smoke-meta-install-state.
9
+ set -euo pipefail
10
+
11
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+ REPO="$(cd "$HERE/.." && pwd)"
13
+ STATE="$REPO/scripts/meta-bridge-state.py"
14
+ OVERLAP="$REPO/scripts/check-keyset-overlap.py"
15
+
16
+ command -v python3 >/dev/null || { echo "FAIL: python3 not on PATH"; exit 1; }
17
+
18
+ fail=0
19
+ ok() { echo " ok $*"; }
20
+ bad() { echo " FAIL $*"; fail=1; }
21
+
22
+ TMP="$(mktemp -d -t psa-meta-keyset.XXXXXX)"
23
+ cleanup() { rm -rf "$TMP"; }
24
+ trap cleanup EXIT
25
+
26
+ # --- managed-keys SSOT sanity ------------------------------------------------
27
+ KEYS_JSON="$(python3 "$STATE" managed-keys)"
28
+ echo "$KEYS_JSON" | python3 -c "
29
+ import json,sys
30
+ d=json.load(sys.stdin)
31
+ s=d['settings']
32
+ assert 'statusLine' in s['map-entry'], 'statusLine missing from SSOT map-entry'
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'
35
+ assert any(k.startswith('enabledPlugins.') for k in s['map-entry']), 'plugin ref missing'
36
+ assert d['claudeRoot']['map-entry'] == ['mcpServers.entwurf-bridge'], 'MCP key missing'
37
+ " && ok "managed-keys SSOT is valid JSON with the expected owned keys" \
38
+ || bad "managed-keys SSOT malformed or missing expected keys"
39
+
40
+ # --- DISJOINT consumer fragment passes ---------------------------------------
41
+ # Mirrors a clean agent-config fragment: its OWN concerns only. permissions.defaultMode
42
+ # and language must NOT trip the guard (pi owns permissions.allow/deny + scalars, not these).
43
+ cat > "$TMP/disjoint.json" <<'JSON'
44
+ {
45
+ "permissions": { "defaultMode": "acceptEdits" },
46
+ "hooks": { "SessionStart": [] },
47
+ "enabledPlugins": { "github@claude-plugins-official": true },
48
+ "language": "ko",
49
+ "editorMode": "normal",
50
+ "preferredNotifChannel": "notifications_disabled"
51
+ }
52
+ JSON
53
+ if python3 "$OVERLAP" "$TMP/disjoint.json" >/dev/null 2>&1; then
54
+ ok "disjoint fragment passes (exit 0)"
55
+ else
56
+ bad "disjoint fragment wrongly flagged"
57
+ fi
58
+
59
+ # --- COLLISIONS fail loud -----------------------------------------------------
60
+ assert_collision() {
61
+ local label="$1" file="$2" expect_key="$3"
62
+ local out code
63
+ set +e
64
+ out="$(python3 "$OVERLAP" "$file" 2>&1)"; code=$?
65
+ set -e
66
+ if [ "$code" = "1" ] && printf '%s' "$out" | grep -q "$expect_key"; then
67
+ ok "$label collides (exit 1, names '$expect_key')"
68
+ else
69
+ bad "$label not detected (code=$code): $out"
70
+ fi
71
+ }
72
+
73
+ # exact scalar collision
74
+ echo '{ "verbose": true }' > "$TMP/scalar.json"
75
+ assert_collision "scalar (verbose)" "$TMP/scalar.json" "verbose"
76
+
77
+ # array collision (permissions.allow — the array agent-config's merge would replace)
78
+ echo '{ "permissions": { "allow": ["Read"] } }' > "$TMP/array.json"
79
+ assert_collision "array (permissions.allow)" "$TMP/array.json" "permissions.allow"
80
+
81
+ # parent/child collision: consumer sets statusLine.command, pi owns whole statusLine
82
+ echo '{ "statusLine": { "command": "/evil.sh" } }' > "$TMP/child.json"
83
+ assert_collision "parent/child (statusLine.command)" "$TMP/child.json" "statusLine"
84
+
85
+ # exact plugin-ref collision
86
+ python3 "$STATE" managed-keys | python3 -c "
87
+ import json,sys
88
+ d=json.load(sys.stdin)
89
+ ref=[k for k in d['settings']['map-entry'] if k.startswith('enabledPlugins.')][0].split('.',1)[1]
90
+ print(json.dumps({'enabledPlugins': {ref: True}}))
91
+ " > "$TMP/plugin.json"
92
+ assert_collision "exact plugin ref" "$TMP/plugin.json" "enabledPlugins"
93
+
94
+ # --- multi-fragment: one clean + one dirty -> overall fail --------------------
95
+ set +e
96
+ python3 "$OVERLAP" "$TMP/disjoint.json" "$TMP/scalar.json" >/dev/null 2>&1; multi=$?
97
+ set -e
98
+ [ "$multi" = "1" ] && ok "multi-fragment fails if ANY collides" || bad "multi-fragment did not fail on a dirty member"
99
+
100
+ # --- missing fragment is a usage error, not a false pass ----------------------
101
+ set +e
102
+ python3 "$OVERLAP" "$TMP/nope.json" >/dev/null 2>&1; miss=$?
103
+ set -e
104
+ [ "$miss" = "2" ] && ok "missing fragment -> usage exit 2" || bad "missing fragment did not exit 2 (got $miss)"
105
+
106
+ if [ "$fail" = "0" ]; then
107
+ echo "smoke-meta-keyset-guard PASS"
108
+ else
109
+ echo "smoke-meta-keyset-guard FAIL"
110
+ exit 1
111
+ fi
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-meta-prune — deterministic Phase-4 gate for the LISTING-ONLY meta-store
3
+ # janitor. Builds a synthetic meta-sessions store covering every class
4
+ # (keep / orphan / stale / duplicate / corrupt / drift), runs meta-bridge-prune,
5
+ # and proves it CLASSIFIES correctly, exits 0, and DELETES NOTHING. Offline /
6
+ # deterministic: no real claude CLI, no user config. Deps: bash + node.
7
+ set -euo pipefail
8
+
9
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ REPO="$(cd "$HERE/.." && pwd)"
11
+ LIB="$REPO/pi-extensions/lib/meta-session.ts"
12
+ PRUNE="$REPO/scripts/meta-bridge-prune.ts"
13
+
14
+ command -v node >/dev/null || { echo "FAIL: node not on PATH"; exit 1; }
15
+
16
+ fail=0
17
+ ok() { echo " ok $*"; }
18
+ bad() { echo " FAIL $*"; fail=1; }
19
+
20
+ TMP="$(mktemp -d -t psa-meta-prune.XXXXXX)"
21
+ cleanup() { rm -rf "$TMP"; }
22
+ trap cleanup EXIT
23
+
24
+ # Deliberately a path WITH A SPACE so the printed manual rm command is proven to
25
+ # survive shell-quoting (a bare `rm <path>` would split on the space).
26
+ STORE="$TMP/meta sessions dir"
27
+ mkdir -p "$STORE"
28
+ LIVE_TRANSCRIPT="$TMP/live-transcript.jsonl"
29
+ : > "$LIVE_TRANSCRIPT" # an EXISTING transcript file for keep/stale records
30
+
31
+ # Generate valid records through the real lib so the smoke tracks schema truth.
32
+ # keep : transcript exists + recent lastSeen
33
+ # orphan: transcript path absent
34
+ # stale : transcript exists + lastSeen older than ttl
35
+ # dupA/B: same nativeSessionId, two files -> ambiguous (authority conflict)
36
+ # drift : valid body written under a wrong filename -> ambiguous
37
+ # corrupt: non-parseable json -> ambiguous
38
+ cat > "$TMP/gen.mjs" <<'JS'
39
+ const [libPath, store, liveTranscript] = process.argv.slice(2);
40
+ const fs = await import("node:fs");
41
+ const path = await import("node:path");
42
+ const { mintMetaRecord, serializeMetaRecord } = await import(libPath);
43
+
44
+ const DAY = 24 * 60 * 60 * 1000;
45
+ const now = new Date();
46
+ const old = new Date(now.getTime() - 40 * DAY); // older than the 30d default ttl
47
+
48
+ function write(filename, record) {
49
+ fs.writeFileSync(path.join(store, filename), serializeMetaRecord(record));
50
+ }
51
+
52
+ // keep — live transcript, recent
53
+ const keep = mintMetaRecord({ backend: "claude-code", nativeSessionId: "n-keep", transcriptPath: liveTranscript, cwd: "/tmp/keep" }, now);
54
+ write(`${keep.gardenId}.meta.json`, keep);
55
+
56
+ // orphan — transcript path that does not exist
57
+ const orphan = mintMetaRecord({ backend: "claude-code", nativeSessionId: "n-orphan", transcriptPath: "/no/such/transcript.jsonl", cwd: "/tmp/orphan" }, now);
58
+ write(`${orphan.gardenId}.meta.json`, orphan);
59
+
60
+ // stale — live transcript but old lastSeen
61
+ const stale = mintMetaRecord({ backend: "claude-code", nativeSessionId: "n-stale", transcriptPath: liveTranscript, cwd: "/tmp/stale" }, old);
62
+ write(`${stale.gardenId}.meta.json`, stale);
63
+
64
+ // duplicate nativeSessionId across two distinct files
65
+ const dupA = mintMetaRecord({ backend: "claude-code", nativeSessionId: "n-dup", transcriptPath: liveTranscript, cwd: "/tmp/a" }, now);
66
+ const dupB = mintMetaRecord({ backend: "claude-code", nativeSessionId: "n-dup", transcriptPath: liveTranscript, cwd: "/tmp/b" }, now);
67
+ write(`${dupA.gardenId}.meta.json`, dupA);
68
+ write(`${dupB.gardenId}.meta.json`, dupB);
69
+
70
+ // drift — valid body, wrong filename
71
+ const drift = mintMetaRecord({ backend: "claude-code", nativeSessionId: "n-drift", transcriptPath: liveTranscript, cwd: "/tmp/drift" }, now);
72
+ write(`20991231T235959-ffffff.meta.json`, drift);
73
+
74
+ // invalid lastSeen — parseMetaRecord passes (non-empty string) but Date.parse fails;
75
+ // live transcript so NOT orphan. Cannot prove "recent" -> must be ambiguous, not keep.
76
+ const badDate = mintMetaRecord({ backend: "claude-code", nativeSessionId: "n-baddate", transcriptPath: liveTranscript, cwd: "/tmp/baddate" }, now);
77
+ badDate.lastSeen = "not-a-date";
78
+ write(`${badDate.gardenId}.meta.json`, badDate);
79
+
80
+ // corrupt — non-parseable json
81
+ fs.writeFileSync(path.join(store, `20260101T000000-aaaaaa.meta.json`), "{ this is not valid json");
82
+
83
+ console.log(JSON.stringify({
84
+ keep: keep.gardenId, orphan: orphan.gardenId, stale: stale.gardenId,
85
+ dupA: dupA.gardenId, dupB: dupB.gardenId, drift: "20991231T235959-ffffff",
86
+ badDate: badDate.gardenId, corrupt: "20260101T000000-aaaaaa",
87
+ }));
88
+ JS
89
+
90
+ META="$(node --experimental-strip-types "$TMP/gen.mjs" "$LIB" "$STORE" "$LIVE_TRANSCRIPT")"
91
+ ORPHAN_ID="$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).orphan)' "$META")"
92
+ STALE_ID="$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).stale)' "$META")"
93
+ KEEP_ID="$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).keep)' "$META")"
94
+ BADDATE_ID="$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).badDate)' "$META")"
95
+
96
+ FILES_BEFORE="$(find "$STORE" -name '*.meta.json' | wc -l | tr -d ' ')"
97
+
98
+ set +e
99
+ OUT="$(node --experimental-strip-types "$PRUNE" "$STORE" 2>&1)"
100
+ CODE=$?
101
+ set -e
102
+
103
+ echo "$OUT"
104
+ echo "---"
105
+
106
+ # exit 0 — listing-only never errors on a scannable store (corrupt is classified, not fatal)
107
+ [ "$CODE" = "0" ] && ok "exit 0 on classifiable store" || bad "expected exit 0, got $CODE"
108
+
109
+ # classification counts: 1 orphan, 1 stale, 1 keep, 4 ambiguous (duplicate(dupA+dupB -> one line) + drift + invalid-lastSeen + corrupt)
110
+ echo "$OUT" | grep -q "ORPHAN transcript-gone (1):" && ok "orphan count 1" || bad "orphan count != 1"
111
+ echo "$OUT" | grep -q "STALE recordUpdatedAt>30d (1):" && ok "stale count 1" || bad "stale count != 1"
112
+ echo "$OUT" | grep -q "KEEP (live/recent): 1 record" && ok "keep count 1" || bad "keep count != 1"
113
+ # ambiguous lines: duplicate(1) + drift(1) + invalid-lastSeen(1) + corrupt(1) = 4
114
+ echo "$OUT" | grep -q "AMBIGUOUS manual-only (4):" && ok "ambiguous count 4" || bad "ambiguous count != 4"
115
+
116
+ # the right records land in the right class
117
+ echo "$OUT" | grep -A1 "ORPHAN" | grep -q "$ORPHAN_ID" && ok "orphan id correct" || bad "orphan id missing from ORPHAN section"
118
+ echo "$OUT" | grep -A1 "STALE" | grep -q "$STALE_ID" && ok "stale id correct" || bad "stale id missing from STALE section"
119
+
120
+ # ambiguous specifics
121
+ echo "$OUT" | grep -q "duplicate nativeSessionId" && ok "duplicate classified ambiguous" || bad "duplicate not classified"
122
+ echo "$OUT" | grep -q "corrupt" && ok "corrupt classified ambiguous" || bad "corrupt not classified"
123
+ echo "$OUT" | grep -q "body/filename drift" && ok "drift classified ambiguous" || bad "drift not classified"
124
+ # invalid recordUpdatedAt cannot prove recency -> manual-only, NOT keep
125
+ echo "$OUT" | grep -q "unparseable recordUpdatedAt" && ok "invalid recordUpdatedAt classified ambiguous" || bad "invalid recordUpdatedAt not classified"
126
+ echo "$OUT" | grep -A6 "AMBIGUOUS" | grep -q "$BADDATE_ID" && ok "invalid-lastSeen id in AMBIGUOUS section" || bad "invalid-lastSeen id missing from AMBIGUOUS"
127
+
128
+ # keep is NOT offered as a removal command
129
+ echo "$OUT" | grep -q "rm .*${KEEP_ID}.meta.json" && bad "keep record wrongly offered for removal" || ok "keep not offered for removal"
130
+ # invalid-lastSeen is ambiguous, so it must NOT be offered as a removal command either
131
+ echo "$OUT" | grep -q "rm .*${BADDATE_ID}.meta.json" && bad "invalid-lastSeen wrongly offered for removal" || ok "invalid-lastSeen not offered for removal"
132
+ # orphan IS offered as a manual rm
133
+ echo "$OUT" | grep -q "rm .*${ORPHAN_ID}.meta.json" && ok "orphan offered for manual rm" || bad "orphan rm command missing"
134
+
135
+ # the printed manual rm command is POSIX-quoted so a store path with spaces survives.
136
+ # Extract the orphan rm line and replay it against a COPY of the store: the quoted
137
+ # command must remove exactly that one file and nothing splits on the space.
138
+ RMLINE="$(echo "$OUT" | grep -F "${ORPHAN_ID}.meta.json" | grep '^ rm -- ' | head -1)"
139
+ echo "$RMLINE" | grep -q "rm -- '" && ok "manual rm command is single-quoted" || bad "manual rm command not quoted: $RMLINE"
140
+ COPY="$TMP/store copy"
141
+ cp -r "$STORE" "$COPY"
142
+ # rewrite the command's path from the live store to the copy, then execute it
143
+ REPLAY="${RMLINE/$STORE/$COPY}"
144
+ COPY_BEFORE="$(find "$COPY" -name '*.meta.json' | wc -l | tr -d ' ')"
145
+ eval "$REPLAY"
146
+ COPY_AFTER="$(find "$COPY" -name '*.meta.json' | wc -l | tr -d ' ')"
147
+ [ ! -e "$COPY/${ORPHAN_ID}.meta.json" ] && [ "$COPY_AFTER" = "$((COPY_BEFORE - 1))" ] \
148
+ && ok "quoted rm replays correctly on spaced path (removed exactly the orphan)" \
149
+ || bad "quoted rm failed on spaced path ($COPY_BEFORE -> $COPY_AFTER, orphan present=$( [ -e "$COPY/${ORPHAN_ID}.meta.json" ] && echo yes || echo no ))"
150
+
151
+ # the no-deletion invariant — wording + actual files on disk
152
+ echo "$OUT" | grep -q "No files removed." && ok "prints 'No files removed.'" || bad "missing no-deletion wording"
153
+ FILES_AFTER="$(find "$STORE" -name '*.meta.json' | wc -l | tr -d ' ')"
154
+ [ "$FILES_BEFORE" = "$FILES_AFTER" ] && ok "deleted nothing ($FILES_BEFORE files before and after)" || bad "file count changed: $FILES_BEFORE -> $FILES_AFTER"
155
+
156
+ # nonexistent store is a clean 0-record listing, not an error
157
+ set +e
158
+ OUT2="$(node --experimental-strip-types "$PRUNE" "$TMP/does-not-exist" 2>&1)"
159
+ CODE2=$?
160
+ set -e
161
+ [ "$CODE2" = "0" ] && echo "$OUT2" | grep -q "does not exist yet" && ok "missing store -> clean 0-record exit 0" || bad "missing store not handled cleanly"
162
+
163
+ # custom ttl moves the stale boundary: ttl 100d demotes the 40d-old record to keep
164
+ set +e
165
+ OUT3="$(node --experimental-strip-types "$PRUNE" "$STORE" --ttl-days 100 2>&1)"
166
+ set -e
167
+ echo "$OUT3" | grep -q "STALE recordUpdatedAt>100d (0):" && echo "$OUT3" | grep -q "KEEP (live/recent): 2 record" && ok "--ttl-days widens keep window" || bad "--ttl-days did not move stale boundary"
168
+
169
+ if [ "$fail" = "0" ]; then
170
+ echo "smoke-meta-prune PASS"
171
+ else
172
+ echo "smoke-meta-prune FAIL"
173
+ exit 1
174
+ fi