@junghanacs/entwurf 0.22.0 → 0.23.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 +4 -1
- package/BASELINE.md +1 -0
- package/CHANGELOG.md +514 -0
- package/DELIVERY.md +10 -2
- package/README.md +43 -4
- package/VERIFY.md +47 -0
- package/docs/acp-backend-rail.md +96 -17
- package/docs/setup-clean-host.md +11 -1
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +39 -40
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +95 -56
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +46 -273
- package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
- package/mcp/entwurf-bridge/src/index.ts +41 -41
- package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
- package/package.json +9 -8
- package/pi-extensions/entwurf-control.ts +91 -52
- 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 +4 -4
- package/pi-extensions/lib/acp/event-mapper.ts +5 -5
- package/pi-extensions/lib/acp/tool-surface.ts +2 -1
- package/pi-extensions/lib/codex-declaration.js +612 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +107 -51
- package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
- package/pi-extensions/lib/entwurf-facts.ts +14 -1
- package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
- package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
- package/pi-extensions/lib/entwurf-self-address.ts +33 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
- package/pi-extensions/lib/fresh-call-composition.ts +493 -0
- package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
- package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
- package/pi-extensions/lib/herdr-placement.ts +276 -0
- package/pi-extensions/lib/mux-fresh-call.ts +95 -301
- package/run.sh +588 -204
- package/scripts/check-acp-usage-accounting.ts +9 -9
- package/scripts/check-codex-birth-hook.ts +187 -0
- package/scripts/check-entwurf-control-rpc.ts +138 -0
- package/scripts/check-entwurf-fact-provider.ts +4 -2
- package/scripts/check-entwurf-peers-surface.ts +3 -1
- package/scripts/check-entwurf-self-address.ts +68 -0
- package/scripts/check-fresh-call-dispatch.ts +301 -0
- package/scripts/check-gate-qualification.ts +17 -4
- package/scripts/check-herdr-activation.ts +1293 -0
- package/scripts/check-herdr-fresh-call.ts +1250 -0
- package/scripts/check-herdr-placement.ts +383 -0
- package/scripts/check-herdr-plugin-build.ts +842 -0
- package/scripts/check-herdr-plugin-profile.ts +371 -0
- package/scripts/check-herdr-plugin.ts +523 -0
- package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
- package/scripts/check-herdr-sandbox.ts +456 -0
- package/scripts/check-herdr-supply.ts +184 -0
- package/scripts/check-mux-launcher-fence.ts +138 -0
- package/scripts/check-peer-facts.ts +331 -0
- package/scripts/check-pi-launch.ts +358 -0
- package/scripts/check-release-gate-outcomes.ts +19 -0
- package/scripts/check-typing-call-fence.ts +296 -0
- package/scripts/codex-birth-doctor.sh +159 -16
- package/scripts/codex-birth-install.sh +178 -72
- package/scripts/codex-birth-uninstall.sh +162 -7
- package/scripts/fixtures/herdr-supply.json +29 -0
- package/scripts/herdr-activation.mjs +536 -0
- package/scripts/herdr-plugin-activate.mjs +270 -0
- package/scripts/herdr-plugin-deactivate.mjs +193 -0
- package/scripts/herdr-runtime.mjs +1203 -0
- package/scripts/install-herdr-ci.sh +96 -0
- package/scripts/inventory-verification-surface.ts +86 -10
- package/scripts/lib/codex-fresh-live-protocol.ts +2 -2
- package/scripts/lib/tree-digest.ts +97 -0
- package/scripts/meta-bridge-install.sh +19 -2
- package/scripts/meta-bridge-state.py +167 -11
- package/scripts/mutants/codex-caller-seat.json +6 -8
- package/scripts/mutants/codex-native.json +195 -29
- package/scripts/mutants/control-socket-disconnect.json +57 -0
- package/scripts/mutants/fresh-call-dispatch.json +154 -0
- package/scripts/mutants/herdr-activation.json +305 -0
- package/scripts/mutants/herdr-fresh-call.json +448 -0
- package/scripts/mutants/herdr-placement.json +161 -0
- package/scripts/mutants/herdr-plugin-build.json +160 -0
- package/scripts/mutants/herdr-plugin-profile.json +202 -0
- package/scripts/mutants/herdr-plugin.json +133 -0
- package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
- package/scripts/mutants/herdr-supply.json +133 -0
- package/scripts/mutants/meta-hook-session-switch.json +2 -2
- package/scripts/mutants/mux-fresh-call.json +104 -17
- package/scripts/mutants/mux-launcher-fence.json +13 -0
- package/scripts/mutants/omp-fresh.json +4 -4
- package/scripts/mutants/peer-facts.json +98 -0
- package/scripts/mutants/pi-launch.json +113 -0
- package/scripts/mutants/resume-args.json +6 -6
- package/scripts/mutants/self-address.json +33 -0
- package/scripts/mutants/typing-call-fence.json +17 -0
- package/scripts/mutants/v2-surface.json +2 -2
- package/scripts/mutants/v2-visible-resume.json +17 -17
- package/scripts/peer-facts.ts +120 -0
- package/scripts/register-pi-package.py +8 -0
- package/scripts/register-pi-provider.py +88 -12
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-codex-birth.sh +281 -12
- package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
- package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
- package/scripts/smoke-herdr-raw-install-live.sh +849 -0
- package/scripts/tsconfig.json +5 -0
- package/scripts/check-entwurf-facts.ts +0 -385
- package/scripts/check-entwurf-resume-args.ts +0 -122
- package/scripts/check-entwurf-v2-decider.ts +0 -657
- package/scripts/check-entwurf-v2-lock.ts +0 -371
- package/scripts/check-entwurf-v2-mailbox.ts +0 -261
- package/scripts/check-entwurf-v2-matrix.ts +0 -401
- package/scripts/check-entwurf-v2-native-push.ts +0 -221
- package/scripts/check-entwurf-v2-release.ts +0 -151
- package/scripts/check-entwurf-v2-runner.ts +0 -448
- package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
- package/scripts/check-entwurf-v2-send.ts +0 -458
- package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
- package/scripts/check-socket-discovery.ts +0 -401
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-herdr-placement — deterministic gate for the #116 S1 placement evidence axis
|
|
3
|
+
* (`pi-extensions/lib/herdr-placement.ts` + the one read the provider is allowed).
|
|
4
|
+
*
|
|
5
|
+
* SCOPE DISCIPLINE. This gate pins only what is decidable WITHOUT a herdr binary: the
|
|
6
|
+
* payload parse, the two backend join rules, the ambiguity rule, the four-word
|
|
7
|
+
* observation vocabulary, the render, and the structural absence of placement from the
|
|
8
|
+
* dispatch path. There is no fake herdr here, for the same reason
|
|
9
|
+
* `scripts/check-mux-placement.ts:6-9` refuses a fake tmux — a stand-in would author
|
|
10
|
+
* the contract before the real thing was measured. The real-binary half is an isolated
|
|
11
|
+
* private herdr server (measured possible on 2026-09-14: sandbox `HOME`/`XDG_CONFIG_HOME`,
|
|
12
|
+
* no client attach, operator server untouched), and the callback/join round trip stays
|
|
13
|
+
* LIVE. Every payload literal below is a VERBATIM shape recorded from herdr 0.9.0 on
|
|
14
|
+
* 2026-09-14, not an invented one.
|
|
15
|
+
*
|
|
16
|
+
* Each claim carries its QK token on exactly ONE assertion.
|
|
17
|
+
*
|
|
18
|
+
* HP-JOIN-CLAUDE-EXACT a `kind:"id"` pane joins on byte equality, nothing looser
|
|
19
|
+
* HP-JOIN-PI-FILENAME a `kind:"path"` pane joins only on a well-formed uuid tail
|
|
20
|
+
* HP-JOIN-NEVER-GUESSES an unmeasured kind / malformed name yields NO key, never a
|
|
21
|
+
* plausible substring
|
|
22
|
+
* HP-JOIN-OFFICIAL-ONLY only an OFFICIAL herdr integration report is read — the
|
|
23
|
+
* agent/source pair is checked, not discarded
|
|
24
|
+
* HP-JOIN-BACKEND-BOUND a report speaks only for the backend it belongs to
|
|
25
|
+
* HP-DECLINED-IS-UNOBSERVED while any report was declined, a non-join says `unobserved`
|
|
26
|
+
* rather than claiming a complete negative read
|
|
27
|
+
* HP-AMBIGUOUS-NOT-A-PANE two panes claiming one key resolve to `ambiguous`, never to
|
|
28
|
+
* one of them
|
|
29
|
+
* HP-UNREADABLE-IS-UNOBSERVED a payload we cannot read means nobody looked, never
|
|
30
|
+
* "herdr has none of your citizens"
|
|
31
|
+
* HP-NO-HERDR-IS-UNOBSERVED a null index leaves every citizen `unobserved`, and the
|
|
32
|
+
* listing does not refuse
|
|
33
|
+
* HP-PLACEMENT-NOT-DISPATCH the dispatch path never imports the placement axis
|
|
34
|
+
* HP-ONE-READ-PER-LISTING the provider reads the placement owner exactly once for
|
|
35
|
+
* a whole listing — the anti-watcher rule in code
|
|
36
|
+
* HP-READ-ENV-GATED-NULL-NEVER-EMPTY the production READ itself: gated on herdr's own two
|
|
37
|
+
* env facts, spawning nothing when they are absent, asking the
|
|
38
|
+
* fixed `pane list` argv, and turning every failure into `null`
|
|
39
|
+
* HP-READ-TIMEOUT-BOUNDED that read is bounded, so a placement owner that never answers
|
|
40
|
+
* costs a listing a bounded wait and not a hang
|
|
41
|
+
*
|
|
42
|
+
* THE ONE STUB IN THIS FILE, AND WHY IT IS NOT A FAKE HERDR (glm #1, 2026-09-18). Everything
|
|
43
|
+
* above is decided from VERBATIM recordings because a stand-in would author herdr's answers
|
|
44
|
+
* before the real thing was measured. The two claims below are not about herdr's answers at all:
|
|
45
|
+
* they are about OUR spawn seam — which env facts gate it, what argv it asks, and that a process
|
|
46
|
+
* which fails, garbles or hangs becomes `null` rather than an empty index. Proving those needs a
|
|
47
|
+
* controllable PROCESS, and the stub is never asked what a pane list looks like: the one cell that
|
|
48
|
+
* reads a payload feeds it the same recorded bytes section 1 parses. `[glm 감사 2026-09-18]` this
|
|
49
|
+
* seam had zero assertions and zero mutants — a typo in `["pane","list"]` or a deleted timeout
|
|
50
|
+
* would have left the deterministic floor green.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import assert from "node:assert/strict";
|
|
54
|
+
import * as fs from "node:fs";
|
|
55
|
+
import { readFileSync } from "node:fs";
|
|
56
|
+
import * as os from "node:os";
|
|
57
|
+
import * as path from "node:path";
|
|
58
|
+
import { fileURLToPath } from "node:url";
|
|
59
|
+
import { type EntwurfFactsDeps, listEntwurfFacts } from "../pi-extensions/lib/entwurf-fact-provider.ts";
|
|
60
|
+
import { readHerdrPlacementIndex } from "../pi-extensions/lib/entwurf-peer-observe.ts";
|
|
61
|
+
import {
|
|
62
|
+
buildPlacementIndex,
|
|
63
|
+
type HerdrPaneRow,
|
|
64
|
+
joinKeyOf,
|
|
65
|
+
parseHerdrPaneList,
|
|
66
|
+
piNativeSessionIdFromPath,
|
|
67
|
+
renderPlacement,
|
|
68
|
+
resolvePlacement,
|
|
69
|
+
} from "../pi-extensions/lib/herdr-placement.ts";
|
|
70
|
+
import { type MetaIdentity, serializeMetaIdentity } from "../pi-extensions/lib/meta-session.ts";
|
|
71
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
72
|
+
|
|
73
|
+
const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
74
|
+
|
|
75
|
+
let passed = 0;
|
|
76
|
+
function ok(label: string, cond: boolean): void {
|
|
77
|
+
assert.ok(cond, label);
|
|
78
|
+
console.log(` ok ${label}`);
|
|
79
|
+
passed++;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Measured 2026-09-14, herdr 0.9.0: the claude pane carries the vendor uuid directly. */
|
|
83
|
+
const CLAUDE_NATIVE = "8f72ce79-b9dc-4607-83a8-fcc4aeceea69";
|
|
84
|
+
/** Measured the same day: the pi pane carries the session JSONL PATH, uuid inside. */
|
|
85
|
+
const PI_NATIVE = "01a09e58-f06a-70e8-b14a-1f0f0c7f7c7d";
|
|
86
|
+
const PI_PATH = `/home/junghan/.pi/agent/sessions/--home-junghan-repos-gh-entwurf--/2026-09-14T05-17-03-979Z_${PI_NATIVE}.jsonl`;
|
|
87
|
+
|
|
88
|
+
function identity(nativeSessionId: string, backend: MetaIdentity["backend"] = "claude-code"): MetaIdentity {
|
|
89
|
+
return {
|
|
90
|
+
schemaVersion: 3,
|
|
91
|
+
gardenId: `20260914T000000-${nativeSessionId.slice(0, 6)}`,
|
|
92
|
+
backend,
|
|
93
|
+
nativeSessionId,
|
|
94
|
+
cwd: "/x",
|
|
95
|
+
model: null,
|
|
96
|
+
transcriptPath: null,
|
|
97
|
+
createdAt: "2026-09-14T00:00:00.000Z",
|
|
98
|
+
recordUpdatedAt: "2026-09-14T00:00:00.000Z",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** An OFFICIAL report, the shape herdr 0.9.0 actually prints for each axis. */
|
|
103
|
+
function row(paneId: string, axis: "claude" | "pi", value: string | null): HerdrPaneRow {
|
|
104
|
+
return axis === "claude"
|
|
105
|
+
? { paneId, agent: "claude", sessionSource: "herdr:claude", sessionKind: "id", sessionValue: value }
|
|
106
|
+
: { paneId, agent: "pi", sessionSource: "herdr:pi", sessionKind: "path", sessionValue: value };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A pane carrying no session report at all — a bare shell. Not a decline. */
|
|
110
|
+
function bareRow(paneId: string): HerdrPaneRow {
|
|
111
|
+
return { paneId, agent: null, sessionSource: null, sessionKind: null, sessionValue: null };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function main(): Promise<void> {
|
|
115
|
+
// ── 1. the payload parse ────────────────────────────────────────────────────
|
|
116
|
+
{
|
|
117
|
+
// VERBATIM `herdr pane list` output, herdr 0.9.0, 2026-09-14 (fields trimmed to the
|
|
118
|
+
// ones this join reads — the parser ignores the rest by contract).
|
|
119
|
+
const real =
|
|
120
|
+
'{"id":"cli:pane:list","result":{"panes":[' +
|
|
121
|
+
'{"pane_id":"w1:p1","agent_status":"unknown","cwd":"/x"},' +
|
|
122
|
+
`{"agent":"claude","agent_session":{"agent":"claude","kind":"id","source":"herdr:claude","value":"${CLAUDE_NATIVE}"},"pane_id":"w2:p2"},` +
|
|
123
|
+
`{"agent":"pi","agent_session":{"agent":"pi","kind":"path","source":"herdr:pi","value":"${PI_PATH}"},"pane_id":"w2:p4"}` +
|
|
124
|
+
'],"type":"pane_list"}}';
|
|
125
|
+
const rows = parseHerdrPaneList(real);
|
|
126
|
+
assert.ok(rows, "the measured payload parses");
|
|
127
|
+
ok("the measured herdr 0.9.0 payload yields one row per pane", rows.length === 3);
|
|
128
|
+
ok("a pane with no agent_session parses to a keyless row (never dropped)", rows[0]?.sessionKind === null);
|
|
129
|
+
ok(
|
|
130
|
+
"the parser CARRIES agent + source, it does not discard them (they are what makes a report official)",
|
|
131
|
+
rows[1]?.agent === "claude" &&
|
|
132
|
+
rows[1]?.sessionSource === "herdr:claude" &&
|
|
133
|
+
rows[2]?.agent === "pi" &&
|
|
134
|
+
rows[2]?.sessionSource === "herdr:pi",
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
ok(
|
|
138
|
+
"[QK:HP-UNREADABLE-IS-UNOBSERVED] a payload this parser cannot read yields null — the caller then says " +
|
|
139
|
+
"`unobserved`, because reporting `none` for every citizen off an unreadable answer would be a fabricated " +
|
|
140
|
+
"measurement about a read that did not happen",
|
|
141
|
+
parseHerdrPaneList("not json") === null &&
|
|
142
|
+
parseHerdrPaneList("{}") === null &&
|
|
143
|
+
parseHerdrPaneList('{"result":{}}') === null &&
|
|
144
|
+
parseHerdrPaneList('{"result":{"panes":"nope"}}') === null,
|
|
145
|
+
);
|
|
146
|
+
ok(
|
|
147
|
+
"a pane row missing pane_id makes the WHOLE payload unreadable (never partially trusted)",
|
|
148
|
+
parseHerdrPaneList('{"result":{"panes":[{"agent":"pi"}]}}') === null,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
ok(
|
|
152
|
+
'[QK:HP-JOIN-CLAUDE-EXACT] a kind:"id" pane joins on the value BYTE-FOR-BYTE — the claude axis is a ' +
|
|
153
|
+
"string equality against a unique nativeSessionId, and a looser compare would attach a citizen to " +
|
|
154
|
+
"someone else's pane",
|
|
155
|
+
joinKeyOf(row("w2:p2", "claude", CLAUDE_NATIVE))?.nativeSessionId === CLAUDE_NATIVE &&
|
|
156
|
+
joinKeyOf(row("w2:p2", "claude", `${CLAUDE_NATIVE}x`))?.nativeSessionId !== CLAUDE_NATIVE,
|
|
157
|
+
);
|
|
158
|
+
ok(
|
|
159
|
+
'[QK:HP-JOIN-PI-FILENAME] a kind:"path" pane joins ONLY on a well-formed uuid tail of a .jsonl basename — ' +
|
|
160
|
+
"the key lives inside a vendor filename, so both ends are strict and a name that fails either test " +
|
|
161
|
+
"yields no key rather than a plausible-looking substring",
|
|
162
|
+
joinKeyOf(row("w2:p4", "pi", PI_PATH))?.nativeSessionId === PI_NATIVE &&
|
|
163
|
+
piNativeSessionIdFromPath(PI_PATH) === PI_NATIVE &&
|
|
164
|
+
piNativeSessionIdFromPath(`/s/2026_${PI_NATIVE}.txt`) === null &&
|
|
165
|
+
piNativeSessionIdFromPath("/s/2026-09-14T05-17-03-979Z_not-a-uuid.jsonl") === null &&
|
|
166
|
+
piNativeSessionIdFromPath(`/s/${PI_NATIVE}.jsonl`) === null,
|
|
167
|
+
);
|
|
168
|
+
ok(
|
|
169
|
+
"[QK:HP-JOIN-NEVER-GUESSES] an unmeasured agent_session kind and an empty value both yield NO key — an " +
|
|
170
|
+
"unknown herdr shape is declined, never coerced into one of the two rules we actually measured",
|
|
171
|
+
joinKeyOf({ ...row("w2:p9", "claude", CLAUDE_NATIVE), sessionKind: null }) === null &&
|
|
172
|
+
joinKeyOf(row("w2:p9", "claude", "")) === null,
|
|
173
|
+
);
|
|
174
|
+
ok(
|
|
175
|
+
"[QK:HP-JOIN-OFFICIAL-ONLY] a report from an UNOFFICIAL source, or one whose agent or shape does not match " +
|
|
176
|
+
"the source herdr is measured to emit, carries NO key — herdr accepts session reports from third-party " +
|
|
177
|
+
'integrations too, and "the placement owner reported it" is the whole reason mux-launch-rail.md section 7 ' +
|
|
178
|
+
"admits this as exact evidence, so the agent/source pair is checked rather than discarded",
|
|
179
|
+
joinKeyOf({ ...row("w2:p2", "claude", CLAUDE_NATIVE), sessionSource: "someone-else:claude" }) === null &&
|
|
180
|
+
joinKeyOf({ ...row("w2:p2", "claude", CLAUDE_NATIVE), agent: "pi" }) === null &&
|
|
181
|
+
joinKeyOf({ ...row("w2:p4", "pi", PI_PATH), sessionKind: "id" }) === null,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// ── 3. index + resolution vocabulary ────────────────────────────────────────
|
|
185
|
+
{
|
|
186
|
+
const index = buildPlacementIndex([
|
|
187
|
+
bareRow("w1:p1"),
|
|
188
|
+
row("w2:p2", "claude", CLAUDE_NATIVE),
|
|
189
|
+
row("w2:p4", "pi", PI_PATH),
|
|
190
|
+
]);
|
|
191
|
+
ok("a bare shell pane is not a declined report — there was nothing there to read", index.declinedReports === 0);
|
|
192
|
+
ok(
|
|
193
|
+
"a joined citizen resolves to its pane",
|
|
194
|
+
renderPlacement(resolvePlacement(index, identity(CLAUDE_NATIVE))) === "herdr w2:p2",
|
|
195
|
+
);
|
|
196
|
+
ok(
|
|
197
|
+
"the pi axis resolves through the filename rule to its pane",
|
|
198
|
+
renderPlacement(resolvePlacement(index, identity(PI_NATIVE, "pi"))) === "herdr w2:p4",
|
|
199
|
+
);
|
|
200
|
+
ok(
|
|
201
|
+
"a citizen herdr was READ about and does not have reads `none` — a real measurement with a negative result",
|
|
202
|
+
renderPlacement(resolvePlacement(index, identity("ffffffff-0000-0000-0000-000000000000"))) === "none",
|
|
203
|
+
);
|
|
204
|
+
ok(
|
|
205
|
+
"[QK:HP-NO-HERDR-IS-UNOBSERVED] a null index (no herdr on this host, or the read failed) leaves every " +
|
|
206
|
+
"citizen `unobserved` — the word for `nobody looked`, kept distinct from `none` so a host without a " +
|
|
207
|
+
"placement owner never appears to have measured anything",
|
|
208
|
+
renderPlacement(resolvePlacement(null, identity(CLAUDE_NATIVE))) === "unobserved",
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
ok(
|
|
212
|
+
"[QK:HP-JOIN-BACKEND-BOUND] a report speaks only for the backend its integration belongs to — a " +
|
|
213
|
+
"pi-reported pane handed to a claude-code citizen whose native id collides is a contradiction, not a " +
|
|
214
|
+
"placement, so it resolves to `unobserved` rather than naming that pane",
|
|
215
|
+
renderPlacement(resolvePlacement(index, identity(PI_NATIVE, "claude-code"))) === "unobserved",
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const declined = buildPlacementIndex([
|
|
219
|
+
row("w2:p2", "claude", CLAUDE_NATIVE),
|
|
220
|
+
{ ...row("w2:p7", "claude", "whoever"), sessionSource: "thirdparty:tool" },
|
|
221
|
+
]);
|
|
222
|
+
ok(
|
|
223
|
+
"[QK:HP-DECLINED-IS-UNOBSERVED] while ANY report was declined, a citizen that did not join reads " +
|
|
224
|
+
"`unobserved`, not `none` — the report we refused to read might have been that citizen's, so we " +
|
|
225
|
+
"declined to look rather than looked and found nothing",
|
|
226
|
+
declined.declinedReports === 1 &&
|
|
227
|
+
renderPlacement(resolvePlacement(declined, identity("ffffffff-0000-0000-0000-000000000000"))) === "unobserved",
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const dup = buildPlacementIndex([row("w2:p2", "claude", CLAUDE_NATIVE), row("w2:p5", "claude", CLAUDE_NATIVE)]);
|
|
231
|
+
ok(
|
|
232
|
+
"[QK:HP-AMBIGUOUS-NOT-A-PANE] two panes claiming one native session id resolve to `ambiguous`, and the " +
|
|
233
|
+
"key is removed from the index rather than settled by last-write-wins — naming one of them would be " +
|
|
234
|
+
"the guess this axis exists to refuse",
|
|
235
|
+
renderPlacement(resolvePlacement(dup, identity(CLAUDE_NATIVE))) === "ambiguous" &&
|
|
236
|
+
!dup.byNativeSessionId.has(CLAUDE_NATIVE),
|
|
237
|
+
);
|
|
238
|
+
ok(
|
|
239
|
+
"the same pane reported twice is NOT ambiguity (only a genuine second pane is)",
|
|
240
|
+
renderPlacement(
|
|
241
|
+
resolvePlacement(
|
|
242
|
+
buildPlacementIndex([row("w2:p2", "claude", CLAUDE_NATIVE), row("w2:p2", "claude", CLAUDE_NATIVE)]),
|
|
243
|
+
identity(CLAUDE_NATIVE),
|
|
244
|
+
),
|
|
245
|
+
) === "herdr w2:p2",
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── 4. the provider reads the placement owner exactly once ──────────────────
|
|
250
|
+
{
|
|
251
|
+
// TWO citizens on purpose: with one, a per-citizen read and a per-listing read are
|
|
252
|
+
// indistinguishable, and the anti-watcher claim below would pass over a defect.
|
|
253
|
+
const claude = identity(CLAUDE_NATIVE);
|
|
254
|
+
const other = identity("ffffffff-0000-0000-0000-000000000000");
|
|
255
|
+
const store: Record<string, string> = {
|
|
256
|
+
[`${claude.gardenId}.meta.json`]: serializeMetaIdentity(claude),
|
|
257
|
+
[`${other.gardenId}.meta.json`]: serializeMetaIdentity(other),
|
|
258
|
+
};
|
|
259
|
+
const base: EntwurfFactsDeps = {
|
|
260
|
+
metaEntries: Object.keys(store).map((filename) => ({ filename, regularFile: true })),
|
|
261
|
+
readRecord: (f: string) => {
|
|
262
|
+
const v = store[f];
|
|
263
|
+
if (v === undefined) throw new Error(`ENOENT: ${f}`);
|
|
264
|
+
return v;
|
|
265
|
+
},
|
|
266
|
+
socket: { readdir: async () => [], probe: async () => "dead" },
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
let reads = 0;
|
|
270
|
+
const withIndex: EntwurfFactsDeps = {
|
|
271
|
+
...base,
|
|
272
|
+
readPlacementIndex: () => {
|
|
273
|
+
reads++;
|
|
274
|
+
return buildPlacementIndex([row("w2:p2", "claude", CLAUDE_NATIVE)]);
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
const r = await listEntwurfFacts(withIndex);
|
|
278
|
+
ok(
|
|
279
|
+
"[QK:HP-ONE-READ-PER-LISTING] the provider reads the placement owner EXACTLY ONCE for a whole listing — " +
|
|
280
|
+
"herdr publishes no event that says a pane's agent_session has settled, so a per-citizen read or a " +
|
|
281
|
+
"retry loop around that gap would be the discovery watcher docs/mux-launch-rail.md §7 refuses by name",
|
|
282
|
+
reads === 1,
|
|
283
|
+
);
|
|
284
|
+
const joined = r.facts.peers.find((p) => p.gardenId === claude.gardenId);
|
|
285
|
+
const unjoined = r.facts.peers.find((p) => p.gardenId === other.gardenId);
|
|
286
|
+
ok(
|
|
287
|
+
"one listing carries both a joined citizen and a measured-absent one",
|
|
288
|
+
r.facts.peers.length === 2 && joined?.placement.kind === "herdr-pane" && unjoined?.placement.kind === "none",
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const withoutHerdr = await listEntwurfFacts({ ...base, readPlacementIndex: () => null });
|
|
292
|
+
ok(
|
|
293
|
+
"a host with no placement owner still lists its citizens, with placement `unobserved` (never a refusal)",
|
|
294
|
+
withoutHerdr.facts.peers.length === 2 && withoutHerdr.facts.peers.every((p) => p.placement.kind === "unobserved"),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── 5. placement is evidence, never a rail ──────────────────────────────────
|
|
299
|
+
{
|
|
300
|
+
// A source-level absence, not a behavioural one: the moment a dispatch module can
|
|
301
|
+
// SEE the placement axis, a future edit can route on a pane that is one server
|
|
302
|
+
// restart from being false. Hard Rule 16 / docs/mux-launch-rail.md §7.
|
|
303
|
+
const dispatchSubjects = [
|
|
304
|
+
"pi-extensions/lib/entwurf-v2-contract.ts",
|
|
305
|
+
"pi-extensions/lib/entwurf-v2-production.ts",
|
|
306
|
+
"pi-extensions/lib/entwurf-v2-decider.ts",
|
|
307
|
+
"pi-extensions/lib/entwurf-v2-send.ts",
|
|
308
|
+
"pi-extensions/lib/entwurf-v2-mailbox.ts",
|
|
309
|
+
"pi-extensions/lib/entwurf-v2-native-push.ts",
|
|
310
|
+
];
|
|
311
|
+
const leaking = dispatchSubjects.filter((rel) => {
|
|
312
|
+
const src = readFileSync(path.join(REPO_DIR, rel), "utf8");
|
|
313
|
+
return src.includes("herdr-placement") || src.includes("HERDR_") || /\bplacement\b/.test(src);
|
|
314
|
+
});
|
|
315
|
+
ok(
|
|
316
|
+
"[QK:HP-PLACEMENT-NOT-DISPATCH] no entwurf_v2 dispatch module imports, names, or reads the placement axis — " +
|
|
317
|
+
`a pane is an ephemeral view and must never reach a routing decision (leaking: ${leaking.join(", ")})`,
|
|
318
|
+
leaking.length === 0,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── 6. the production READ — the spawn seam, on a stub PROCESS ──────────────
|
|
323
|
+
{
|
|
324
|
+
const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-herdr-read."));
|
|
325
|
+
reclaimOnExit(sandbox);
|
|
326
|
+
const witness = path.join(sandbox, "spawned");
|
|
327
|
+
const stub = (body: string): string => {
|
|
328
|
+
const file = path.join(sandbox, `stub-${Math.random().toString(36).slice(2)}.sh`);
|
|
329
|
+
// EVERY stub records that it ran, so "returned null" and "was never spawned" are
|
|
330
|
+
// different observations rather than the same one.
|
|
331
|
+
fs.writeFileSync(file, `#!/usr/bin/env bash\nprintf '%s\\n' "$*" >> ${JSON.stringify(witness)}\n${body}\n`);
|
|
332
|
+
fs.chmodSync(file, 0o755);
|
|
333
|
+
return file;
|
|
334
|
+
};
|
|
335
|
+
const spawns = (): string[] =>
|
|
336
|
+
fs.existsSync(witness) ? fs.readFileSync(witness, "utf8").split("\n").filter(Boolean) : [];
|
|
337
|
+
const RECORDED = `{"id":"cli:pane:list","result":{"panes":[{"agent":"claude","agent_session":{"agent":"claude","kind":"id","source":"herdr:claude","value":"${CLAUDE_NATIVE}"},"pane_id":"w2:p2"},{"agent":"pi","agent_session":{"agent":"pi","kind":"path","source":"herdr:pi","value":"${PI_PATH}"},"pane_id":"w2:p4"}],"type":"pane_list"}}`;
|
|
338
|
+
|
|
339
|
+
const good = stub(`cat <<'JSON'\n${RECORDED}\nJSON`);
|
|
340
|
+
const before = spawns().length;
|
|
341
|
+
const noEnv = readHerdrPlacementIndex({ HERDR_BIN_PATH: good });
|
|
342
|
+
const noBin = readHerdrPlacementIndex({ HERDR_ENV: "1" });
|
|
343
|
+
const wrongEnv = readHerdrPlacementIndex({ HERDR_ENV: "true", HERDR_BIN_PATH: good });
|
|
344
|
+
const gatedSpawns = spawns().length - before;
|
|
345
|
+
|
|
346
|
+
const failing = stub("exit 1");
|
|
347
|
+
const failed = readHerdrPlacementIndex({ HERDR_ENV: "1", HERDR_BIN_PATH: failing });
|
|
348
|
+
const garbling = stub("echo 'not json at all'");
|
|
349
|
+
const garbled = readHerdrPlacementIndex({ HERDR_ENV: "1", HERDR_BIN_PATH: garbling });
|
|
350
|
+
const read = readHerdrPlacementIndex({ HERDR_ENV: "1", HERDR_BIN_PATH: good });
|
|
351
|
+
const argv = spawns();
|
|
352
|
+
|
|
353
|
+
ok(
|
|
354
|
+
"[QK:HP-READ-ENV-GATED-NULL-NEVER-EMPTY] the read is gated on herdr's OWN two env facts and spawns nothing without them, asks the fixed `pane list` argv, and turns a failed or unreadable answer into null — never into an empty index, which would claim herdr was read and has none of your citizens",
|
|
355
|
+
noEnv === null &&
|
|
356
|
+
noBin === null &&
|
|
357
|
+
// `HERDR_ENV` is read for its EXACT value: anything else is not herdr saying so.
|
|
358
|
+
wrongEnv === null &&
|
|
359
|
+
gatedSpawns === 0 &&
|
|
360
|
+
failed === null &&
|
|
361
|
+
garbled === null &&
|
|
362
|
+
read !== null &&
|
|
363
|
+
read.byNativeSessionId.size === 2 &&
|
|
364
|
+
argv.length === 3 &&
|
|
365
|
+
argv.every((line) => line === "pane list"),
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
// A hang is the failure a bound exists for, and it is the one no other cell reaches: the
|
|
369
|
+
// process is alive and silent, so nothing but the timeout ends the wait.
|
|
370
|
+
const hanging = stub("sleep 30");
|
|
371
|
+
const started = Date.now();
|
|
372
|
+
const hung = readHerdrPlacementIndex({ HERDR_ENV: "1", HERDR_BIN_PATH: hanging });
|
|
373
|
+
const waited = Date.now() - started;
|
|
374
|
+
ok(
|
|
375
|
+
`[QK:HP-READ-TIMEOUT-BOUNDED] a placement owner that never answers costs the listing a BOUNDED wait and then reads unobserved — measured ${waited}ms against the 2000ms bound`,
|
|
376
|
+
hung === null && waited < 10_000,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
console.log(`\n[check-herdr-placement] ${passed} assertions ok`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
main();
|