@junghanacs/entwurf 0.17.2 → 0.18.1
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 +6 -5
- package/CHANGELOG.md +299 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +9 -5
- package/VERIFY.md +3 -3
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +46 -12
- package/docs/setup-clean-host.md +35 -3
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +14 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +76 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +9 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +11 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +66 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +10 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +8 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +25 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +6 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +38 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +64 -4
- package/mcp/entwurf-bridge/src/index.ts +16 -8
- package/package.json +10 -9
- package/pi-extensions/entwurf-control.ts +12 -2
- package/pi-extensions/lib/acp/acp-client.ts +3 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
- package/pi-extensions/lib/acp/backend.ts +3 -3
- package/pi-extensions/lib/acp/event-mapper.ts +5 -5
- package/pi-extensions/lib/entwurf-deliverability.ts +104 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +20 -6
- package/pi-extensions/lib/entwurf-facts.ts +52 -3
- package/pi-extensions/lib/entwurf-peer-observe.ts +77 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +12 -1
- package/pi-extensions/lib/entwurf-v2-decider.ts +26 -8
- package/pi-extensions/lib/entwurf-v2-production.ts +32 -12
- package/pi-extensions/lib/entwurf-v2-surface.ts +6 -0
- package/pi-extensions/lib/meta-session.ts +44 -0
- package/pi-extensions/meta-bridge-hook.ts +72 -3
- package/run.sh +146 -56
- package/scripts/check-acp-usage-accounting.ts +9 -9
- package/scripts/check-bridge-delivery.ts +42 -1
- package/scripts/check-entwurf-fact-provider.ts +22 -1
- package/scripts/check-entwurf-facts.ts +17 -2
- package/scripts/check-entwurf-peers-surface.ts +20 -2
- package/scripts/check-entwurf-self-address.ts +20 -2
- package/scripts/check-entwurf-v2-decider.ts +6 -3
- package/scripts/check-entwurf-v2-matrix.ts +10 -5
- package/scripts/check-entwurf-v2-production.ts +72 -1
- package/scripts/check-gate-qualification.ts +63 -12
- package/scripts/check-meta-hook-session-switch.ts +560 -0
- package/scripts/check-release-gate-outcomes.ts +160 -3
- package/scripts/meta-bridge-doctor.sh +29 -4
- package/scripts/mutants/bridge-command-boot.json +3 -3
- package/scripts/mutants/gate-qualification.json +28 -0
- package/scripts/mutants/meta-hook-session-switch.json +223 -0
- package/scripts/mutants/pack-install.json +14 -3
- package/scripts/mutants/release-gate.json +22 -0
- package/scripts/raw-acp-compaction-measure/README.md +77 -0
- package/scripts/raw-acp-compaction-measure/probe.ts +196 -0
- package/scripts/raw-claude-session-switch/README.md +170 -0
- package/scripts/smoke-acp-cortex-live.ts +22 -2
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-v2-send-live.ts +23 -2
- package/scripts/smoke-entwurf-chain-live.ts +85 -13
- package/scripts/smoke-entwurf-v2-matrix-live.ts +23 -1
- package/scripts/smoke-mux-fresh-call-live.ts +13 -3
package/run.sh
CHANGED
|
@@ -144,6 +144,7 @@ Usage:
|
|
|
144
144
|
./run.sh check-meta-mailbox-state-write # deterministic gate (0.11 Stage 0 step 3D-4 commit2): post-cut receipt is state-only — meta-record file byte-identical across enqueue/read, state carries lastEnqueuedAt/lastReadAt (field isolation), empty inbox no-op on record+state, drift surfaces; no API
|
|
145
145
|
./run.sh check-meta-receiver-marker # deterministic gate (SE-2): receiver marker round-trip/start-key/provenance, UserPromptSubmit cannot mint presence, reader does not gate on record existence — marker SEMANTICS only; launch topology moved to check-hook-launch-topology
|
|
146
146
|
./run.sh check-hook-launch-topology # #51 gate 1: shipped hooks.json is exec form through hook-launch.sh, launcher is loud on an empty argv (older Claude's silent args drop), exec preserves the pid so the hook's parent is Claude, and a space/$/backtick plugin path survives as one argv element
|
|
147
|
+
./run.sh check-meta-hook-session-switch # #101 gate: ONE Claude pid, TWO SessionStarts (resume picker placeholder -> resumed id). The hook retires the marker of the garden it stopped serving (records untouched, same-garden re-registration retires nothing), the reader stays fail-closed on a re-planted stale marker (watchArmed measured against the owner's sender marker, never copied), production dispatch refuses the retired garden and NAMES the failing axis, and exactly one of the two gardens is deliverable. Sandboxed roots, no API
|
|
147
148
|
./run.sh check-meta-identity-consumers # deterministic gate: V3-only consumer seam — per-entry targeted read + addressable read snapshot uniqueness, non-regular rivals never read, drift/unparseable rivals unreachable, unreadable regular rivals fail loud; strict upsert refuses an unreadable store before any write, no API
|
|
148
149
|
./run.sh check-meta-capability-source # deterministic gate (0.11 Stage 0 step 3D-3): capability-source cut-over — mint/parse read wakeMode/deliveryLevel from the registry (metaCapabilityFor, registry-driven via injection), not META_BACKEND_DESCRIPTORS; behaviour-preserving (registry ≡ const); the record.delivery slot 3D-3 preserved was deleted by 3D-4, no API
|
|
149
150
|
./run.sh check-socket-probe # deterministic gate (0.11 Stage 0, F3): three-valued control-socket liveness (alive|dead|indeterminate) — GC reclaims dead only, indeterminate survives; pure classify + 2-socket integration, no API
|
|
@@ -191,6 +192,7 @@ Usage:
|
|
|
191
192
|
./run.sh smoke-omp-receive-live # #87 bundle B acceptance: another harness wakes an ALREADY-OPEN omp citizen, which reads its inbox and answers in the same session. Decides its own outcome from the capability registry — omp without a drainable mailbox is a protocol SKIP (no receiver unit exists to arm), which an unattended release-gate reports and `--cut` reads as RED; a registry that claims a receive rail with no acceptance body here is a FAIL
|
|
192
193
|
./run.sh smoke-entwurf-chain-live # LIVE cross-harness delivery chain: native Claude Code -> pi GPT -> pi ACP Sonnet -> mailbox terminus, proving sender identity/replyable at every hop and a real read receipt at the end. Prerequisites (claude on PATH, pi credentials per backend) report protocol SKIP, never a pass
|
|
193
194
|
./run.sh check-release-gate-outcomes # release-gate STEP OUTCOME protocol (P1): one skip exit code shared by the shell + TS halves, classifier never rounds a skip up to a pass, `--cut` refuses a MUST SKIP while a bare diagnostic stays exit 0, no LIVE smoke keeps the old exit-0 skip shape, and both real skip surfaces are INVOKED and observed to propagate the code
|
|
195
|
+
./run.sh check-gate-manifests # the HEAD of that qualification, on its own: runner self-test + the committed manifests validated against the origin index + the declared lane inventory. Executes ZERO mutants and never snapshots this repo (~8s), which is why it lives in check:hermetic while the body stays on CI/release-gate — three of the five reds qualification ever produced in CI died right here, before a mutant gate ran (#99 B-3)
|
|
194
196
|
./run.sh check-gate-qualification # kill-proof qualification (the gate-of-gates): runner self-test (classifier truth table + synthetic negatives incl. wrong-reason/hang/control-red/impurity) + committed mutant manifests (scripts/mutants/*.json) run in an isolated snapshot repo under control→mutant→restore→control; the real checkout is never written. Evidence = claim IDs + killed mutant IDs, never assertion counts
|
|
195
197
|
./run.sh check-probe-ordering # §11-7 ordering-probe deterministic gate: raw-client SAMENESS pinned to backend.ts (sequence/args/timeouts/permission policy — the probe may never measure a lookalike), phase attribution incl. set-model, probe-mode fixture wire markers (delay honored, probeRunId REQUIRED, smoke-acp-mcp-live legacy compat), event-log door integrity (reserved keys refused at write; unknown marker name, broken sort axis, or a payload the classifier cannot judge on is MALFORMED, never a quiet event), and the §11-7 paired-verdict truth table (P0/I0 outside the space, phase-qualified D, B promotion ladder, C, A two-delay rule). Offline/deterministic; one product claim (no production prompt cutoff) is replant-qualified via scripts/mutants/probe-ordering.json, while the other assertions are direct [CHECK:*] contracts
|
|
196
198
|
./run.sh check-probe-cli-shim # §11-7-c B-name-snapshot PRODUCER gate: the CLI shim driven as a REAL process against fake CLIs (no API, no cost). Proves what a defect would buy — FABRICATED evidence (a malformed init is never reported as an empty name set; the boot report carries the true target path+sha256 the classifier verifies against the roster), a DESTROYED turn (byte transparency across mid-UTF8/CRLF/oversized/unterminated framing, exit-code fidelity, signal re-raise, inbound signal forwarding, stderr passthrough, stdout backpressure), and LEAKED operator state (exact-allowlist env scrub, no argv/env/prompt body in the log). Offline/deterministic; 20 direct [CHECK:*] contracts, deliberately no longer replant-qualified after #70 subtraction
|
|
@@ -252,7 +254,7 @@ Usage:
|
|
|
252
254
|
./run.sh check-dep-versions # local deterministic check that the pi pin agrees across package.json (devDeps + peer range), run.sh (peer-install pins), and the baseline docs (AGENTS/README/ROADMAP/setup-clean-host/demo)
|
|
253
255
|
./run.sh check-node-floor-coherence # binds the Node floor (24+, single axis) across engines.node, run.sh setup preflight, meta-bridge install/doctor judgment logic, clean-host docs, the bridge launcher header, and the CI runner node-version — engines.node is the SSOT, everything else is derived; sweeps tracked contract text for an unregistered declaration
|
|
254
256
|
./run.sh check-pack # publish gate (dry-run): npm pack --dry-run + tarball invariants (runtime-critical present, dev residue absent)
|
|
255
|
-
./run.sh check-pack-pin-matcher # pure self-test of check-pack-install's pin-leak matcher against synthetic .pnpm lookalikes
|
|
257
|
+
./run.sh check-pack-pin-matcher # pure self-test of check-pack-install's pin-leak matcher against synthetic .pnpm lookalikes, one cell per property: version boundary (@0.85.10 must leak, @0.85.1 bare or with either measured peer-hash must pass) and closure prefix (an off-pin @earendil-works/chord must leak — it carries no `pi-` prefix); snapshot-safe qualification oracle, also run first inside check-pack-install
|
|
256
258
|
./run.sh check-fresh-cut-gate # SOURCE cell of the generation-boundary proof (IN pnpm run check:full): drives real install/setup/fresh-cut in a sandbox; certification refusal is pre-write, quiescence is fail-closed, archives preserve bytes, and the #54 exit matrix distinguishes complete / no-move / usage / incomplete transition / complete-with-cleanup-residue. No model/network/cost
|
|
257
259
|
./run.sh check-pack-install # heavy publish gate (prepublishOnly): actual npm pack + tar -tf + fresh-temp install smoke with the pinned pi peers (pins derived from the package.json devDep; check-dep-versions binds them) + the npm-installed bridge BOOTS (tools/list) and DELIVERS (tools/call entwurf_v2 → .msg lands) + the installed all-absent and copilot-present (four-unit fake-vendor) `entwurf setup` rows + the INSTALLED generation lifecycle on a seeded previous-generation host (REFUSE before activation writes / zero Claude invocations → installed fresh-cut archives + opens empty → install-meta-bridge PASSES) + the INSTALLED-PACKAGE branch of the Copilot and OMP birth installers actually RUN (compiled entry selected, no raw .ts, and a real birth edge mints a citizen — the half a required-artifact list can never stand in for)
|
|
258
260
|
./run.sh check-install-container # 0.12.8 (#51 C): Linux artifact-CONSUMER gate — one candidate .tgz handed read-only to a checkout-invisible node:<engines-major>-bookworm cell. Default packs once to temp; ENTWURF_CANDIDATE_TGZ=/absolute/preserved.tgz consumes those exact bytes with no re-pack and prints canonical path+sha256 for release. Non-root global PATH install, frozen package, MCP tools/list, fake-Claude install-meta-bridge, path+sha256 fence, strict doctor, and the GENERATION host-state matrix (clean / v3-only store bytes unchanged / previous-generation REFUSE→fresh-cut→retry PASS) seeded inline. Docker missing = honest SKIP; ENTWURF_REQUIRE_DOCKER=1 makes that RED (required CI)
|
|
@@ -894,6 +896,23 @@ check_meta_receiver_marker() {
|
|
|
894
896
|
run_ts scripts/check-meta-receiver-marker.ts
|
|
895
897
|
}
|
|
896
898
|
|
|
899
|
+
check_meta_hook_session_switch() {
|
|
900
|
+
# #101 gate: a Claude Code SESSION SWITCH — one native process that stops serving one
|
|
901
|
+
# garden and starts serving another. The resume picker (and `/clear`) fire SessionStart
|
|
902
|
+
# twice inside one pid: once for the TUI's placeholder id, once for the id the operator
|
|
903
|
+
# picked. Before this gate nothing asked "one owner pid, two gardens", so a placeholder
|
|
904
|
+
# citizen stayed registered with an armed-looking doorbell and real mail rotted in it.
|
|
905
|
+
# Plays Claude the way check-hook-launch-topology does (spawn the SHIPPED launcher with
|
|
906
|
+
# the manifest's argv, so the hook's parent is this process = one fake owner pid) with
|
|
907
|
+
# every meta root sandboxed. Asserts: both records survive (marker retired, never the
|
|
908
|
+
# record); the retired garden's marker is gone; a same-garden re-registration retires
|
|
909
|
+
# NOTHING; a re-planted stale marker is still undeliverable (the sender-marker join,
|
|
910
|
+
# fail-closed at the reader); production dispatch refuses it and carries the predicate's
|
|
911
|
+
# reason to the rendered surface; exactly one garden is deliverable and it is the served
|
|
912
|
+
# one. Real tmpdir, no API.
|
|
913
|
+
run_ts scripts/check-meta-hook-session-switch.ts
|
|
914
|
+
}
|
|
915
|
+
|
|
897
916
|
check_copilot_birth_hook() {
|
|
898
917
|
# #82 gate: the Copilot BIRTH path, proven without Copilot. Drives the REAL
|
|
899
918
|
# assembler (--assemble-only, into a temp dir), then fires the baked launcher the
|
|
@@ -1827,13 +1846,13 @@ assert.equal(peerTui, piAi,
|
|
|
1827
1846
|
// floor tracks the devDep pin so a consumer can't install against a pi lacking
|
|
1828
1847
|
// the public trust exports the bridge imports at the pinned minor, AND an upper
|
|
1829
1848
|
// bound at the next minor stops a fresh install from silently pulling a future
|
|
1830
|
-
// pi (past the declared ceiling — 0.
|
|
1849
|
+
// pi (past the declared ceiling — 0.86+ at the current 0.85.1 pin) whose
|
|
1831
1850
|
// internal export surface has drifted from the one we typecheck against.
|
|
1832
1851
|
// pi moves its public surface every minor (the 0.79→0.80 getModels→provider-
|
|
1833
1852
|
// factory churn is exactly this), so an open `>=` floor is exactly how the next
|
|
1834
1853
|
// installer re-acquires the drift. The floor is also the HARD MINIMUM a consumer
|
|
1835
|
-
// install resolves: at `>=0.
|
|
1836
|
-
// Expected shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.
|
|
1854
|
+
// install resolves: at `>=0.85.1` an existing 0.85.0 host is upgraded, not kept.
|
|
1855
|
+
// Expected shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.85.1 <0.86`).
|
|
1837
1856
|
const [piMaj, piMin] = piAi.split('.').map(Number);
|
|
1838
1857
|
assert.equal(piMaj, 0,
|
|
1839
1858
|
`pi pin major must stay 0 for the next-minor ceiling rule (got ${piAi}); revisit check-dep-versions when pi reaches 1.x`);
|
|
@@ -1860,7 +1879,11 @@ assert.equal(peerDepTui, expectedPeer,
|
|
|
1860
1879
|
// Scope is deliberately narrow: only sentences that DECLARE the pi pin. History
|
|
1861
1880
|
// (CHANGELOG/NEXT) keeps its old versions, and a pi mention without a version
|
|
1862
1881
|
// (an uninstall line, a type import) is not a declaration.
|
|
1863
|
-
|
|
1882
|
+
// VERIFY.md joined on 2026-09-06: it declared the pi range in prose that no gate
|
|
1883
|
+
// read, and sat two bumps stale (`>=0.84.3 <0.85` at a 0.85.1 pin) precisely
|
|
1884
|
+
// because it was outside this list. Its declaration is a plain range, so the
|
|
1885
|
+
// range scan below binds it — no new PROSE_DECL is needed for it.
|
|
1886
|
+
const BASELINE_DOCS = ['AGENTS.md', 'README.md', 'ROADMAP.md', 'docs/setup-clean-host.md', 'demo/README.md', 'VERIFY.md'];
|
|
1864
1887
|
let rangeDecls = 0, exactDecls = 0;
|
|
1865
1888
|
for (const file of BASELINE_DOCS) {
|
|
1866
1889
|
const text = readFileSync(file, 'utf8');
|
|
@@ -3096,35 +3119,65 @@ check_pack() {
|
|
|
3096
3119
|
}
|
|
3097
3120
|
|
|
3098
3121
|
# The pin-leak filter for the install tree's .pnpm listing, shared by the real scan and its
|
|
3099
|
-
# self-test below.
|
|
3100
|
-
#
|
|
3101
|
-
#
|
|
3102
|
-
#
|
|
3103
|
-
#
|
|
3122
|
+
# self-test below. TWO properties are load-bearing and each has its own cell below.
|
|
3123
|
+
#
|
|
3124
|
+
# (1) The version BOUNDARY. A pnpm .pnpm entry is `<name>@<version>` followed by either
|
|
3125
|
+
# `_<peer-hash>` or end-of-name (measured pnpm 11.20.0 on this tree), so an unbounded
|
|
3126
|
+
# substring match would bless a lookalike such as `@0.85.10` while announcing the pinned
|
|
3127
|
+
# floor — a false-green oracle (found by independent review, 2026-08-25).
|
|
3128
|
+
# (2) The name PREFIX covers the whole @earendil-works closure, not the `pi-` families alone.
|
|
3129
|
+
# 0.85.0 added `@earendil-works/chord`, declared as a runtime `dependencies` entry by
|
|
3130
|
+
# pi-coding-agent, pi-agent-core, pi-client and pi-protocol — inside the closure, with a
|
|
3131
|
+
# name that carries no `pi-` prefix. `[측정 2026-09-06]` a real 0.85.1 install tree lists
|
|
3132
|
+
# `@earendil-works+chord@0.85.1` next to the seven pi entries, and the old
|
|
3133
|
+
# `^@earendil-works+pi-` filter did not see it: chord would have floated silently while
|
|
3134
|
+
# this gate printed a verified pin. The prefix is the closure's org, not one family in it.
|
|
3135
|
+
# (The measured peer-hash suffix moved too — `_ws@8.21.3` at 0.85.1 where 0.84.4 carried
|
|
3136
|
+
# `_@modelcontextprotocol+sdk@…` — which is exactly what property (1)'s `(_|$)` absorbs;
|
|
3137
|
+
# both shapes are fixtures below so a future suffix change cannot pass vacuously.)
|
|
3104
3138
|
pack_install_leaked_pi() {
|
|
3105
|
-
grep '^@earendil-works+
|
|
3139
|
+
grep '^@earendil-works+' | grep -Ev '@0\.85\.1(_|$)' || true
|
|
3106
3140
|
}
|
|
3107
3141
|
|
|
3108
|
-
# Matcher self-test on SYNTHETIC lookalikes: a healthy install tree cannot exercise
|
|
3109
|
-
# false-green shape (it contains no 0.
|
|
3110
|
-
#
|
|
3111
|
-
#
|
|
3112
|
-
# check-pack-install cannot run inside
|
|
3113
|
-
# and a kill-proof needs a control-green
|
|
3114
|
-
# first so the heavy gate cannot proceed on a
|
|
3142
|
+
# Matcher self-test on SYNTHETIC lookalikes: a healthy install tree cannot exercise either
|
|
3143
|
+
# false-green shape (it contains no 0.85.10 and no off-pin chord), so the oracle is proven
|
|
3144
|
+
# against fixture listings. One cell per property of pack_install_leaked_pi, so a regression
|
|
3145
|
+
# in either is named by its own signature rather than folded into a single verdict. Exposed
|
|
3146
|
+
# as its own snapshot-safe subcommand because the heavy check-pack-install cannot run inside
|
|
3147
|
+
# the qualification snapshot (no install environment), and a kill-proof needs a control-green
|
|
3148
|
+
# gate there; check-pack-install still runs this first so the heavy gate cannot proceed on a
|
|
3149
|
+
# broken oracle.
|
|
3115
3150
|
check_pack_pin_matcher() {
|
|
3116
3151
|
local matcher_probe
|
|
3152
|
+
|
|
3153
|
+
# Cell 1 — the version boundary. Both measured peer-hash shapes are fixtures: the
|
|
3154
|
+
# 0.84.4-era `_@modelcontextprotocol+sdk@…` and the `_ws@8.21.3` observed on a real
|
|
3155
|
+
# 0.85.1 tree. Neither may leak; the two lookalikes must.
|
|
3117
3156
|
matcher_probe=$(printf '%s\n' \
|
|
3118
|
-
'@earendil-works+pi-ai@0.
|
|
3119
|
-
'@earendil-works+pi-ai@0.
|
|
3120
|
-
'@earendil-works+pi-ai@0.
|
|
3121
|
-
'@earendil-works+pi-
|
|
3122
|
-
|
|
3123
|
-
@earendil-works+pi-
|
|
3124
|
-
|
|
3157
|
+
'@earendil-works+pi-ai@0.85.1' \
|
|
3158
|
+
'@earendil-works+pi-ai@0.85.1_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
|
|
3159
|
+
'@earendil-works+pi-ai@0.85.1_ws@8.21.3' \
|
|
3160
|
+
'@earendil-works+pi-ai@0.85.10' \
|
|
3161
|
+
'@earendil-works+pi-agent-core@0.85.0' | pack_install_leaked_pi)
|
|
3162
|
+
if [ "$matcher_probe" != '@earendil-works+pi-ai@0.85.10
|
|
3163
|
+
@earendil-works+pi-agent-core@0.85.0' ]; then
|
|
3164
|
+
fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the 0.85.10/0.85.0 lookalikes and pass 0.85.1 bare or with either measured peer-hash — got: ${matcher_probe:-<nothing leaked>}"
|
|
3125
3165
|
return 1
|
|
3126
3166
|
fi
|
|
3127
|
-
|
|
3167
|
+
|
|
3168
|
+
# Cell 2 — the closure prefix. chord is a non-`pi-` member of the same runtime closure
|
|
3169
|
+
# (0.85.0 onward). An off-pin chord MUST leak; the pinned one must not. A matcher narrowed
|
|
3170
|
+
# back to `^@earendil-works+pi-` sees nothing here and dies at this signature.
|
|
3171
|
+
matcher_probe=$(printf '%s\n' \
|
|
3172
|
+
'@earendil-works+chord@0.85.1' \
|
|
3173
|
+
'@earendil-works+chord@0.85.0' \
|
|
3174
|
+
'@earendil-works+pi-ai@0.85.1' | pack_install_leaked_pi)
|
|
3175
|
+
if [ "$matcher_probe" != '@earendil-works+chord@0.85.0' ]; then
|
|
3176
|
+
fail "[QK:PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE] the pin-leak matcher must cover every @earendil-works closure member, not just the pi-* families — an off-pin @earendil-works/chord has to leak (it is a runtime dependency of pi-coding-agent/pi-agent-core/pi-client/pi-protocol at 0.85.x) — got: ${matcher_probe:-<nothing leaked>}"
|
|
3177
|
+
return 1
|
|
3178
|
+
fi
|
|
3179
|
+
|
|
3180
|
+
echo "[check-pack-pin-matcher] ok — the pin-leak matcher is version-bounded (lookalikes leak, pinned version passes bare and with either measured peer-hash) and covers the whole @earendil-works closure (chord included)"
|
|
3128
3181
|
}
|
|
3129
3182
|
|
|
3130
3183
|
_check_pack_install_impl() {
|
|
@@ -3351,7 +3404,7 @@ _check_pack_install_impl() {
|
|
|
3351
3404
|
printf '%s\n' '{ "name": "entwurf-install-smoke", "version": "0.0.0", "private": true }' > "$tmp/package.json"
|
|
3352
3405
|
|
|
3353
3406
|
# pi-agent-core is pinned even though we never import it: pi-coding-agent depends
|
|
3354
|
-
# on it by CARET (`^0.
|
|
3407
|
+
# on it by CARET (`^0.85.x`), so with no lockfile in this fresh temp project it
|
|
3355
3408
|
# floats to whatever pi published last — and that newer core then drags a NESTED
|
|
3356
3409
|
# pi-ai of its own. Measured 2026-07-21: pinning only the three we import left
|
|
3357
3410
|
# pi-agent-core@0.80.10 + pi-ai@0.80.10 in the tree while the gate still announced
|
|
@@ -3369,21 +3422,30 @@ _check_pack_install_impl() {
|
|
|
3369
3422
|
# `^0.84.x` carets on it), and upstream's 0.84.4 patch publish (2026-08-28
|
|
3370
3423
|
# 22:04Z) floated that caret in this lockfile-less temp install, turning CI
|
|
3371
3424
|
# red through the leak assertion below. That explicit pin is what held the
|
|
3372
|
-
# line until the bump lane ran; the
|
|
3373
|
-
#
|
|
3374
|
-
#
|
|
3375
|
-
#
|
|
3376
|
-
|
|
3425
|
+
# line until the bump lane ran; the pin moved WITH each verified floor rather
|
|
3426
|
+
# than being retired.
|
|
3427
|
+
#
|
|
3428
|
+
# At 0.85.1 the constellation GREW AGAIN, and this time outside the `pi-`
|
|
3429
|
+
# naming: `@earendil-works/chord` is new in 0.85.0 and is a runtime
|
|
3430
|
+
# `dependencies` entry of pi-coding-agent, pi-agent-core, pi-client and
|
|
3431
|
+
# pi-protocol (`[측정 2026-09-06]` `npm view <pkg>@0.85.1 dependencies`), so an
|
|
3432
|
+
# unpinned `^0.85.x` caret on it floats exactly like pi-agent-core did in 2026-07-21.
|
|
3433
|
+
# It is pinned here for the first time. The leak assertion below covers every
|
|
3434
|
+
# other @earendil-works package too — but only because the matcher's prefix was
|
|
3435
|
+
# widened from `pi-` to the org in the same change; before that it was blind to
|
|
3436
|
+
# precisely this class of new member. The verified floor is 0.85.1 as of 2026-09-06.
|
|
3437
|
+
echo "[check-pack-install] pnpm add into $tmp (with 0.85.x peers + chord + typebox)"
|
|
3377
3438
|
local install_log
|
|
3378
3439
|
install_log=$(cd "$tmp" && pnpm add \
|
|
3379
3440
|
"$tgz_path" \
|
|
3380
|
-
"@earendil-works/pi-ai@0.
|
|
3381
|
-
"@earendil-works/pi-coding-agent@0.
|
|
3382
|
-
"@earendil-works/pi-tui@0.
|
|
3383
|
-
"@earendil-works/pi-agent-core@0.
|
|
3384
|
-
"@earendil-works/pi-client@0.
|
|
3385
|
-
"@earendil-works/pi-protocol@0.
|
|
3386
|
-
"@earendil-works/pi-telemetry@0.
|
|
3441
|
+
"@earendil-works/pi-ai@0.85.1" \
|
|
3442
|
+
"@earendil-works/pi-coding-agent@0.85.1" \
|
|
3443
|
+
"@earendil-works/pi-tui@0.85.1" \
|
|
3444
|
+
"@earendil-works/pi-agent-core@0.85.1" \
|
|
3445
|
+
"@earendil-works/pi-client@0.85.1" \
|
|
3446
|
+
"@earendil-works/pi-protocol@0.85.1" \
|
|
3447
|
+
"@earendil-works/pi-telemetry@0.85.1" \
|
|
3448
|
+
"@earendil-works/chord@0.85.1" \
|
|
3387
3449
|
"typebox@latest" \
|
|
3388
3450
|
--ignore-workspace --ignore-scripts 2>&1) || {
|
|
3389
3451
|
fail "[check-pack-install] pnpm add failed:"
|
|
@@ -3392,18 +3454,18 @@ _check_pack_install_impl() {
|
|
|
3392
3454
|
}
|
|
3393
3455
|
|
|
3394
3456
|
# A pin is a wish until the resolved tree is read back. Assert it: EVERY
|
|
3395
|
-
# @earendil-works
|
|
3396
|
-
# must be the pinned 0.
|
|
3397
|
-
# rest of this gate would be exercising a runtime nobody verified,
|
|
3398
|
-
# printing "pinned pi 0.
|
|
3457
|
+
# @earendil-works package present — direct or transitive, top level or nested,
|
|
3458
|
+
# chord included — must be the pinned 0.85.1. Anything else means an unpinned caret
|
|
3459
|
+
# floated and the rest of this gate would be exercising a runtime nobody verified,
|
|
3460
|
+
# while still printing "pinned pi 0.85.1". Fail loud instead of proving the wrong floor.
|
|
3399
3461
|
local leaked_pi
|
|
3400
3462
|
leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | pack_install_leaked_pi)
|
|
3401
3463
|
if [ -n "$leaked_pi" ]; then
|
|
3402
|
-
fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.
|
|
3464
|
+
fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.85.1):"
|
|
3403
3465
|
printf '%s\n' "$leaked_pi" | sed 's/^/ /' >&2
|
|
3404
3466
|
return 1
|
|
3405
3467
|
fi
|
|
3406
|
-
echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works
|
|
3468
|
+
echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 0.85.1 (chord included)"
|
|
3407
3469
|
|
|
3408
3470
|
# Resolve the installed package.json and confirm pi.extensions
|
|
3409
3471
|
# arrived intact. If pi.extensions is empty or missing, the
|
|
@@ -3701,10 +3763,23 @@ sys.exit(0 if any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in src
|
|
|
3701
3763
|
# SAME installed candidate, on a FRESH sandbox home with every harness absent.
|
|
3702
3764
|
# This is the first living consumer of package-installed `entwurf setup`:
|
|
3703
3765
|
# mode=installed decided by name before anything else, NO pnpm bootstrap inside
|
|
3704
|
-
# node_modules, pi/claude/agy explicit zero-state SKIPs, stable bins
|
|
3705
|
-
# npm-provided, core bridge boundary PASS, computed green, zero
|
|
3706
|
-
# writes. Harness probes are pinned absent via the
|
|
3707
|
-
# COPILOT_BIN seams so the gate host's real
|
|
3766
|
+
# node_modules, pi/claude/agy/copilot/omp explicit zero-state SKIPs, stable bins
|
|
3767
|
+
# PASS as npm-provided, core bridge boundary PASS, computed green, zero
|
|
3768
|
+
# harness/auth writes. Harness probes are pinned absent via the FIVE
|
|
3769
|
+
# PI_BIN/CLAUDE_BIN/AGY_BIN/COPILOT_BIN/OMP_BIN seams so the gate host's real
|
|
3770
|
+
# harnesses never leak in.
|
|
3771
|
+
#
|
|
3772
|
+
# OMP_BIN was MISSING here from the 0.16.0 OMP admission until 2026-09-06, while
|
|
3773
|
+
# the two sibling fixtures (`smoke-setup-verdict.sh:70`,
|
|
3774
|
+
# `check-setup-qualification.sh:76`) both carried it. On a host that HAS omp on
|
|
3775
|
+
# PATH the row's own premise was false: setup detected omp, and the omp config /
|
|
3776
|
+
# receiver installers then refused — correctly — because this row also exports
|
|
3777
|
+
# `PI_CODING_AGENT_DIR`, which omp reads too, so the target directory is
|
|
3778
|
+
# ambiguous. Nothing was wrong with the product; the fixture was. It stayed
|
|
3779
|
+
# invisible because CI runners carry no omp AND `release_gate()` does not run
|
|
3780
|
+
# this gate (it is prepublishOnly + the CI install-surface job). The skip probe
|
|
3781
|
+
# below now requires FIVE zero-states, so a sixth harness cannot be admitted
|
|
3782
|
+
# while this seam is quietly left behind again.
|
|
3708
3783
|
local setup_home="$npm_tmp/setuphome" setup_proj="$npm_tmp/setupproj" setup_out setup_rc setup_auth
|
|
3709
3784
|
mkdir -p "$setup_home/.pi/agent" "$setup_proj"
|
|
3710
3785
|
setup_auth="$setup_home/.pi/agent/auth.json"
|
|
@@ -3715,7 +3790,7 @@ sys.exit(0 if any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in src
|
|
|
3715
3790
|
setup_out=$(HOME="$setup_home" XDG_DATA_HOME="$setup_home/.local/share" XDG_STATE_HOME="$setup_home/.local/state" \
|
|
3716
3791
|
XDG_CACHE_HOME="$setup_home/.cache" XDG_CONFIG_HOME="$setup_home/.config" PI_CODING_AGENT_DIR="$setup_home/.pi/agent" \
|
|
3717
3792
|
PI_BIN="$npm_tmp/definitely-absent" CLAUDE_BIN="$npm_tmp/definitely-absent" AGY_BIN="$npm_tmp/definitely-absent" \
|
|
3718
|
-
COPILOT_BIN="$npm_tmp/definitely-absent" \
|
|
3793
|
+
COPILOT_BIN="$npm_tmp/definitely-absent" OMP_BIN="$npm_tmp/definitely-absent" \
|
|
3719
3794
|
"$npmroot/node_modules/.bin/entwurf" setup "$setup_proj" 2>&1)
|
|
3720
3795
|
setup_rc=$?
|
|
3721
3796
|
set -e
|
|
@@ -3735,7 +3810,7 @@ sys.exit(0 if any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in src
|
|
|
3735
3810
|
return 1
|
|
3736
3811
|
fi
|
|
3737
3812
|
local skip_probe
|
|
3738
|
-
for skip_probe in "pi: SKIP" "claude: SKIP" "agy: SKIP" "copilot: SKIP"; do
|
|
3813
|
+
for skip_probe in "pi: SKIP" "claude: SKIP" "agy: SKIP" "copilot: SKIP" "omp: SKIP"; do
|
|
3739
3814
|
if ! grep -q "$skip_probe" <<<"$setup_out"; then
|
|
3740
3815
|
fail "[check-pack-install] installed all-absent setup missing explicit zero-state '$skip_probe':"
|
|
3741
3816
|
echo "$setup_out" | tail -25 | sed 's/^/ /' >&2
|
|
@@ -3760,7 +3835,7 @@ sys.exit(0 if any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in src
|
|
|
3760
3835
|
fail "[check-pack-install] installed setup touched the credential store (byte drift or .bak)"
|
|
3761
3836
|
return 1
|
|
3762
3837
|
fi
|
|
3763
|
-
echo "[check-pack-install] installed all-absent setup pass (mode-first, no bootstrap,
|
|
3838
|
+
echo "[check-pack-install] installed all-absent setup pass (mode-first, no bootstrap, 5x SKIP, bins npm-provided, core PASS, computed green, zero writes)"
|
|
3764
3839
|
|
|
3765
3840
|
# Installed copilot-present aggregate `setup` row (#86 C3b): the SAME consumer
|
|
3766
3841
|
# bin on a fresh sandbox home, with the shared fake vendor
|
|
@@ -3786,6 +3861,7 @@ sys.exit(0 if any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in src
|
|
|
3786
3861
|
setup_out=$(HOME="$cop_setup_home" XDG_DATA_HOME="$cop_setup_home/.local/share" XDG_STATE_HOME="$cop_setup_home/.local/state" \
|
|
3787
3862
|
XDG_CACHE_HOME="$cop_setup_home/.cache" XDG_CONFIG_HOME="$cop_setup_home/.config" PI_CODING_AGENT_DIR="$cop_setup_home/.pi/agent" \
|
|
3788
3863
|
PI_BIN="$npm_tmp/definitely-absent" CLAUDE_BIN="$npm_tmp/definitely-absent" AGY_BIN="$npm_tmp/definitely-absent" \
|
|
3864
|
+
OMP_BIN="$npm_tmp/definitely-absent" \
|
|
3789
3865
|
COPILOT_BIN="$cop_fake/copilot" PATH="$cop_fake:$npmroot/node_modules/.bin:$PATH" \
|
|
3790
3866
|
"$npmroot/node_modules/.bin/entwurf" setup "$cop_setup_proj" 2>&1)
|
|
3791
3867
|
setup_rc=$?
|
|
@@ -5440,10 +5516,11 @@ release_gate() {
|
|
|
5440
5516
|
results+=("FAIL static (pnpm run check:full)"); failc=$((failc + 1))
|
|
5441
5517
|
fi
|
|
5442
5518
|
|
|
5443
|
-
# 1b. Discriminating power of that floor.
|
|
5444
|
-
# default check chains (operator inner-loop cost)
|
|
5445
|
-
#
|
|
5446
|
-
#
|
|
5519
|
+
# 1b. Discriminating power of that floor. The mutant-EXECUTING body left the
|
|
5520
|
+
# default check chains (operator inner-loop cost) — only its head rides along,
|
|
5521
|
+
# as check-gate-manifests inside check:hermetic — so release acceptance carries
|
|
5522
|
+
# the body explicitly as its own MUST step: a cut must re-prove the gates still
|
|
5523
|
+
# kill what they claim to kill.
|
|
5447
5524
|
section "release-gate step: check-gate-qualification"
|
|
5448
5525
|
if (cd "$REPO_DIR" && bash "$self" check-gate-qualification); then
|
|
5449
5526
|
ok "check-gate-qualification: PASS"
|
|
@@ -5700,6 +5777,9 @@ case "$cmd" in
|
|
|
5700
5777
|
check-meta-receiver-marker)
|
|
5701
5778
|
check_meta_receiver_marker
|
|
5702
5779
|
;;
|
|
5780
|
+
check-meta-hook-session-switch)
|
|
5781
|
+
check_meta_hook_session_switch
|
|
5782
|
+
;;
|
|
5703
5783
|
check-meta-identity-consumers)
|
|
5704
5784
|
check_meta_identity_consumers
|
|
5705
5785
|
;;
|
|
@@ -6015,6 +6095,16 @@ case "$cmd" in
|
|
|
6015
6095
|
# per cell; offline + deterministic (deps: python3).
|
|
6016
6096
|
(cd "$REPO_DIR" && python3 scripts/check-agy-permission-matrix.py)
|
|
6017
6097
|
;;
|
|
6098
|
+
check-gate-manifests)
|
|
6099
|
+
# The HEAD of check-gate-qualification, exposed on its own so the deterministic
|
|
6100
|
+
# floor can pay for it: the runner self-test (classifier truth table + synthetic
|
|
6101
|
+
# negatives) plus the REAL manifests — schema, global claim uniqueness, subjects
|
|
6102
|
+
# tracked and lstat-regular in the origin index, claim tokens exactly once in their
|
|
6103
|
+
# gate sources — plus the declared lane inventory. It executes ZERO mutants and
|
|
6104
|
+
# makes NO snapshot of this repo; the body (check-gate-qualification) owns that and
|
|
6105
|
+
# is unchanged. Cheap enough for check:hermetic (~8s).
|
|
6106
|
+
run_ts scripts/check-gate-qualification.ts --manifests-only
|
|
6107
|
+
;;
|
|
6018
6108
|
check-gate-qualification)
|
|
6019
6109
|
# Kill-proof qualification — the gate-of-gates. Proves the committed defect mutants
|
|
6020
6110
|
# (scripts/mutants/*.json) make their gates fail BOUNDED and FOR THE CLAIMED REASON
|
|
@@ -72,7 +72,7 @@ interface WireUsage {
|
|
|
72
72
|
* One `_meta.quota.token_count` row. The field NAMES deliberately differ from
|
|
73
73
|
* `PromptResponse.usage`: cache reads are `cachedInputTokens` here because the
|
|
74
74
|
* shape is shared with codex-acp, and `cachedWriteTokens` is Claude's extra
|
|
75
|
-
* sibling (read at claude-agent-acp 0.
|
|
75
|
+
* sibling (read at claude-agent-acp 0.75.1 `dist/acp-agent.js:6493-6502`).
|
|
76
76
|
* Reading a quota row with the `usage` field names silently yields zeros, so the
|
|
77
77
|
* fixture below spells the vendor's names out rather than reusing `WireUsage`.
|
|
78
78
|
*/
|
|
@@ -137,7 +137,7 @@ function makeFakeChild() {
|
|
|
137
137
|
*
|
|
138
138
|
* Each `prompt` call consumes the next TurnScript: it first pushes that turn's
|
|
139
139
|
* `usage_update` notification (the wire the running cost total actually arrives
|
|
140
|
-
* on — read at claude-agent-acp 0.
|
|
140
|
+
* on — read at claude-agent-acp 0.75.1 `dist/acp-agent.js:3467-3482`), then answers the
|
|
141
141
|
* prompt with that turn's `PromptResponse.usage` (the wire the turn aggregate
|
|
142
142
|
* arrives on). Both orderings are the real one: the notification precedes the
|
|
143
143
|
* response, because the SDK emits it from the `result` message that ENDS the turn.
|
|
@@ -393,8 +393,8 @@ try {
|
|
|
393
393
|
// AGENT LOOP only. `_meta.quota.model_usage` comes from `result.modelUsage` and
|
|
394
394
|
// also counts Task subagents, sidechains and INTERNAL CALLS SUCH AS COMPACTION;
|
|
395
395
|
// the vendor states its rows "can total more than `token_count`" and are "the
|
|
396
|
-
// fuller picture, not a decomposition of it" (read at claude-agent-acp 0.
|
|
397
|
-
// `dist/acp-agent.js:
|
|
396
|
+
// fuller picture, not a decomposition of it" (read at claude-agent-acp 0.75.1
|
|
397
|
+
// `dist/acp-agent.js:6468-6474`).
|
|
398
398
|
//
|
|
399
399
|
// The wide one is required, not merely nicer, because the DENOMINATOR already
|
|
400
400
|
// has that scope: turn cost is the adjacent diff of the backend's running total,
|
|
@@ -608,8 +608,8 @@ try {
|
|
|
608
608
|
// identity that produces the bound is a property of the MAIN AGENT LOOP's
|
|
609
609
|
// cache breakpoints. `_meta.quota.model_usage` is a WIDER scope: the vendor
|
|
610
610
|
// states those rows also count Task subagents, sidechains, and INTERNAL
|
|
611
|
-
// CALLS SUCH AS COMPACTION (read at claude-agent-acp 0.
|
|
612
|
-
// `dist/acp-agent.js:
|
|
611
|
+
// CALLS SUCH AS COMPACTION (read at claude-agent-acp 0.75.1
|
|
612
|
+
// `dist/acp-agent.js:6465-6485`). Mixing the two scopes inflates the bound
|
|
613
613
|
// through both remaining terms that mention cacheWrite:
|
|
614
614
|
// max(0, occupancy − cacheWrite) shrinks as wide cacheWrite grows, so
|
|
615
615
|
// less is subtracted; min(rawBound, cacheWrite) rises with it.
|
|
@@ -749,9 +749,9 @@ try {
|
|
|
749
749
|
// CELL 3 — a turn with NO cost notification holds the baseline.
|
|
750
750
|
//
|
|
751
751
|
// Measured upstream: the result-path `usage_update` carries cost (read at
|
|
752
|
-
// claude-agent-acp 0.
|
|
752
|
+
// claude-agent-acp 0.75.1 `dist/acp-agent.js:3467-3482`), while other
|
|
753
753
|
// `usage_update` paths can carry `used` without cost (for example the
|
|
754
|
-
// rate-limit path at `:
|
|
754
|
+
// rate-limit path at `:4269-4277`). A live thinkpad ledger shows such turns
|
|
755
755
|
// really occur. The honest handling is to HOLD the baseline so the amount lands
|
|
756
756
|
// in the NEXT diff: misattributed by turn, exact by session. Rebaselining to 0
|
|
757
757
|
// there would double-count the whole prefix.
|
|
@@ -831,7 +831,7 @@ try {
|
|
|
831
831
|
// CELL 4 — a DECREASING session total is never silently absorbed.
|
|
832
832
|
//
|
|
833
833
|
// `conversation_reset` switches the session to a fresh transcript (read at
|
|
834
|
-
// claude-agent-acp 0.
|
|
834
|
+
// claude-agent-acp 0.75.1 `dist/acp-agent.js:4282-4289`), but whether that
|
|
835
835
|
// changes `total_cost_usd` is an SDK-internal value we cannot observe here.
|
|
836
836
|
// A diff can therefore go negative in a session we are still holding.
|
|
837
837
|
// Absorbing it quietly would both misreport the turn and destroy the only
|
|
@@ -181,16 +181,40 @@ writeMetaReceiverMarker({
|
|
|
181
181
|
|
|
182
182
|
// RECEIVER — a different armed self-fetch citizen. A dead/unarmed target must remain
|
|
183
183
|
// mailbox-undeliverable, so this marker isolates artifact delivery from target liveness.
|
|
184
|
+
//
|
|
185
|
+
// It is owned by a DIFFERENT live pid than the sender, and that is not decoration (#101):
|
|
186
|
+
// a claude-code watch owner serves exactly one garden at a time, and deliverability now
|
|
187
|
+
// requires that owner's sender marker to still name the garden being addressed. Two
|
|
188
|
+
// citizens armed under ONE pid is a state the real world does not produce — it is what a
|
|
189
|
+
// session switch leaves behind — so a fixture that modelled it would be asking the
|
|
190
|
+
// artifact to deliver into a retired watch.
|
|
191
|
+
//
|
|
192
|
+
// The stand-in second native CLI is a spawned idle process, NOT this gate's parent: the
|
|
193
|
+
// subject bridge resolves its own sender identity through its ancestry, and an ancestor
|
|
194
|
+
// holding a second garden's marker is the "ambiguous sender identity" refusal the bridge
|
|
195
|
+
// already ships. A live pid outside that ancestry is what a real second CLI looks like.
|
|
196
|
+
// It exits on its own timer, and is killed in the outer finally.
|
|
197
|
+
const receiverOwner = spawn(process.execPath, ["-e", "setTimeout(() => {}, 300000)"], { stdio: "ignore" });
|
|
198
|
+
const receiverOwnerPid = receiverOwner.pid;
|
|
199
|
+
if (typeof receiverOwnerPid !== "number") throw new Error("could not spawn the stand-in receiver owner process");
|
|
184
200
|
const receiver = upsertMetaSession({
|
|
185
201
|
input: { backend: "claude-code", nativeSessionId: `bridge-delivery-receiver-${process.pid}`, cwd: tmp },
|
|
186
202
|
dir: sessionsDir,
|
|
187
203
|
});
|
|
188
204
|
const gid = receiver.record.gardenId;
|
|
205
|
+
writeMetaSenderMarker({
|
|
206
|
+
backend: "claude-code",
|
|
207
|
+
gardenId: gid,
|
|
208
|
+
nativeSessionId: receiver.record.nativeSessionId,
|
|
209
|
+
cwd: tmp,
|
|
210
|
+
ownerPid: receiverOwnerPid,
|
|
211
|
+
sendersDir,
|
|
212
|
+
});
|
|
189
213
|
writeMetaReceiverMarker({
|
|
190
214
|
gardenId: gid,
|
|
191
215
|
backend: "claude-code",
|
|
192
216
|
nativeSessionId: receiver.record.nativeSessionId,
|
|
193
|
-
ownerPid:
|
|
217
|
+
ownerPid: receiverOwnerPid,
|
|
194
218
|
armProvenance: "session-start",
|
|
195
219
|
receiversDir,
|
|
196
220
|
});
|
|
@@ -471,6 +495,20 @@ try {
|
|
|
471
495
|
await fsp.writeFile(receiverRecordFile, receiverRecordV3Bytes);
|
|
472
496
|
const anonSendersDir = path.join(tmp, "meta-senders-anon");
|
|
473
497
|
await fsp.mkdir(anonSendersDir, { recursive: true });
|
|
498
|
+
// The TARGET's owner marker still belongs in this dir (#101). Anonymity is about the
|
|
499
|
+
// CALLER — no marker under the bridge child's own owner pid — while deliverability asks
|
|
500
|
+
// a different question of a different pid: is the target's watch owner still serving the
|
|
501
|
+
// target's garden? Leaving it out would make every cell below fail as
|
|
502
|
+
// mailbox-undeliverable and prove nothing about the anonymous hatch. In production no
|
|
503
|
+
// one swaps this dir; the split exists only because this fixture does.
|
|
504
|
+
writeMetaSenderMarker({
|
|
505
|
+
backend: "claude-code",
|
|
506
|
+
gardenId: gid,
|
|
507
|
+
nativeSessionId: receiver.record.nativeSessionId,
|
|
508
|
+
cwd: tmp,
|
|
509
|
+
ownerPid: receiverOwnerPid,
|
|
510
|
+
sendersDir: anonSendersDir,
|
|
511
|
+
});
|
|
474
512
|
const oneShotV2 = async (
|
|
475
513
|
cellEnv: NodeJS.ProcessEnv,
|
|
476
514
|
message: string,
|
|
@@ -582,6 +620,9 @@ try {
|
|
|
582
620
|
try {
|
|
583
621
|
child?.kill("SIGTERM");
|
|
584
622
|
} catch {}
|
|
623
|
+
try {
|
|
624
|
+
receiverOwner.kill("SIGTERM");
|
|
625
|
+
} catch {}
|
|
585
626
|
await fsp.rm(tmp, { recursive: true, force: true });
|
|
586
627
|
}
|
|
587
628
|
|
|
@@ -19,7 +19,12 @@
|
|
|
19
19
|
* socket-dir-read-error diagnostic (P2e②),
|
|
20
20
|
* - diagnostics are kind-tagged and sorted.
|
|
21
21
|
*
|
|
22
|
-
* No IO — meta entries/reader and socket dir/readdir/probe are injected fakes
|
|
22
|
+
* No IO — meta entries/reader and socket dir/readdir/probe are injected fakes, and so is
|
|
23
|
+
* the #101 `observe` seam. That seam is the one dep whose PRODUCTION default is real IO
|
|
24
|
+
* (one default is safer than two wiring sites each passing their own observer), so a gate
|
|
25
|
+
* that left it out would quietly stat the operator's real garden roots for fabricated
|
|
26
|
+
* garden ids. It is injected here for that reason, and the cell below proves the injected
|
|
27
|
+
* observation actually reaches the fact rather than being dropped on the way.
|
|
23
28
|
*/
|
|
24
29
|
|
|
25
30
|
import assert from "node:assert/strict";
|
|
@@ -74,6 +79,11 @@ function deps(
|
|
|
74
79
|
): EntwurfFactsDeps {
|
|
75
80
|
const symlinkSet = new Set(opts.symlinks ?? []);
|
|
76
81
|
return {
|
|
82
|
+
// Injected observation (see the header): a per-identity fake, never the real IO.
|
|
83
|
+
observe: (identity) => ({
|
|
84
|
+
receiver: identity.backend === "claude-code" ? "inactive" : "n/a",
|
|
85
|
+
transcript: identity.gardenId === GID_CLAUDE ? "absent" : "exists",
|
|
86
|
+
}),
|
|
77
87
|
// Kind-carrying entries, like the real bindings: the listing must be able to refuse a
|
|
78
88
|
// symlinked record without following it. `irregularMeta` names the ones that are not
|
|
79
89
|
// regular files.
|
|
@@ -144,6 +154,17 @@ async function main(): Promise<void> {
|
|
|
144
154
|
);
|
|
145
155
|
ok("basic: record-less-socket diagnostic keyset exact", true);
|
|
146
156
|
}
|
|
157
|
+
// #101: the injected observation rides onto each peer. A provider that dropped the
|
|
158
|
+
// seam would return `unobserved` rows — visible, but useless to the caller the
|
|
159
|
+
// columns exist for.
|
|
160
|
+
{
|
|
161
|
+
const claude = r.facts.peers.find((p) => p.gardenId === GID_CLAUDE);
|
|
162
|
+
const pi = r.facts.peers.find((p) => p.gardenId === GID_PI);
|
|
163
|
+
ok(
|
|
164
|
+
"basic: the injected observation reaches every peer fact",
|
|
165
|
+
claude?.receiver === "inactive" && claude?.transcript === "absent" && pi?.receiver === "n/a",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
147
168
|
// A DEAD record-less socket groups under a different (stale) message — no fresh-cut
|
|
148
169
|
// pointer for a leftover file, so same-state sockets aggregate per liveness.
|
|
149
170
|
const r2 = await listEntwurfFacts(deps({}, { [GID_SOCKET_ONLY]: "dead" }));
|
|
@@ -138,7 +138,8 @@ for (const backend of META_CITIZEN_BACKENDS) {
|
|
|
138
138
|
ok("passthrough recordUpdatedAt", fact.recordUpdatedAt === id.recordUpdatedAt);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
// ── facts-only keyset: identity facts + liveness
|
|
141
|
+
// ── facts-only keyset: identity facts + liveness + the two observed axes, and
|
|
142
|
+
// still NO verb-routing / no transcript PATH ────────────────────────────────
|
|
142
143
|
{
|
|
143
144
|
const fact = resolvePeerFact(identity("pi"), "alive");
|
|
144
145
|
const keys = Object.keys(fact).sort();
|
|
@@ -151,9 +152,23 @@ for (const backend of META_CITIZEN_BACKENDS) {
|
|
|
151
152
|
"model",
|
|
152
153
|
"nativeSessionId",
|
|
153
154
|
"recordUpdatedAt",
|
|
155
|
+
"receiver",
|
|
156
|
+
"transcript",
|
|
154
157
|
].sort();
|
|
155
158
|
assert.deepStrictEqual(keys, expected, `PeerFact keyset drift: got ${keys.join(",")}`);
|
|
156
|
-
ok("facts-only keyset exact (identity facts + liveness)", true);
|
|
159
|
+
ok("facts-only keyset exact (identity facts + liveness + observed receiver/transcript)", true);
|
|
160
|
+
|
|
161
|
+
// #101: an unmeasured row says so. `unobserved` is the default for a caller that
|
|
162
|
+
// injected no observer — never `none`/`absent`, which would be a fabricated fact.
|
|
163
|
+
ok(
|
|
164
|
+
"an observer-less composition reports both axes as unobserved (never a default fact)",
|
|
165
|
+
fact.receiver === "unobserved" && fact.transcript === "unobserved",
|
|
166
|
+
);
|
|
167
|
+
const observed = resolvePeerFact(identity("claude-code"), null, { receiver: "inactive", transcript: "absent" });
|
|
168
|
+
ok(
|
|
169
|
+
"an injected observation rides onto the fact verbatim",
|
|
170
|
+
observed.receiver === "inactive" && observed.transcript === "absent",
|
|
171
|
+
);
|
|
157
172
|
|
|
158
173
|
const FORBIDDEN = [
|
|
159
174
|
"resumable",
|
|
@@ -47,7 +47,12 @@ const FORBIDDEN = ["sendable", "resumable", "dispatch", "action", "transport", "
|
|
|
47
47
|
// #50 C4: socket-shaped identity must not reappear on this surface.
|
|
48
48
|
const FORBIDDEN_C4_KEYS = ["sessions", "socketOnly", "controlDir", "socketPath", "count"];
|
|
49
49
|
|
|
50
|
-
function peer(
|
|
50
|
+
function peer(
|
|
51
|
+
gardenId: string,
|
|
52
|
+
backend: MetaCitizenBackend,
|
|
53
|
+
liveness: FactLiveness,
|
|
54
|
+
observed: Partial<Pick<PeerFact, "receiver" | "transcript">> = {},
|
|
55
|
+
): PeerFact {
|
|
51
56
|
return {
|
|
52
57
|
gardenId,
|
|
53
58
|
backend,
|
|
@@ -57,6 +62,8 @@ function peer(gardenId: string, backend: MetaCitizenBackend, liveness: FactLiven
|
|
|
57
62
|
createdAt: "2026-06-11T00:00:00.000Z",
|
|
58
63
|
recordUpdatedAt: "2026-06-11T00:00:00.000Z",
|
|
59
64
|
liveness,
|
|
65
|
+
receiver: observed.receiver ?? "n/a",
|
|
66
|
+
transcript: observed.transcript ?? "exists",
|
|
60
67
|
};
|
|
61
68
|
}
|
|
62
69
|
|
|
@@ -91,7 +98,7 @@ function main(): void {
|
|
|
91
98
|
peer(GID_PI_ALIVE, "pi", "alive"),
|
|
92
99
|
peer(GID_PI_DEAD, "pi", "dead"),
|
|
93
100
|
peer(GID_PI_INDET, "pi", "indeterminate"),
|
|
94
|
-
peer(GID_CLAUDE, "claude-code", "unsupported"),
|
|
101
|
+
peer(GID_CLAUDE, "claude-code", "unsupported", { receiver: "inactive", transcript: "absent" }),
|
|
95
102
|
],
|
|
96
103
|
// The provider has already folded these into diagnostics; the render layer
|
|
97
104
|
// never re-renders them as a section (facts carry them for the union math).
|
|
@@ -116,6 +123,17 @@ function main(): void {
|
|
|
116
123
|
ok("C4: payload is exactly { peers, diagnostics }", true);
|
|
117
124
|
}
|
|
118
125
|
ok("peers carry all 4 citizens (unsupported NOT dropped)", payload.peers.length === 4);
|
|
126
|
+
// #101: the two observed axes are the only thing separating two same-cwd claude rows,
|
|
127
|
+
// since `liveness` reads `unsupported` for every one of them. They are FACTS on the
|
|
128
|
+
// row — the forbidden-word scans below still apply to them unchanged.
|
|
129
|
+
ok(
|
|
130
|
+
"observed receiver + transcript ride the row in text",
|
|
131
|
+
new RegExp(`${GID_CLAUDE}.*receiver=inactive.*transcript=absent`).test(text),
|
|
132
|
+
);
|
|
133
|
+
ok(
|
|
134
|
+
"observed axes ride the JSON payload too",
|
|
135
|
+
payload.peers.some((p) => p.gardenId === GID_CLAUDE && p.receiver === "inactive" && p.transcript === "absent"),
|
|
136
|
+
);
|
|
119
137
|
ok(
|
|
120
138
|
"unsupported citizen present in peers",
|
|
121
139
|
payload.peers.some((p) => p.gardenId === GID_CLAUDE && p.liveness === "unsupported"),
|