@junghanacs/entwurf 0.12.8 → 0.12.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +20 -1
- package/DELIVERY.md +1 -1
- package/README.md +122 -10
- package/demo/README.md +2 -2
- package/docs/setup-clean-host.md +14 -3
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +9 -15
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +12 -12
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +8 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +15 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +483 -34
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +8 -3
- package/mcp/entwurf-bridge/dist/scripts/agy-imprint.js +14 -2
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +155 -29
- package/mcp/entwurf-bridge/src/index.ts +10 -12
- package/package.json +7 -7
- package/pi-extensions/entwurf-control.ts +12 -12
- package/pi-extensions/lib/acp/models.ts +12 -12
- package/pi-extensions/lib/entwurf-fact-provider.ts +9 -2
- package/pi-extensions/lib/entwurf-v2-production.ts +8 -2
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +8 -2
- package/pi-extensions/lib/meta-sender-identity.ts +15 -5
- package/pi-extensions/lib/meta-session.ts +526 -38
- package/pi-extensions/meta-bridge-hook.ts +8 -2
- package/run.sh +30 -26
- package/scripts/agy-imprint.ts +15 -1
- package/scripts/check-acp-session-reuse.ts +1 -1
- package/scripts/check-acp-session-store.ts +3 -3
- package/scripts/check-agy-sender-identity.ts +83 -0
- package/scripts/check-entwurf-control-rpc.ts +2 -2
- package/scripts/check-entwurf-fact-provider.ts +9 -1
- package/scripts/check-entwurf-facts.ts +1 -1
- package/scripts/check-entwurf-mailbox-guard.ts +6 -2
- package/scripts/check-entwurf-resume-args.ts +6 -6
- package/scripts/check-entwurf-session-identity.ts +7 -6
- package/scripts/check-entwurf-v2-production.ts +4 -2
- package/scripts/check-entwurf-v2-spawn-production.ts +2 -2
- package/scripts/check-fresh-cut-gate.sh +305 -4
- package/scripts/check-meta-identity-consumers.ts +501 -1
- package/scripts/check-meta-listing.ts +91 -9
- package/scripts/check-meta-receiver-marker.ts +54 -0
- package/scripts/check-model-lock.ts +1 -1
- package/scripts/meta-bridge-fresh-cut.ts +164 -28
- package/scripts/pi_settings_io.py +65 -0
- package/scripts/register-pi-package.py +183 -37
- package/scripts/register-pi-provider.py +68 -10
- package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
- package/scripts/smoke-meta-install-state.sh +1 -1
- package/scripts/smoke-pi-attach.ts +7 -2
- package/scripts/smoke-user-scope-citizen.sh +177 -0
|
@@ -49,6 +49,7 @@ import * as path from "node:path";
|
|
|
49
49
|
import {
|
|
50
50
|
defaultMetaMailboxDir,
|
|
51
51
|
defaultMetaSessionsDir,
|
|
52
|
+
isPlausibleOwnerPid,
|
|
52
53
|
type MetaReceiverArmProvenance,
|
|
53
54
|
upsertMetaSession,
|
|
54
55
|
writeMetaReceiverMarker,
|
|
@@ -140,7 +141,12 @@ const META_HOOK_LAUNCH_TOKEN = "hook-launch/v1";
|
|
|
140
141
|
* no token means we do not know what our parent is, so we claim nothing.
|
|
141
142
|
* 2. A PLAUSIBLE LIVE PARENT. A reparented orphan (ppid 0/1) is not an owner, and
|
|
142
143
|
* minting a marker for init would be exactly the "blind ancestor" false-positive
|
|
143
|
-
* the old ancestry walk existed to prevent.
|
|
144
|
+
* the old ancestry walk existed to prevent. The rule is `isPlausibleOwnerPid`,
|
|
145
|
+
* shared with the other writer and with every reader: this side already refused
|
|
146
|
+
* what all three readers accepted, and one host's leftover `ownerPid: 1` marker
|
|
147
|
+
* then blocked its fresh-cut until the file was deleted by hand (#53 A). A
|
|
148
|
+
* predicate only one layer knows is how that drift happened, so it is no longer
|
|
149
|
+
* written out by hand here.
|
|
144
150
|
*
|
|
145
151
|
* Failing closed costs only reply-addressability, and the doctor sees the ERROR. The
|
|
146
152
|
* opposite — a marker keyed to a transient wrapper — is a lie a sender acts on.
|
|
@@ -148,7 +154,7 @@ const META_HOOK_LAUNCH_TOKEN = "hook-launch/v1";
|
|
|
148
154
|
function resolveMetaHookOwnerPid(): number | null {
|
|
149
155
|
if (process.env[META_HOOK_LAUNCH_ENV] !== META_HOOK_LAUNCH_TOKEN) return null;
|
|
150
156
|
const ownerPid = process.ppid;
|
|
151
|
-
if (!
|
|
157
|
+
if (!isPlausibleOwnerPid(ownerPid)) return null;
|
|
152
158
|
return ownerPid;
|
|
153
159
|
}
|
|
154
160
|
|
package/run.sh
CHANGED
|
@@ -90,7 +90,7 @@ Usage:
|
|
|
90
90
|
./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
|
|
91
91
|
./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
|
|
92
92
|
./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
|
|
93
|
-
./run.sh check-meta-identity-consumers # deterministic gate: V3-only consumer seam — read
|
|
93
|
+
./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
|
|
94
94
|
./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
|
|
95
95
|
./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
|
|
96
96
|
./run.sh check-project-trust-handler # deterministic gate (0.11 Stage 0, Trust 2층): project_trust handler — decideProjectTrust matrix (escape=inherited-false+interactive+trust-here→{yes,remember:true}; non-interactive→undecided; never undefined) + adapter single-writer, fake prompt, no UI
|
|
@@ -112,7 +112,7 @@ Usage:
|
|
|
112
112
|
./run.sh smoke-agy-native-push-live # 봉인 8 LIVE acceptance for the native-push (agy) rail — OUT of pnpm check, needs LIVE=1 + AGY_CONVERSATION_ID (a live agy conversation). Drives the REAL antigravity adapter + register core + runEntwurfV2 (production deps): doctor-static preflight (dangling→FAIL, the ③ gate), probe route, register create/attach idempotency, fire→native-push delivered, post-send re-probe (D7 partial), owned-outcome→native-push-no-resume-authority, bogus-conv→native-push-probe-indeterminate. Meta-store isolated to a temp dir (only the agy round-trip is real; no real-store residue). LIVE=1 AGY_CONVERSATION_ID=<convId> ./run.sh smoke-agy-native-push-live
|
|
113
113
|
./run.sh check-entwurf-facts # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 1+2): PURE PeerFact core + resolveFactList union — R1 out-of-domain→unsupported, R3b pi 4-value, facts-only keyset; union: PeerFact + RecordLessSocketFact by gardenId (#50 C4: record-less socket = diagnostic subject, gid+liveness only), dormant→dead, F3 indeterminate preserved, non-pi+socket fail-loud; pure, no IO
|
|
114
114
|
./run.sh check-socket-discovery # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 3): SOCKET-axis scanSocketProbes — probes (dir sockets) ∪ (in-domain citizen canonical paths) 3-valued; dormant citizen no-file → dead (resumable, not unprobed), stall → indeterminate (F3), dir hygiene/dedup/missing-dir + e2e → resolveFactList; readdir/probe injected, no IO
|
|
115
|
-
./run.sh check-meta-listing # deterministic gate
|
|
115
|
+
./run.sh check-meta-listing # deterministic gate: META-STORE facts axis — kind-carrying entries; non-regular records are never read, parse/drift become diagnostics, duplicate nativeSessionId quarantines every rival but not unrelated citizens; strict throws / collect partial; pure injected IO
|
|
116
116
|
./run.sh check-entwurf-fact-provider # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 4b): ASSEMBLY listEntwurfFacts — listAllMetaIdentities→scanSocketProbes→pre-quarantine non-pi/socket conflicts→resolveFactList(clean)→{facts,diagnostics}; C-원칙: expected corruption (parse/collision)→diagnostics (listing survives), impossible invariant (dup/unprobed)→throw; collision quarantines BOTH PeerFact+socket; deps injected, no IO
|
|
117
117
|
./run.sh check-entwurf-peers-surface # deterministic gate (0.11 Stage 0 step 4, fact-provider slice 4c): MCP entwurf_peers RENDER renderEntwurfPeers (#50 C4) — payload keyset exactly {peers, diagnostics}; FORBIDDEN keys sessions/socketOnly/controlDir/socketPath/count + no .sock in text (socket is transport, never identity); record-less socket = aggregated record-less-socket diagnostic (F8, liveness-keyed message, alive names fresh-cut); NO verb-routing key (JSON deep scan) NOR word (text), diagnostics both surfaces, empty→(none), unsupported shown; WIRING guard: both surfaces call provider+render, getLiveSessions + /entwurf-sessions gone; facts fabricated, no IO
|
|
118
118
|
./run.sh check-entwurf-self-address # deterministic gate (SE-1/SE-2 slice 1): self-addressability honesty predicate computeSelfAddressability — pi replyable ⟺ live socket; meta ⟺ recordBacked ∧ ownerAlive ∧ watchArmed (regression-proof record-present rows); SOURCE GUARD buildStrictPiSenderEnvelope drops hardcoded replyable:true + existsSync-probes socket, entwurf_self renders alive vs expected. meta watchArmed wired in slice 2 (same release block)
|
|
@@ -150,13 +150,13 @@ Usage:
|
|
|
150
150
|
./run.sh uninstall-agy-hooks # honest inverse of install-agy-hooks from install-state
|
|
151
151
|
./run.sh doctor-agy-hooks # fail-loud doctor for agy hooks.json imprint wiring
|
|
152
152
|
./run.sh meta-bridge-prune # 1.0.0 meta-bridge Phase 4: LISTING-ONLY store hygiene — classify orphan/stale/ambiguous/keep, print manual rm commands, delete NOTHING ([dir] [--ttl-days N])
|
|
153
|
-
./run.sh meta-bridge-fresh-cut # the ONE generation verb (the verb every v3-only rejection names): quiesce-check live sockets/markers/native-push conversations (refusing any surface it cannot inspect), archive meta-sessions/ + meta-mailbox/ to `<dir>.archive-<ts>`, clear dead transport residue, open an empty v3 generation. No migration, no restore — the archive is forensic only
|
|
153
|
+
./run.sh meta-bridge-fresh-cut # the ONE generation verb (the verb every v3-only rejection names): quiesce-check live sockets/markers/native-push conversations (refusing any surface it cannot inspect), archive meta-sessions/ + meta-mailbox/ to `<dir>.archive-<ts>`, clear dead transport residue, open an empty v3 generation. No migration, no restore — the archive is forensic only. EXIT CONTRACT (#54, `--help` prints it): 0 complete / 1 NOTHING MOVED (re-run, do not setup) / 2 usage / 3 cut transition incomplete (inspect) / 4 cut complete but residue cleanup failed (`setup` may run; re-run only before new citizen birth, otherwise remove residue manually)
|
|
154
154
|
./run.sh meta-bridge-managed-keys # 0.10.0 meta-bridge: print the SSOT of settings keys entwurf OWNS (consumers read this to stay disjoint — keyset-owner invariant)
|
|
155
155
|
./run.sh check-keyset-overlap <fragment.json...> # 0.10.0 meta-bridge: PREVENTIVE keyset guard — fail if a consumer fragment collides with any pi-owned key (cross-repo; not in pnpm check)
|
|
156
156
|
./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)
|
|
157
157
|
./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
|
|
158
158
|
./run.sh check-pack # publish gate (dry-run): npm pack --dry-run + tarball invariants (runtime-critical present, dev residue absent)
|
|
159
|
-
./run.sh check-fresh-cut-gate # SOURCE cell of the generation-boundary proof (IN pnpm check):
|
|
159
|
+
./run.sh check-fresh-cut-gate # SOURCE cell of the generation-boundary proof (IN pnpm check): 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
|
|
160
160
|
./run.sh check-pack-install # heavy publish gate (prepublishOnly): actual npm pack + tar -tf + fresh-temp install smoke with the pinned pi peers (0.82.x) + the npm-installed bridge BOOTS (tools/list) and DELIVERS (tools/call entwurf_v2 → .msg lands) + 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)
|
|
161
161
|
./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)
|
|
162
162
|
./run.sh sync-auth # copy ~/.pi/agent/auth.json anthropic OAuth credentials to entwurf alias
|
|
@@ -501,7 +501,8 @@ register_user_scope_citizen() {
|
|
|
501
501
|
# exists to hold). Same explicit-global-lifecycle shape as install/uninstall-meta-bridge.
|
|
502
502
|
# Uses the same is_entwurf_source SSOT + --remove, so it never over-deletes a look-alike
|
|
503
503
|
# (entwurf-notes, openclaw-entwurf) and preserves every other package/key. Idempotent:
|
|
504
|
-
# no entwurf entry → no-op.
|
|
504
|
+
# no entwurf entry → no-op. A settings-relative entry naming this repo is preserved and
|
|
505
|
+
# reported rather than deleted — install never writes that form (#53 B).
|
|
505
506
|
remove_user_scope_citizen() {
|
|
506
507
|
local agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
|
|
507
508
|
python3 "$REPO_DIR/scripts/register-pi-package.py" "$agent_dir/settings.json" "$REPO_DIR" --remove
|
|
@@ -525,7 +526,10 @@ remove_local_package() {
|
|
|
525
526
|
preflight_pi_settings_shapes "$project_dir/.pi/settings.json" "$(mktemp -u)"
|
|
526
527
|
# packages[] cleanup via the shared SSOT — same is_entwurf_source predicate as
|
|
527
528
|
# install, so remove never over-deletes a look-alike repo (entwurf-notes, …)
|
|
528
|
-
# that install would never have registered.
|
|
529
|
+
# that install would never have registered. Same rule, one shape further (#53 B):
|
|
530
|
+
# register only ever WRITES the absolute path, so a settings-RELATIVE entry naming
|
|
531
|
+
# this repo (the committed `".."` in <repo>/.pi/settings.json) is source, not
|
|
532
|
+
# install state — remove leaves it and says so instead of editing tracked bytes.
|
|
529
533
|
python3 "$REPO_DIR/scripts/register-pi-package.py" "$project_dir/.pi/settings.json" "$REPO_DIR" --remove
|
|
530
534
|
# entwurfProvider.mcpServers.entwurf-bridge cleanup (project scope) via the shared SSOT: strip
|
|
531
535
|
# our-managed shapes (the bare stable bin AND the legacy repo start.sh path — a true user
|
|
@@ -988,12 +992,12 @@ smoke_entwurf_v2_spawn_live() {
|
|
|
988
992
|
smoke_acp_socket_citizen_live() {
|
|
989
993
|
# S1 acceptance smoke (ACP plugin on v2) — OUT of pnpm check, needs LIVE=1.
|
|
990
994
|
# Spawns a REAL `pi --entwurf-control` resident on an ACP model
|
|
991
|
-
# (entwurf/claude-opus-
|
|
995
|
+
# (entwurf/claude-opus-5) and proves it is a first-class socket-citizen:
|
|
992
996
|
# the control socket stands up, get_info answers with the ACP model (model-lock
|
|
993
997
|
# did NOT revert — QM1), idle/cwd are reported, and the fail-loud streamSimple
|
|
994
998
|
# stub never fires (turn-free launch — QM2). No prompt is sent: S1 proves
|
|
995
999
|
# citizenship, never a backend turn (that is S2). Honest skip when LIVE!=1.
|
|
996
|
-
# Model override: ENTWURF_S1_MODEL (default claude-opus-
|
|
1000
|
+
# Model override: ENTWURF_S1_MODEL (default claude-opus-5).
|
|
997
1001
|
# LIVE=1 ./run.sh smoke-acp-socket-citizen-live
|
|
998
1002
|
run_ts scripts/smoke-acp-socket-citizen-live.ts
|
|
999
1003
|
}
|
|
@@ -1442,12 +1446,12 @@ assert.equal(peerTui, piAi,
|
|
|
1442
1446
|
// floor tracks the devDep pin so a consumer can't install against a pi lacking
|
|
1443
1447
|
// the public trust exports the bridge imports at the pinned minor, AND an upper
|
|
1444
1448
|
// bound at the next minor stops a fresh install from silently pulling a future
|
|
1445
|
-
// pi (past the declared ceiling — 0.83+ at the current 0.82.
|
|
1449
|
+
// pi (past the declared ceiling — 0.83+ at the current 0.82.1 pin) whose
|
|
1446
1450
|
// internal export surface has drifted from the one we typecheck against.
|
|
1447
1451
|
// pi moves its public surface every minor (the 0.79→0.80 getModels→provider-
|
|
1448
1452
|
// factory churn is exactly this), so an open `>=` floor is exactly how the next
|
|
1449
1453
|
// installer re-acquires the drift. Expected
|
|
1450
|
-
// shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.82.
|
|
1454
|
+
// shape: `>=<devDep> <0.<minor+1>` (e.g. `>=0.82.1 <0.83`).
|
|
1451
1455
|
const [piMaj, piMin] = piAi.split('.').map(Number);
|
|
1452
1456
|
assert.equal(piMaj, 0,
|
|
1453
1457
|
`pi pin major must stay 0 for the next-minor ceiling rule (got ${piAi}); revisit check-dep-versions when pi reaches 1.x`);
|
|
@@ -1902,13 +1906,13 @@ check_pi_import_surface() {
|
|
|
1902
1906
|
# 0.80 moved the standalone root `getModels()` to the deprecated `/compat`
|
|
1903
1907
|
# entrypoint. This repo's pi-extensions/** are loaded by pi's EXTENSION loader
|
|
1904
1908
|
# (pi-coding-agent `core/extensions/loader.ts`), whose jiti alias map resolves
|
|
1905
|
-
#
|
|
1906
|
-
#
|
|
1909
|
+
# FOUR pi-ai specifiers for extensions — the bare root, `/compat`, `/oauth`, and
|
|
1910
|
+
# (since pi 0.81) `/providers/all`. Other `providers/*` subpaths are NOT in that
|
|
1907
1911
|
# map: jiti prefix-matches the bare `@earendil-works/pi-ai` alias and appends
|
|
1908
1912
|
# the remainder, producing the unresolvable `…/dist/compat.js/providers/
|
|
1909
1913
|
# anthropic` (verified live: extension load crash — invisible to static
|
|
1910
1914
|
# typecheck, which resolves against node_modules `exports`). So `/compat` is the
|
|
1911
|
-
# sanctioned extension entrypoint for the old global model-catalog API
|
|
1915
|
+
# sanctioned extension entrypoint this repo uses for the old global model-catalog API
|
|
1912
1916
|
# (lib/acp/models.ts: `getModels`), and the ONLY allowlisted exception. The
|
|
1913
1917
|
# allow-pattern is closing-quote-anchored (`@earendil-works/pi-ai/compat["'\`]`)
|
|
1914
1918
|
# so it permits ONLY that exact specifier: `/compat-foo`, `/oauth`, every
|
|
@@ -2715,7 +2719,7 @@ _check_pack_install_impl() {
|
|
|
2715
2719
|
printf '%s\n' '{ "name": "entwurf-install-smoke", "version": "0.0.0", "private": true }' > "$tmp/package.json"
|
|
2716
2720
|
|
|
2717
2721
|
# pi-agent-core is pinned even though we never import it: pi-coding-agent depends
|
|
2718
|
-
# on it by CARET (`^0.82.
|
|
2722
|
+
# on it by CARET (`^0.82.1`), so with no lockfile in this fresh temp project it
|
|
2719
2723
|
# floats to whatever pi published last — and that newer core then drags a NESTED
|
|
2720
2724
|
# pi-ai of its own. Measured 2026-07-21: pinning only the three we import left
|
|
2721
2725
|
# pi-agent-core@0.80.10 + pi-ai@0.80.10 in the tree while the gate still announced
|
|
@@ -2726,10 +2730,10 @@ _check_pack_install_impl() {
|
|
|
2726
2730
|
local install_log
|
|
2727
2731
|
install_log=$(cd "$tmp" && pnpm add \
|
|
2728
2732
|
"$tgz_path" \
|
|
2729
|
-
"@earendil-works/pi-ai@0.82.
|
|
2730
|
-
"@earendil-works/pi-coding-agent@0.82.
|
|
2731
|
-
"@earendil-works/pi-tui@0.82.
|
|
2732
|
-
"@earendil-works/pi-agent-core@0.82.
|
|
2733
|
+
"@earendil-works/pi-ai@0.82.1" \
|
|
2734
|
+
"@earendil-works/pi-coding-agent@0.82.1" \
|
|
2735
|
+
"@earendil-works/pi-tui@0.82.1" \
|
|
2736
|
+
"@earendil-works/pi-agent-core@0.82.1" \
|
|
2733
2737
|
"typebox@latest" \
|
|
2734
2738
|
--ignore-workspace --ignore-scripts 2>&1) || {
|
|
2735
2739
|
fail "[check-pack-install] pnpm add failed:"
|
|
@@ -2739,17 +2743,17 @@ _check_pack_install_impl() {
|
|
|
2739
2743
|
|
|
2740
2744
|
# A pin is a wish until the resolved tree is read back. Assert it: EVERY
|
|
2741
2745
|
# @earendil-works pi package present — direct or transitive, top level or nested —
|
|
2742
|
-
# must be the pinned 0.82.
|
|
2746
|
+
# must be the pinned 0.82.1. Anything else means an unpinned caret floated and the
|
|
2743
2747
|
# rest of this gate would be exercising a runtime nobody verified, while still
|
|
2744
|
-
# printing "pinned pi 0.82.
|
|
2748
|
+
# printing "pinned pi 0.82.1". Fail loud instead of proving the wrong floor.
|
|
2745
2749
|
local leaked_pi
|
|
2746
|
-
leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | grep '^@earendil-works+pi-' | grep -v '@0\.82\.
|
|
2750
|
+
leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | grep '^@earendil-works+pi-' | grep -v '@0\.82\.1' || true)
|
|
2747
2751
|
if [ -n "$leaked_pi" ]; then
|
|
2748
|
-
fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.82.
|
|
2752
|
+
fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 0.82.1):"
|
|
2749
2753
|
printf '%s\n' "$leaked_pi" | sed 's/^/ /' >&2
|
|
2750
2754
|
return 1
|
|
2751
2755
|
fi
|
|
2752
|
-
echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works pi package is 0.82.
|
|
2756
|
+
echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works pi package is 0.82.1"
|
|
2753
2757
|
|
|
2754
2758
|
# Resolve the installed package.json and confirm pi.extensions
|
|
2755
2759
|
# arrived intact. If pi.extensions is empty or missing, the
|
|
@@ -2834,14 +2838,14 @@ _check_pack_install_impl() {
|
|
|
2834
2838
|
# opus anchor (CURATED_ANCHOR_MODEL_ID in lib/acp/models.ts — the model whose
|
|
2835
2839
|
# absence is a hard registry regression). Checking only one would let half the
|
|
2836
2840
|
# surface drop silently.
|
|
2837
|
-
for anchor in "claude-sonnet-5" "claude-opus-
|
|
2841
|
+
for anchor in "claude-sonnet-5" "claude-opus-5"; do
|
|
2838
2842
|
if ! grep -q "$anchor" <<<"$loader_out"; then
|
|
2839
2843
|
fail "[check-pack-install] pi loader output missing curated Claude model $anchor:"
|
|
2840
2844
|
echo "$loader_out" | tail -10 | sed 's/^/ /' >&2
|
|
2841
2845
|
return 1
|
|
2842
2846
|
fi
|
|
2843
2847
|
done
|
|
2844
|
-
echo "[check-pack-install] pi loader smoke pass (entwurf registered, claude-sonnet-5 + claude-opus-
|
|
2848
|
+
echo "[check-pack-install] pi loader smoke pass (entwurf registered, claude-sonnet-5 + claude-opus-5 anchor)"
|
|
2845
2849
|
|
|
2846
2850
|
# npm-managed neutral install regression — the README's PRIMARY install path is
|
|
2847
2851
|
# now `npm install @junghanacs/entwurf` (NOT `pi install npm:...`). This layout
|
|
@@ -2922,7 +2926,7 @@ sys.exit(0 if any(isinstance(s,str) and s.endswith('/node_modules/@junghanacs/en
|
|
|
2922
2926
|
echo "$foreign_out" | tail -10 | sed 's/^/ /' >&2
|
|
2923
2927
|
return 1
|
|
2924
2928
|
}
|
|
2925
|
-
if grep -qi "Unknown option" <<<"$foreign_out" || ! grep -q "claude-opus-
|
|
2929
|
+
if grep -qi "Unknown option" <<<"$foreign_out" || ! grep -q "claude-opus-5" <<<"$foreign_out"; then
|
|
2926
2930
|
fail "[check-pack-install] foreign-cwd --entwurf-control did not load the entwurf extension from user scope:"
|
|
2927
2931
|
echo "$foreign_out" | tail -10 | sed 's/^/ /' >&2
|
|
2928
2932
|
return 1
|
package/scripts/agy-imprint.ts
CHANGED
|
@@ -17,6 +17,7 @@ import * as fs from "node:fs";
|
|
|
17
17
|
import * as os from "node:os";
|
|
18
18
|
import * as path from "node:path";
|
|
19
19
|
import {
|
|
20
|
+
isPlausibleOwnerPid,
|
|
20
21
|
parentPid,
|
|
21
22
|
processStartKey,
|
|
22
23
|
upsertMetaSession,
|
|
@@ -165,8 +166,16 @@ function imprint(raw: string): void {
|
|
|
165
166
|
// Written only after the upsert above: the record store is the identity authority, and a
|
|
166
167
|
// marker pointing at a garden-id with no record would be a window of un-backed identity.
|
|
167
168
|
// A failed marker costs reply-addressability, never the session — log and move on.
|
|
169
|
+
//
|
|
170
|
+
// The owner rule is `isPlausibleOwnerPid`, the SAME predicate the Claude hook
|
|
171
|
+
// and every reader hold. This writer used to ask `> 0` on its own, so THIS hook
|
|
172
|
+
// could mint `meta-senders/antigravity/1.json` through the very reparenting that
|
|
173
|
+
// stranded a host's fresh-cut (#53 A): agy exits, the imprint is reparented to
|
|
174
|
+
// init, and `process.ppid` reads 1. Refusing is fail-closed — it costs reply
|
|
175
|
+
// addressability for that turn and is logged; honoring it would cost the operator
|
|
176
|
+
// their generation cut, with no in-band way to get it back.
|
|
168
177
|
const ownerPid = process.ppid;
|
|
169
|
-
if (
|
|
178
|
+
if (isPlausibleOwnerPid(ownerPid)) {
|
|
170
179
|
try {
|
|
171
180
|
writeMetaSenderMarker({
|
|
172
181
|
backend: "antigravity",
|
|
@@ -181,6 +190,11 @@ function imprint(raw: string): void {
|
|
|
181
190
|
`sender-marker-failed pid=${ownerPid} gardenId=${result.record.gardenId} ${err instanceof Error ? err.message : String(err)}`,
|
|
182
191
|
);
|
|
183
192
|
}
|
|
193
|
+
} else {
|
|
194
|
+
logLine(
|
|
195
|
+
`sender-marker-refused pid=${ownerPid} gardenId=${result.record.gardenId} implausible owner pid ` +
|
|
196
|
+
"(<= 1: this imprint was reparented, so its parent is not the agy process that owns the turn)",
|
|
197
|
+
);
|
|
184
198
|
}
|
|
185
199
|
} catch (err) {
|
|
186
200
|
logLine(
|
|
@@ -33,7 +33,7 @@ import { pathToFileURL } from "node:url";
|
|
|
33
33
|
import type { Api, AssistantMessageEvent, Context, Message, Model } from "@earendil-works/pi-ai";
|
|
34
34
|
|
|
35
35
|
const sonnet = { id: "claude-sonnet-5" } as unknown as Model<Api>;
|
|
36
|
-
const opus = { id: "claude-opus-
|
|
36
|
+
const opus = { id: "claude-opus-5" } as unknown as Model<Api>;
|
|
37
37
|
|
|
38
38
|
type Stream = AsyncIterable<AssistantMessageEvent> & {
|
|
39
39
|
result: () => Promise<{ stopReason: string; errorMessage?: string }>;
|
|
@@ -85,7 +85,7 @@ const baseInput = (): BridgeConfigInput => ({
|
|
|
85
85
|
);
|
|
86
86
|
// Model drift → different signature.
|
|
87
87
|
assert.notEqual(
|
|
88
|
-
bridgeConfigSignature({ ...baseInput(), modelId: "claude-opus-
|
|
88
|
+
bridgeConfigSignature({ ...baseInput(), modelId: "claude-opus-5" }),
|
|
89
89
|
bridgeConfigSignature(baseInput()),
|
|
90
90
|
"model drift changes the signature",
|
|
91
91
|
);
|
|
@@ -186,7 +186,7 @@ const baseInput = (): BridgeConfigInput => ({
|
|
|
186
186
|
const params = facts({ contextMessageSignatures: ["user:text:a", "assistant:text:b"] });
|
|
187
187
|
assert.ok(isCompatible(facts(), params), "prefix history + same cfg → compatible");
|
|
188
188
|
assert.ok(!isCompatible(facts({ cwd: "/other" }), params), "cwd drift → incompatible");
|
|
189
|
-
assert.ok(!isCompatible(facts({ modelId: "claude-opus-
|
|
189
|
+
assert.ok(!isCompatible(facts({ modelId: "claude-opus-5" }), params), "model drift → incompatible");
|
|
190
190
|
assert.ok(!isCompatible(facts({ bridgeConfigSignature: "different" }), params), "signature drift → incompatible");
|
|
191
191
|
assert.ok(
|
|
192
192
|
!isCompatible(facts({ contextMessageSignatures: ["user:text:EDITED"] }), params),
|
|
@@ -292,7 +292,7 @@ const baseInput = (): BridgeConfigInput => ({
|
|
|
292
292
|
assert.throws(
|
|
293
293
|
() =>
|
|
294
294
|
decideBootstrap(params("process-scoped"), {
|
|
295
|
-
existing: { ...aliveCompat, modelId: "claude-opus-
|
|
295
|
+
existing: { ...aliveCompat, modelId: "claude-opus-5" },
|
|
296
296
|
}),
|
|
297
297
|
SessionModelLockedError,
|
|
298
298
|
"live model mismatch throws SessionModelLockedError",
|
|
@@ -48,6 +48,9 @@ import {
|
|
|
48
48
|
import {
|
|
49
49
|
FRESH_CUT_COMMAND,
|
|
50
50
|
type MetaIdentity,
|
|
51
|
+
metaRecordExistsByGardenId,
|
|
52
|
+
processStartKey,
|
|
53
|
+
readMetaSenderMarker,
|
|
51
54
|
upsertMetaSession,
|
|
52
55
|
writeMetaSenderMarker,
|
|
53
56
|
} from "../pi-extensions/lib/meta-session.ts";
|
|
@@ -358,6 +361,86 @@ try {
|
|
|
358
361
|
trusted?.identity.gardenId === record.record.gardenId,
|
|
359
362
|
);
|
|
360
363
|
}
|
|
364
|
+
// ── #53 A: an init-owned marker names NOBODY, however live init is ─────────
|
|
365
|
+
// The shape measured on a second Linux host: `meta-senders/claude-code/1.json`,
|
|
366
|
+
// pid 1, and the REAL start-key for init — so the pid-reuse guard PASSES and
|
|
367
|
+
// classifyMarkerOwner answers `live` for as long as the host is up. The defect is
|
|
368
|
+
// one layer up: init owns no Claude session. THIS gate's own writer is one of the
|
|
369
|
+
// two that could produce the shape before #53 A — the agy imprint asked `> 0`
|
|
370
|
+
// while the Claude hook already refused `<= 1` — so "only the shell-form hook
|
|
371
|
+
// could have written it" was never true. After this cut no writer in the tree can.
|
|
372
|
+
{
|
|
373
|
+
clearMarkers();
|
|
374
|
+
const record = upsertMetaSession({
|
|
375
|
+
input: { backend: "claude-code", nativeSessionId: "sess-init", cwd: REPO_DIR },
|
|
376
|
+
});
|
|
377
|
+
const initKey = processStartKey(1);
|
|
378
|
+
ok("this host CAN read a start key for pid 1 (the residue cells are not vacuous)", initKey !== "");
|
|
379
|
+
const initDir = path.join(SENDERS_DIR, "claude-code");
|
|
380
|
+
fs.mkdirSync(initDir, { recursive: true });
|
|
381
|
+
fs.writeFileSync(
|
|
382
|
+
path.join(initDir, "1.json"),
|
|
383
|
+
`${JSON.stringify({
|
|
384
|
+
backend: "claude-code",
|
|
385
|
+
gardenId: record.record.gardenId,
|
|
386
|
+
nativeSessionId: "sess-init",
|
|
387
|
+
cwd: REPO_DIR,
|
|
388
|
+
ownerPid: 1,
|
|
389
|
+
ownerStartKey: initKey,
|
|
390
|
+
updatedAt: "2026-06-10T16:03:10.000Z",
|
|
391
|
+
})}\n`,
|
|
392
|
+
);
|
|
393
|
+
ok(
|
|
394
|
+
"an init-owned sender marker reads as null — it grants no identity",
|
|
395
|
+
readMetaSenderMarker({ backend: "claude-code", ownerPid: 1, sendersDir: SENDERS_DIR }) === null,
|
|
396
|
+
);
|
|
397
|
+
ok(
|
|
398
|
+
"...while its record is present and readable, so the refusal is about the marker's CLAIM",
|
|
399
|
+
metaRecordExistsByGardenId(record.record.gardenId),
|
|
400
|
+
);
|
|
401
|
+
// End to end, with pid 1 supplied as an explicit candidate: the answer is
|
|
402
|
+
// "nobody" — not an identity, and not a throw either. Stated at the size of the
|
|
403
|
+
// evidence: the DEFAULT candidate set is only the bridge's parent and grandparent,
|
|
404
|
+
// so pid 1 enters it just when the native host itself was reparented (a detached
|
|
405
|
+
// Claude). Reachable, not observed — the affected host's own launch shape
|
|
406
|
+
// (bridge → claude → bash → wrapper) never had 1 among its candidates, and no
|
|
407
|
+
// wrong-identity read was measured there. The cut's deadlock is the proven half;
|
|
408
|
+
// this is the code-path half.
|
|
409
|
+
let identityThrew: unknown = null;
|
|
410
|
+
let identityAnswer: unknown = "unset";
|
|
411
|
+
try {
|
|
412
|
+
identityAnswer = resolveTrustedMetaSenderIdentity({ ownerPids: [1], sendersDir: SENDERS_DIR });
|
|
413
|
+
} catch (err) {
|
|
414
|
+
identityThrew = err;
|
|
415
|
+
}
|
|
416
|
+
ok(
|
|
417
|
+
"resolving with pid 1 as a candidate yields NO identity (and no throw)",
|
|
418
|
+
identityAnswer === null && identityThrew === null,
|
|
419
|
+
);
|
|
420
|
+
let initWriteRejected = false;
|
|
421
|
+
try {
|
|
422
|
+
writeMetaSenderMarker({
|
|
423
|
+
backend: "claude-code",
|
|
424
|
+
gardenId: record.record.gardenId,
|
|
425
|
+
nativeSessionId: "sess-init",
|
|
426
|
+
cwd: REPO_DIR,
|
|
427
|
+
ownerPid: 1,
|
|
428
|
+
sendersDir: SENDERS_DIR,
|
|
429
|
+
});
|
|
430
|
+
} catch {
|
|
431
|
+
initWriteRejected = true;
|
|
432
|
+
}
|
|
433
|
+
ok("minting an init-owned sender marker THROWS at the write boundary", initWriteRejected);
|
|
434
|
+
// WIRING: the agy imprint is the SECOND writer, and the one that still asked
|
|
435
|
+
// `> 0` on its own while the Claude hook already refused `<= 1`. A predicate only
|
|
436
|
+
// one writer knows is how #53 A drifted in; both must reach the shared one.
|
|
437
|
+
ok(
|
|
438
|
+
"the agy imprint routes its owner pid through the shared predicate",
|
|
439
|
+
/isPlausibleOwnerPid\(ownerPid\)/.test(fs.readFileSync(HOOK, "utf8")),
|
|
440
|
+
);
|
|
441
|
+
clearMarkers();
|
|
442
|
+
}
|
|
443
|
+
|
|
361
444
|
ok(
|
|
362
445
|
"antigravity is a native-push backend → its replyable comes from the adapter probe",
|
|
363
446
|
nativePushSupported("antigravity"),
|
|
@@ -196,14 +196,14 @@ async function main(): Promise<void> {
|
|
|
196
196
|
{
|
|
197
197
|
const base = {
|
|
198
198
|
sessionId: "20260613T091000-98363c",
|
|
199
|
-
agentId: "pi/claude-opus-
|
|
199
|
+
agentId: "pi/claude-opus-5",
|
|
200
200
|
cwd: "/w",
|
|
201
201
|
timestamp: "2026-06-13T09:10:00.000Z",
|
|
202
202
|
};
|
|
203
203
|
ok(
|
|
204
204
|
"6: minimal envelope → exact block (leading blank line, required fields only)",
|
|
205
205
|
formatSenderInfoBlock(base) ===
|
|
206
|
-
`\n\n<sender_info>{"sessionId":"20260613T091000-98363c","agentId":"pi/claude-opus-
|
|
206
|
+
`\n\n<sender_info>{"sessionId":"20260613T091000-98363c","agentId":"pi/claude-opus-5","cwd":"/w","timestamp":"2026-06-13T09:10:00.000Z"}</sender_info>`,
|
|
207
207
|
);
|
|
208
208
|
const full = formatSenderInfoBlock({ ...base, origin: "pi-session", replyable: false }, true);
|
|
209
209
|
ok(
|
|
@@ -63,6 +63,8 @@ interface SocketOpts {
|
|
|
63
63
|
extraNames?: string[];
|
|
64
64
|
/** readdir throws with this code (e.g. "EACCES" / "ENOENT") (P2e②). */
|
|
65
65
|
readdirErrorCode?: string;
|
|
66
|
+
/** `.meta.json` entries that are NOT regular files (symlink/dir/special). */
|
|
67
|
+
irregularMeta?: string[];
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
function deps(
|
|
@@ -72,7 +74,13 @@ function deps(
|
|
|
72
74
|
): EntwurfFactsDeps {
|
|
73
75
|
const symlinkSet = new Set(opts.symlinks ?? []);
|
|
74
76
|
return {
|
|
75
|
-
|
|
77
|
+
// Kind-carrying entries, like the real bindings: the listing must be able to refuse a
|
|
78
|
+
// symlinked record without following it. `irregularMeta` names the ones that are not
|
|
79
|
+
// regular files.
|
|
80
|
+
metaEntries: Object.keys(meta).map((filename) => ({
|
|
81
|
+
filename,
|
|
82
|
+
regularFile: !(opts.irregularMeta ?? []).includes(filename),
|
|
83
|
+
})),
|
|
76
84
|
readRecord: (f: string) => {
|
|
77
85
|
const v = meta[f];
|
|
78
86
|
if (v === undefined) throw new Error(`ENOENT: ${f}`);
|
|
@@ -65,7 +65,7 @@ function identity(backend: MetaCitizenBackend, over: Partial<MetaIdentity> = {})
|
|
|
65
65
|
backend,
|
|
66
66
|
nativeSessionId: "native-abc",
|
|
67
67
|
cwd: "/home/junghan/repos/gh/entwurf",
|
|
68
|
-
model: "claude-opus-
|
|
68
|
+
model: "claude-opus-5",
|
|
69
69
|
transcriptPath: "/home/junghan/.claude/projects/x/native-abc.jsonl",
|
|
70
70
|
createdAt: "2026-06-11T00:38:58.000Z",
|
|
71
71
|
recordUpdatedAt: "2026-06-11T02:40:00.000Z",
|
|
@@ -59,12 +59,16 @@ const liveIdentity = (): MetaIdentity => ({
|
|
|
59
59
|
createdAt: "2026-06-14T01:00:00.000Z",
|
|
60
60
|
recordUpdatedAt: "2026-06-14T01:00:00.000Z",
|
|
61
61
|
});
|
|
62
|
+
// The owner pid is a plausible one on purpose: these markers are injected straight
|
|
63
|
+
// into the pure predicate, which never reads the field — but a fixture carrying
|
|
64
|
+
// `ownerPid: 1` would model a marker no writer can mint and no reader honors
|
|
65
|
+
// (isPlausibleOwnerPid, #53 A), and fixtures are read as claims about the world.
|
|
62
66
|
const liveMarker = () =>
|
|
63
67
|
({
|
|
64
68
|
gardenId: GARDEN,
|
|
65
69
|
backend: "claude-code",
|
|
66
70
|
nativeSessionId: "n-a",
|
|
67
|
-
ownerPid:
|
|
71
|
+
ownerPid: 4242,
|
|
68
72
|
ownerStartKey: "x",
|
|
69
73
|
ownerKind: "claude-code-cli",
|
|
70
74
|
armProvenance: "session-start",
|
|
@@ -77,7 +81,7 @@ const driftMarker = () =>
|
|
|
77
81
|
gardenId: GARDEN,
|
|
78
82
|
backend: "claude-code",
|
|
79
83
|
nativeSessionId: "n-OTHER",
|
|
80
|
-
ownerPid:
|
|
84
|
+
ownerPid: 4242,
|
|
81
85
|
ownerStartKey: "x",
|
|
82
86
|
ownerKind: "claude-code-cli",
|
|
83
87
|
armProvenance: "session-start",
|
|
@@ -52,7 +52,7 @@ function main(): void {
|
|
|
52
52
|
sessionFile: SESSION_FILE,
|
|
53
53
|
explicitExtensionArgs: EXT,
|
|
54
54
|
provider: "entwurf",
|
|
55
|
-
model: "claude-opus-
|
|
55
|
+
model: "claude-opus-5",
|
|
56
56
|
prompt: "continue the task",
|
|
57
57
|
});
|
|
58
58
|
ok("1 legacy has --no-extensions", args.includes("--no-extensions"));
|
|
@@ -61,14 +61,14 @@ function main(): void {
|
|
|
61
61
|
ok("3 legacy prompt is the final positional", args[args.length - 1] === "continue the task");
|
|
62
62
|
ok("4 legacy keeps ext args exactly once", args.filter((a) => a === "-e").length === 1);
|
|
63
63
|
ok("6 legacy provider laid out", valueAfter(args, "--provider") === "entwurf");
|
|
64
|
-
ok("6 legacy model laid out", valueAfter(args, "--model") === "claude-opus-
|
|
64
|
+
ok("6 legacy model laid out", valueAfter(args, "--model") === "claude-opus-5");
|
|
65
65
|
ok("6 legacy resumes by exact FILE (--session <abs path>)", valueAfter(args, "--session") === SESSION_FILE);
|
|
66
66
|
ok("6 legacy carries NO --session-id (the id is pi's own now)", !args.includes("--session-id"));
|
|
67
67
|
// model + prompt are the last three tokens: --model <m> <prompt>
|
|
68
68
|
ok(
|
|
69
69
|
"6 legacy --model <m> <prompt> tail",
|
|
70
70
|
args[args.length - 3] === "--model" &&
|
|
71
|
-
args[args.length - 2] === "claude-opus-
|
|
71
|
+
args[args.length - 2] === "claude-opus-5" &&
|
|
72
72
|
args[args.length - 1] === "continue the task",
|
|
73
73
|
);
|
|
74
74
|
}
|
|
@@ -80,7 +80,7 @@ function main(): void {
|
|
|
80
80
|
sessionFile: SESSION_FILE,
|
|
81
81
|
explicitExtensionArgs: EXT,
|
|
82
82
|
provider: "entwurf",
|
|
83
|
-
model: "claude-opus-
|
|
83
|
+
model: "claude-opus-5",
|
|
84
84
|
prompt: "resume now",
|
|
85
85
|
launchArgs: ["--approve"],
|
|
86
86
|
});
|
|
@@ -93,7 +93,7 @@ function main(): void {
|
|
|
93
93
|
ok("5 v2 includes launchArgs --approve", args.includes("--approve"));
|
|
94
94
|
ok("5 v2 --approve is before the prompt", args.indexOf("--approve") < args.length - 1);
|
|
95
95
|
ok("6 v2 provider laid out", valueAfter(args, "--provider") === "entwurf");
|
|
96
|
-
ok("6 v2 model laid out", valueAfter(args, "--model") === "claude-opus-
|
|
96
|
+
ok("6 v2 model laid out", valueAfter(args, "--model") === "claude-opus-5");
|
|
97
97
|
ok("6 v2 resumes by exact FILE (--session <abs path>)", valueAfter(args, "--session") === SESSION_FILE);
|
|
98
98
|
ok(
|
|
99
99
|
"6 v2 carries NO --session-id (a garden id would MINT a session, not resume one)",
|
|
@@ -102,7 +102,7 @@ function main(): void {
|
|
|
102
102
|
ok(
|
|
103
103
|
"6 v2 --model <m> <prompt> tail",
|
|
104
104
|
args[args.length - 3] === "--model" &&
|
|
105
|
-
args[args.length - 2] === "claude-opus-
|
|
105
|
+
args[args.length - 2] === "claude-opus-5" &&
|
|
106
106
|
args[args.length - 1] === "resume now",
|
|
107
107
|
);
|
|
108
108
|
}
|
|
@@ -103,7 +103,7 @@ try {
|
|
|
103
103
|
fileA,
|
|
104
104
|
sessionLine(idA, "/identity/a") +
|
|
105
105
|
mc("openai-codex", "gpt-5.5") +
|
|
106
|
-
msg({ content: "drifted", model: "claude-opus-
|
|
106
|
+
msg({ content: "drifted", model: "claude-opus-5", provider: "entwurf" }),
|
|
107
107
|
);
|
|
108
108
|
const recA = readSessionIdentity(fileA);
|
|
109
109
|
eq(recA?.provider, "openai-codex", "identity: provider = first model_change (not assistant message)");
|
|
@@ -115,7 +115,7 @@ try {
|
|
|
115
115
|
const fileB = path.join(idDir, `2026-06-03T22-00-00-000Z_${idB}.jsonl`);
|
|
116
116
|
fs.writeFileSync(
|
|
117
117
|
fileB,
|
|
118
|
-
sessionLine(idB, "/identity/b") + mc("entwurf", "claude-opus-
|
|
118
|
+
sessionLine(idB, "/identity/b") + mc("entwurf", "claude-opus-5") + mc("openai-codex", "gpt-5.5"),
|
|
119
119
|
);
|
|
120
120
|
throws(() => readSessionIdentity(fileB), "identity: later model_change drift → fail-fast");
|
|
121
121
|
|
|
@@ -124,7 +124,7 @@ try {
|
|
|
124
124
|
// judge — identity must resolve from the transcript alone, no throw.
|
|
125
125
|
const idC = "20260603T220000-3333cc";
|
|
126
126
|
const fileC = path.join(idDir, `2026-06-03T22-00-00-000Z_${idC}.jsonl`);
|
|
127
|
-
const mismatchName = `${idC}==entwurf/claude-opus-
|
|
127
|
+
const mismatchName = `${idC}==entwurf/claude-opus-5--x__entwurf`;
|
|
128
128
|
fs.writeFileSync(fileC, sessionLine(idC, "/identity/c") + mc("openai-codex", "gpt-5.5") + infoLine(mismatchName));
|
|
129
129
|
noThrow(() => readSessionIdentity(fileC), "identity: name provider/model disagreement is pi's business — no throw");
|
|
130
130
|
eq(readSessionIdentity(fileC)?.modelId, "gpt-5.5", "identity: model still resolves from first model_change");
|
|
@@ -165,17 +165,18 @@ try {
|
|
|
165
165
|
// ---- T-name-blind: the reader takes NO options and never reads the name (#50 C3) ----
|
|
166
166
|
// The old `requireEntwurf` name-tag authorization (0.9.0 "entwurf 여부 = name tag")
|
|
167
167
|
// is deleted, not just unused: resume authorization is record existence
|
|
168
|
-
// (
|
|
168
|
+
// (readAddressableMetaIdentity — the record must also hold its nativeSessionId
|
|
169
|
+
// alone, #52) + the header-id ↔ record.nativeSessionId integrity
|
|
169
170
|
// check in resolveResumeLaunchIdentity. A session with no name, a non-canonical
|
|
170
171
|
// name, or no `entwurf` tag resolves identity exactly like any other pi session.
|
|
171
172
|
const idG = "20260603T230000-7777aa";
|
|
172
173
|
const fileG = path.join(idDir, `2026-06-03T23-00-00-000Z_${idG}.jsonl`);
|
|
173
174
|
fs.writeFileSync(
|
|
174
175
|
fileG,
|
|
175
|
-
sessionLine(idG, "/identity/g") + mc("entwurf", "claude-opus-
|
|
176
|
+
sessionLine(idG, "/identity/g") + mc("entwurf", "claude-opus-5") + infoLine("not a canonical name"),
|
|
176
177
|
);
|
|
177
178
|
noThrow(() => readSessionIdentity(fileG), "name-blind: non-canonical name resolves like any pi session");
|
|
178
|
-
eq(readSessionIdentity(fileG)?.modelId, "claude-opus-
|
|
179
|
+
eq(readSessionIdentity(fileG)?.modelId, "claude-opus-5", "name-blind: identity from model_change only");
|
|
179
180
|
eq(readSessionIdentity.length, 1, "name-blind: readSessionIdentity takes exactly one arg (no opts, compiler-pinned)");
|
|
180
181
|
|
|
181
182
|
// ========================================================================
|
|
@@ -63,13 +63,15 @@ function identity(backend: MetaIdentity["backend"], gardenId = GID): MetaIdentit
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/** A receiver presence marker. Matches the record identity (gardenId/backend/nativeSessionId)
|
|
66
|
-
* unless `nativeSessionId` is overridden to simulate an identity-drifted/foreign marker.
|
|
66
|
+
* unless `nativeSessionId` is overridden to simulate an identity-drifted/foreign marker.
|
|
67
|
+
* The owner pid is a plausible one: the predicate never reads it, but a fixture carrying
|
|
68
|
+
* `ownerPid: 1` would model a marker no writer can mint and no reader honors (#53 A). */
|
|
67
69
|
function receiverMarker(gid: string, backend: string, nativeSessionId = "n"): MetaReceiverMarker {
|
|
68
70
|
return {
|
|
69
71
|
gardenId: gid,
|
|
70
72
|
backend: backend as MetaReceiverMarker["backend"],
|
|
71
73
|
nativeSessionId,
|
|
72
|
-
ownerPid:
|
|
74
|
+
ownerPid: 4242,
|
|
73
75
|
ownerStartKey: "x",
|
|
74
76
|
ownerKind: "claude-code-cli",
|
|
75
77
|
armProvenance: "session-start",
|
|
@@ -59,7 +59,7 @@ const IDENTITY: LaunchIdentity = {
|
|
|
59
59
|
cwd: "/home/test/repo",
|
|
60
60
|
explicitExtensionArgs: ["-e", "/path/to/entwurf/index.ts"],
|
|
61
61
|
provider: "entwurf",
|
|
62
|
-
model: "claude-opus-
|
|
62
|
+
model: "claude-opus-5",
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
// True if `p` has NOT settled after a macrotask tick (all pending microtasks drained).
|
|
@@ -224,7 +224,7 @@ async function main(): Promise<void> {
|
|
|
224
224
|
ok("2 argv carries plan.launchArgs (--approve)", args.includes("--approve"));
|
|
225
225
|
ok("2 argv carries the ext args", args.includes("-e"));
|
|
226
226
|
ok("2 argv carries provider", args[args.indexOf("--provider") + 1] === "entwurf");
|
|
227
|
-
ok("2 argv carries model", args[args.indexOf("--model") + 1] === "claude-opus-
|
|
227
|
+
ok("2 argv carries model", args[args.indexOf("--model") + 1] === "claude-opus-5");
|
|
228
228
|
// #50 C2: argv names the exact transcript FILE, never a garden id. `--session-id`
|
|
229
229
|
// would CREATE a session at that id when it is missing — post-cut the garden id is
|
|
230
230
|
// never a pi session id, so the old flag would have minted an empty session and
|