@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.
- package/AGENTS.md +240 -0
- package/BASELINE.md +227 -0
- package/CHANGELOG.md +1210 -0
- package/CONTRIBUTING.md +63 -0
- package/DELIVERY.md +209 -0
- package/LICENSE +21 -0
- package/README.md +504 -0
- package/VERIFY.md +260 -0
- package/demo/README.md +188 -0
- package/demo/demo-baseline.sh +156 -0
- package/demo/demo.sh +183 -0
- package/docs/assets/entwurf-demo.gif +0 -0
- package/docs/assets/entwurf-doomemacs.gif +0 -0
- package/docs/assets/entwurf-entwurf.gif +0 -0
- package/docs/assets/entwurf-hero.jpg +0 -0
- package/docs/setup-clean-host.md +305 -0
- package/mcp/entwurf-bridge/src/index.ts +513 -0
- package/mcp/entwurf-bridge/start.sh +25 -0
- package/mcp/entwurf-bridge/test.sh +54 -0
- package/mcp/tsconfig.json +29 -0
- package/package.json +130 -0
- package/pi/entwurf-capabilities.json +9 -0
- package/pi/entwurf-targets.json +20 -0
- package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
- package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
- package/pi/settings.reference.json +43 -0
- package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
- package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
- package/pi-extensions/acp-provider.ts +63 -0
- package/pi-extensions/entwurf-control.ts +1692 -0
- package/pi-extensions/lib/acp/acp-client.ts +90 -0
- package/pi-extensions/lib/acp/augment.ts +238 -0
- package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
- package/pi-extensions/lib/acp/backend.ts +873 -0
- package/pi-extensions/lib/acp/config.ts +552 -0
- package/pi-extensions/lib/acp/context.ts +177 -0
- package/pi-extensions/lib/acp/engraving.ts +123 -0
- package/pi-extensions/lib/acp/event-mapper.ts +339 -0
- package/pi-extensions/lib/acp/models.ts +102 -0
- package/pi-extensions/lib/acp/overlay.ts +220 -0
- package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
- package/pi-extensions/lib/acp/session-store.ts +418 -0
- package/pi-extensions/lib/acp/tool-surface.ts +184 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
- package/pi-extensions/lib/entwurf-core.ts +2033 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
- package/pi-extensions/lib/entwurf-facts.ts +251 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
- package/pi-extensions/lib/entwurf-preflight.ts +247 -0
- package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
- package/pi-extensions/lib/entwurf-self-address.ts +117 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
- package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
- package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
- package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
- package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
- package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
- package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
- package/pi-extensions/lib/meta-session.ts +1951 -0
- package/pi-extensions/lib/project-trust-handler.ts +154 -0
- package/pi-extensions/lib/session-id.js +57 -0
- package/pi-extensions/lib/socket-discovery.ts +346 -0
- package/pi-extensions/lib/socket-probe.ts +92 -0
- package/pi-extensions/meta-bridge-hook.ts +250 -0
- package/pi-extensions/model-lock.ts +235 -0
- package/prompts/engraving.md +27 -0
- package/protocol.js +31 -0
- package/run.sh +2832 -0
- package/scripts/check-acp-backend-preflight.ts +130 -0
- package/scripts/check-acp-carrier-augment.ts +297 -0
- package/scripts/check-acp-config.ts +322 -0
- package/scripts/check-acp-event-mapper.ts +253 -0
- package/scripts/check-acp-overlay.ts +154 -0
- package/scripts/check-acp-prompt-builder.ts +191 -0
- package/scripts/check-acp-provider-surface.ts +159 -0
- package/scripts/check-acp-sdk-surface.ts +211 -0
- package/scripts/check-acp-session-reuse.ts +756 -0
- package/scripts/check-acp-session-store.ts +387 -0
- package/scripts/check-acp-tool-surface.ts +159 -0
- package/scripts/check-entwurf-bridge-boot.ts +199 -0
- package/scripts/check-entwurf-capabilities.ts +123 -0
- package/scripts/check-entwurf-control-rpc.ts +187 -0
- package/scripts/check-entwurf-deliverability.ts +158 -0
- package/scripts/check-entwurf-fact-provider.ts +316 -0
- package/scripts/check-entwurf-facts.ts +358 -0
- package/scripts/check-entwurf-mailbox-guard.ts +264 -0
- package/scripts/check-entwurf-peers-surface.ts +235 -0
- package/scripts/check-entwurf-resume-args.ts +149 -0
- package/scripts/check-entwurf-self-address.ts +209 -0
- package/scripts/check-entwurf-session-identity.ts +703 -0
- package/scripts/check-entwurf-v2-contract.ts +536 -0
- package/scripts/check-entwurf-v2-decider.ts +795 -0
- package/scripts/check-entwurf-v2-lock.ts +368 -0
- package/scripts/check-entwurf-v2-mailbox.ts +228 -0
- package/scripts/check-entwurf-v2-matrix.ts +437 -0
- package/scripts/check-entwurf-v2-production.ts +406 -0
- package/scripts/check-entwurf-v2-release.ts +245 -0
- package/scripts/check-entwurf-v2-runner.ts +452 -0
- package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
- package/scripts/check-entwurf-v2-send.ts +454 -0
- package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
- package/scripts/check-entwurf-v2-spawn.ts +398 -0
- package/scripts/check-entwurf-v2-surface.ts +352 -0
- package/scripts/check-keyset-overlap.py +120 -0
- package/scripts/check-mailbox-receipt-state.ts +170 -0
- package/scripts/check-meta-capability-source.ts +112 -0
- package/scripts/check-meta-dual-consumers.ts +154 -0
- package/scripts/check-meta-dual-read.ts +158 -0
- package/scripts/check-meta-listing.ts +138 -0
- package/scripts/check-meta-mailbox-state-write.ts +135 -0
- package/scripts/check-meta-migration.ts +212 -0
- package/scripts/check-meta-receiver-marker.ts +185 -0
- package/scripts/check-meta-record-v2.ts +191 -0
- package/scripts/check-meta-session.ts +673 -0
- package/scripts/check-model-lock.ts +408 -0
- package/scripts/check-package-source-routing.ts +253 -0
- package/scripts/check-pi-preflight.ts +304 -0
- package/scripts/check-project-trust-handler.ts +265 -0
- package/scripts/check-shell-quote.ts +121 -0
- package/scripts/check-socket-discovery.ts +428 -0
- package/scripts/check-socket-probe.ts +106 -0
- package/scripts/fixtures/probe-mcp-server.ts +33 -0
- package/scripts/gnew-rpc-drive.ts +211 -0
- package/scripts/lib/acp-child-cleanup.ts +116 -0
- package/scripts/meta-bridge-doctor.sh +315 -0
- package/scripts/meta-bridge-hook-log.sh +26 -0
- package/scripts/meta-bridge-install.sh +135 -0
- package/scripts/meta-bridge-prune.ts +199 -0
- package/scripts/meta-bridge-state.py +549 -0
- package/scripts/meta-bridge-statusline.sh +192 -0
- package/scripts/meta-bridge-store-doctor.ts +64 -0
- package/scripts/meta-bridge-uninstall.sh +39 -0
- package/scripts/new-session-id.ts +25 -0
- package/scripts/postinstall-chmod.cjs +58 -0
- package/scripts/raw-async-delivery/README.md +258 -0
- package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
- package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
- package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
- package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
- package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
- package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
- package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
- package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
- package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
- package/scripts/resolve-acp-bridge.ts +25 -0
- package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
- package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
- package/scripts/smoke-acp-mcp-live.ts +129 -0
- package/scripts/smoke-acp-memory-containment-live.ts +389 -0
- package/scripts/smoke-acp-overlay-live.ts +314 -0
- package/scripts/smoke-acp-provider-live.ts +162 -0
- package/scripts/smoke-acp-raw-turn-live.ts +261 -0
- package/scripts/smoke-acp-session-reuse-live.ts +172 -0
- package/scripts/smoke-acp-skill-live.ts +144 -0
- package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
- package/scripts/smoke-claude-native-resume-live.sh +198 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
- package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
- package/scripts/smoke-meta-async-drift.sh +171 -0
- package/scripts/smoke-meta-honesty.sh +147 -0
- package/scripts/smoke-meta-install-state.sh +403 -0
- package/scripts/smoke-meta-keyset-guard.sh +111 -0
- package/scripts/smoke-meta-prune.sh +174 -0
- package/scripts/smoke-resident-garden-guard.sh +433 -0
- package/scripts/smoke-session-id-name.ts +187 -0
- 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
|