@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.
Files changed (135) hide show
  1. package/AGENTS.md +4 -1
  2. package/BASELINE.md +1 -0
  3. package/CHANGELOG.md +514 -0
  4. package/DELIVERY.md +10 -2
  5. package/README.md +43 -4
  6. package/VERIFY.md +47 -0
  7. package/docs/acp-backend-rail.md +96 -17
  8. package/docs/setup-clean-host.md +11 -1
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +39 -40
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +95 -56
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +46 -273
  27. package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
  28. package/mcp/entwurf-bridge/src/index.ts +41 -41
  29. package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
  30. package/package.json +9 -8
  31. package/pi-extensions/entwurf-control.ts +91 -52
  32. package/pi-extensions/lib/acp/acp-client.ts +3 -3
  33. package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
  34. package/pi-extensions/lib/acp/backend.ts +4 -4
  35. package/pi-extensions/lib/acp/event-mapper.ts +5 -5
  36. package/pi-extensions/lib/acp/tool-surface.ts +2 -1
  37. package/pi-extensions/lib/codex-declaration.js +612 -0
  38. package/pi-extensions/lib/codex-fresh-preflight.ts +107 -51
  39. package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
  40. package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
  41. package/pi-extensions/lib/entwurf-facts.ts +14 -1
  42. package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
  43. package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
  44. package/pi-extensions/lib/entwurf-self-address.ts +33 -0
  45. package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
  46. package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
  47. package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
  48. package/pi-extensions/lib/fresh-call-composition.ts +493 -0
  49. package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
  50. package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
  51. package/pi-extensions/lib/herdr-placement.ts +276 -0
  52. package/pi-extensions/lib/mux-fresh-call.ts +95 -301
  53. package/run.sh +588 -204
  54. package/scripts/check-acp-usage-accounting.ts +9 -9
  55. package/scripts/check-codex-birth-hook.ts +187 -0
  56. package/scripts/check-entwurf-control-rpc.ts +138 -0
  57. package/scripts/check-entwurf-fact-provider.ts +4 -2
  58. package/scripts/check-entwurf-peers-surface.ts +3 -1
  59. package/scripts/check-entwurf-self-address.ts +68 -0
  60. package/scripts/check-fresh-call-dispatch.ts +301 -0
  61. package/scripts/check-gate-qualification.ts +17 -4
  62. package/scripts/check-herdr-activation.ts +1293 -0
  63. package/scripts/check-herdr-fresh-call.ts +1250 -0
  64. package/scripts/check-herdr-placement.ts +383 -0
  65. package/scripts/check-herdr-plugin-build.ts +842 -0
  66. package/scripts/check-herdr-plugin-profile.ts +371 -0
  67. package/scripts/check-herdr-plugin.ts +523 -0
  68. package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
  69. package/scripts/check-herdr-sandbox.ts +456 -0
  70. package/scripts/check-herdr-supply.ts +184 -0
  71. package/scripts/check-mux-launcher-fence.ts +138 -0
  72. package/scripts/check-peer-facts.ts +331 -0
  73. package/scripts/check-pi-launch.ts +358 -0
  74. package/scripts/check-release-gate-outcomes.ts +19 -0
  75. package/scripts/check-typing-call-fence.ts +296 -0
  76. package/scripts/codex-birth-doctor.sh +159 -16
  77. package/scripts/codex-birth-install.sh +178 -72
  78. package/scripts/codex-birth-uninstall.sh +162 -7
  79. package/scripts/fixtures/herdr-supply.json +29 -0
  80. package/scripts/herdr-activation.mjs +536 -0
  81. package/scripts/herdr-plugin-activate.mjs +270 -0
  82. package/scripts/herdr-plugin-deactivate.mjs +193 -0
  83. package/scripts/herdr-runtime.mjs +1203 -0
  84. package/scripts/install-herdr-ci.sh +96 -0
  85. package/scripts/inventory-verification-surface.ts +86 -10
  86. package/scripts/lib/codex-fresh-live-protocol.ts +2 -2
  87. package/scripts/lib/tree-digest.ts +97 -0
  88. package/scripts/meta-bridge-install.sh +19 -2
  89. package/scripts/meta-bridge-state.py +167 -11
  90. package/scripts/mutants/codex-caller-seat.json +6 -8
  91. package/scripts/mutants/codex-native.json +195 -29
  92. package/scripts/mutants/control-socket-disconnect.json +57 -0
  93. package/scripts/mutants/fresh-call-dispatch.json +154 -0
  94. package/scripts/mutants/herdr-activation.json +305 -0
  95. package/scripts/mutants/herdr-fresh-call.json +448 -0
  96. package/scripts/mutants/herdr-placement.json +161 -0
  97. package/scripts/mutants/herdr-plugin-build.json +160 -0
  98. package/scripts/mutants/herdr-plugin-profile.json +202 -0
  99. package/scripts/mutants/herdr-plugin.json +133 -0
  100. package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
  101. package/scripts/mutants/herdr-supply.json +133 -0
  102. package/scripts/mutants/meta-hook-session-switch.json +2 -2
  103. package/scripts/mutants/mux-fresh-call.json +104 -17
  104. package/scripts/mutants/mux-launcher-fence.json +13 -0
  105. package/scripts/mutants/omp-fresh.json +4 -4
  106. package/scripts/mutants/peer-facts.json +98 -0
  107. package/scripts/mutants/pi-launch.json +113 -0
  108. package/scripts/mutants/resume-args.json +6 -6
  109. package/scripts/mutants/self-address.json +33 -0
  110. package/scripts/mutants/typing-call-fence.json +17 -0
  111. package/scripts/mutants/v2-surface.json +2 -2
  112. package/scripts/mutants/v2-visible-resume.json +17 -17
  113. package/scripts/peer-facts.ts +120 -0
  114. package/scripts/register-pi-package.py +8 -0
  115. package/scripts/register-pi-provider.py +88 -12
  116. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  117. package/scripts/smoke-codex-birth.sh +281 -12
  118. package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
  119. package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
  120. package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
  121. package/scripts/smoke-herdr-raw-install-live.sh +849 -0
  122. package/scripts/tsconfig.json +5 -0
  123. package/scripts/check-entwurf-facts.ts +0 -385
  124. package/scripts/check-entwurf-resume-args.ts +0 -122
  125. package/scripts/check-entwurf-v2-decider.ts +0 -657
  126. package/scripts/check-entwurf-v2-lock.ts +0 -371
  127. package/scripts/check-entwurf-v2-mailbox.ts +0 -261
  128. package/scripts/check-entwurf-v2-matrix.ts +0 -401
  129. package/scripts/check-entwurf-v2-native-push.ts +0 -221
  130. package/scripts/check-entwurf-v2-release.ts +0 -151
  131. package/scripts/check-entwurf-v2-runner.ts +0 -448
  132. package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
  133. package/scripts/check-entwurf-v2-send.ts +0 -458
  134. package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
  135. package/scripts/check-socket-discovery.ts +0 -401
@@ -0,0 +1,276 @@
1
+ /**
2
+ * herdr-placement — the PURE half of the placement evidence axis (#116 S1).
3
+ *
4
+ * WHAT THIS IS FOR. A citizen opened inside herdr is visible in one of herdr's panes.
5
+ * That pane is an EPHEMERAL VIEW, never an address and never a liveness receipt
6
+ * (AGENTS Hard Rule 16, `docs/mux-launch-rail.md` §7). It earns a column in
7
+ * `entwurf_peers` for one reason: on a herdr host the operator's own question is
8
+ * "which pane is that one in", and answering it by hand means reading two listings
9
+ * side by side.
10
+ *
11
+ * WHY THIS JOIN IS EXACT EVIDENCE AND NOT A GUESS. §7 admitted exactly two forms of
12
+ * exact placement evidence (our own launch receipt, and a peer's verifiable
13
+ * self-report) and named the guesses it refuses: window title, cwd match, and time
14
+ * proximity. This is a THIRD form and it belongs to neither the receipts nor the
15
+ * guesses: herdr reports the native session id its OWN OFFICIAL integration told it,
16
+ * and `nativeSessionId` is unique across the whole meta-store (Hard Rule 7). Two
17
+ * independently-owned facts meet on one unique key. Nothing here narrows candidates
18
+ * by cwd, reads a title, or prefers the nearest timestamp. §7 was amended in the same
19
+ * change that added this module; if the two ever disagree, §7 is the owning document.
20
+ *
21
+ * THE TWO AXES ARE NOT EQUALLY EXACT, AND THE DIFFERENCE IS NAMED. On the claude axis
22
+ * the reported value IS the key, byte for byte. On the pi axis it is a session FILE
23
+ * PATH, and the key is recovered from that filename by a strict conversion measured on
24
+ * pi 0.85.1 — a VENDOR FLOOR, not a key equality. Calling both "a unique-key join"
25
+ * would hide which one can drift when a vendor renames a file. The conversion is
26
+ * deliberately strict at both ends so that drift lands as a missed join, never a wrong
27
+ * one, and a listing that declined to read anything says so (see `declinedReports`).
28
+ *
29
+ * ONE READ, NO WATCHER. The observer performs exactly ONE `pane list` read for a
30
+ * whole listing and never retries. herdr publishes no event that says "this pane's
31
+ * `agent_session` is now settled" — measured on herdr 0.9.0: `pane.updated` is the
32
+ * only event whose payload carries `agent_session` and plugin `[[events]]` refuses
33
+ * that name, `events.subscribe` offers three pane kinds and none of them carry it,
34
+ * and `pane.agent_detected` arrives BEFORE the session reference exists. A retry loop
35
+ * around that gap is a discovery watcher, which §7 forbids by name. So a citizen
36
+ * whose pane has not reported yet reads `none` on this pass and is simply asked again
37
+ * next time.
38
+ *
39
+ * FACTS, NOT VERBS. Nothing here returns a transport, a target, or a suggestion. A
40
+ * pane id must never reach dispatch: `entwurf_v2` resolves its rail from the record
41
+ * and a live probe, and a placement that is one server restart from being false has
42
+ * no business in that decision.
43
+ */
44
+
45
+ import type { MetaIdentity } from "./meta-session.ts";
46
+
47
+ /**
48
+ * Where a citizen was found, as OBSERVED for this listing. Four words, and the two
49
+ * blank ones are deliberately distinct:
50
+ * `unobserved` nobody looked — no herdr on this host, or the read failed. This is
51
+ * the honest word for "not measured", never a quiet stand-in for the
52
+ * next one, and it is what a host with no herdr always reads.
53
+ * `none` herdr WAS read and this citizen is in none of its panes. A real
54
+ * measurement with a negative result.
55
+ * `ambiguous` two or more panes claim this citizen's native session id. herdr
56
+ * shows it, but not in one place, so naming a pane would be a guess.
57
+ * `herdr-pane` exactly one pane joined on the unique key.
58
+ */
59
+ export type PlacementObservation =
60
+ | { readonly kind: "unobserved" }
61
+ | { readonly kind: "none" }
62
+ | { readonly kind: "ambiguous" }
63
+ | { readonly kind: "herdr-pane"; readonly paneId: string };
64
+
65
+ export const UNOBSERVED_PLACEMENT: PlacementObservation = { kind: "unobserved" };
66
+ export const NO_PLACEMENT: PlacementObservation = { kind: "none" };
67
+ export const AMBIGUOUS_PLACEMENT: PlacementObservation = { kind: "ambiguous" };
68
+
69
+ /**
70
+ * The two shapes herdr reports a native session in, measured on herdr 0.9.0:
71
+ * `id` claude — the vendor's own session uuid, byte-identical to our
72
+ * `nativeSessionId`.
73
+ * `path` pi — the absolute path of the session JSONL. The uuid we key on is
74
+ * INSIDE that filename, so this arm needs the conversion below.
75
+ * Any other word is a herdr version we have not measured. It is declined, never
76
+ * coerced into one of these two.
77
+ */
78
+ export type HerdrAgentSessionKind = "id" | "path";
79
+
80
+ /** One pane as herdr reported it, reduced to the fields this join reads. The
81
+ * `agent`/`source` pair is carried, not discarded: it is what makes a row an
82
+ * OFFICIAL integration report rather than any report (see `OFFICIAL_REPORTS`). */
83
+ export interface HerdrPaneRow {
84
+ readonly paneId: string;
85
+ readonly agent: string | null;
86
+ readonly sessionSource: string | null;
87
+ readonly sessionKind: HerdrAgentSessionKind | null;
88
+ readonly sessionValue: string | null;
89
+ }
90
+
91
+ /**
92
+ * The OFFICIAL integration reports this join accepts, and nothing else.
93
+ *
94
+ * WHY THIS TABLE EXISTS. herdr accepts a session reference from any integration,
95
+ * including one a user wrote (`integrations.mdx:65-90 @ c77af189`), and the row
96
+ * carries who reported it in `agent_session.agent` / `.source`. Reading only
97
+ * `kind`/`value` would have let a third party's report stand in for the placement
98
+ * owner's own — and "the placement owner reported it" is the entire reason
99
+ * `docs/mux-launch-rail.md` §7 admits this as exact evidence. So the triple is
100
+ * pinned: source, agent, AND the shape that source is measured to emit. A row that
101
+ * misses any leg carries no key.
102
+ *
103
+ * `backend` is the meta-record backend this report may speak for. It is checked at
104
+ * resolve time so a claude-reported pane can never be handed to a pi citizen whose
105
+ * native id happens to collide.
106
+ */
107
+ const OFFICIAL_REPORTS: Readonly<
108
+ Record<string, { readonly agent: string; readonly kind: HerdrAgentSessionKind; readonly backend: string }>
109
+ > = {
110
+ "herdr:claude": { agent: "claude", kind: "id", backend: "claude-code" },
111
+ "herdr:pi": { agent: "pi", kind: "path", backend: "pi" },
112
+ };
113
+
114
+ /**
115
+ * A pi session filename, measured 2026-09-14 on pi 0.85.1:
116
+ *
117
+ * 2026-09-14T05-17-03-979Z_01a09e58-f06a-70e8-b14a-1f0f0c7f7c7d.jsonl
118
+ * └────────── start stamp ─────────┘ └──────── nativeSessionId ────────┘
119
+ *
120
+ * WHY THIS IS PINNED HERE AND NOT INFERRED. The uuid is the join key and it lives in
121
+ * a VENDOR filename, so this rule depends on pi's naming and would break silently if
122
+ * pi changed it. **Vendor floor: pi 0.85.1, measured 2026-09-14** — whether that layout
123
+ * is a vendor contract or a convention is NOT measured, so this is the one place in the
124
+ * axis that can drift under us. It is therefore strict on both ends — the name must end
125
+ * in `.jsonl` and the tail after the last `_` must be a well-formed uuid — and a name
126
+ * that fails either test yields no key at all rather than a substring that happens to
127
+ * look plausible. Drift then lands as a DECLINED report (the citizen reads `unobserved`,
128
+ * not `none`), never as a wrong join onto someone else's pane.
129
+ */
130
+ const PI_SESSION_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
131
+ const PI_SESSION_SUFFIX = ".jsonl";
132
+
133
+ export function piNativeSessionIdFromPath(value: string): string | null {
134
+ if (!value.endsWith(PI_SESSION_SUFFIX)) return null;
135
+ const stem = value.slice(0, -PI_SESSION_SUFFIX.length);
136
+ const base = stem.slice(stem.lastIndexOf("/") + 1);
137
+ const cut = base.lastIndexOf("_");
138
+ if (cut < 0) return null;
139
+ const candidate = base.slice(cut + 1);
140
+ return PI_SESSION_UUID.test(candidate) ? candidate : null;
141
+ }
142
+
143
+ /** What one pane contributes to the join: the key and the backend the reporting
144
+ * integration speaks for. `null` means this row carries nothing we may read. */
145
+ export interface HerdrJoinEntry {
146
+ readonly nativeSessionId: string;
147
+ readonly backend: string;
148
+ }
149
+
150
+ export function joinKeyOf(row: HerdrPaneRow): HerdrJoinEntry | null {
151
+ if (row.sessionSource === null) return null;
152
+ const official = OFFICIAL_REPORTS[row.sessionSource];
153
+ // An unmeasured source — a herdr version we have not run, or a third-party
154
+ // integration. Declined on purpose: coercing it would invent a rule, and the
155
+ // whole exact-evidence argument rests on WHO reported.
156
+ if (official === undefined) return null;
157
+ if (row.agent !== official.agent) return null;
158
+ if (row.sessionKind !== official.kind) return null;
159
+ if (row.sessionValue === null || row.sessionValue.length === 0) return null;
160
+ const nativeSessionId = official.kind === "id" ? row.sessionValue : piNativeSessionIdFromPath(row.sessionValue);
161
+ return nativeSessionId === null ? null : { nativeSessionId, backend: official.backend };
162
+ }
163
+
164
+ /**
165
+ * The join index for ONE listing.
166
+ *
167
+ * `ambiguous` is carried separately rather than letting last-write-wins pick a pane,
168
+ * because "herdr shows this twice" and "herdr shows this here" are different facts and
169
+ * only one of them is a placement.
170
+ *
171
+ * `declinedReports` is what makes a non-join honest. A pane that carried a session
172
+ * report we refused to read (unofficial source, wrong agent, wrong shape, unparsable
173
+ * value) might have BEEN the citizen we are now failing to find — we declined to look,
174
+ * we did not look and find nothing. So while any row was declined, a non-join reads
175
+ * `unobserved` rather than `none`. A bare shell pane with no report at all is not a
176
+ * decline: there was nothing there to read.
177
+ */
178
+ export interface HerdrPlacementIndex {
179
+ readonly byNativeSessionId: ReadonlyMap<string, HerdrJoinEntry & { readonly paneId: string }>;
180
+ readonly ambiguous: ReadonlySet<string>;
181
+ readonly declinedReports: number;
182
+ }
183
+
184
+ export function buildPlacementIndex(rows: readonly HerdrPaneRow[]): HerdrPlacementIndex {
185
+ const byNativeSessionId = new Map<string, HerdrJoinEntry & { paneId: string }>();
186
+ const ambiguous = new Set<string>();
187
+ let declinedReports = 0;
188
+ for (const row of rows) {
189
+ const entry = joinKeyOf(row);
190
+ if (entry === null) {
191
+ // Only a row that actually CARRIED a report counts as declined.
192
+ if (row.sessionSource !== null || row.sessionValue !== null) declinedReports++;
193
+ continue;
194
+ }
195
+ const key = entry.nativeSessionId;
196
+ if (ambiguous.has(key)) continue;
197
+ const seen = byNativeSessionId.get(key);
198
+ if (seen !== undefined && seen.paneId !== row.paneId) {
199
+ byNativeSessionId.delete(key);
200
+ ambiguous.add(key);
201
+ continue;
202
+ }
203
+ byNativeSessionId.set(key, { ...entry, paneId: row.paneId });
204
+ }
205
+ return { byNativeSessionId, ambiguous, declinedReports };
206
+ }
207
+
208
+ /**
209
+ * Parse what `herdr pane list` printed. Returns `null` — "nobody looked" — for any
210
+ * payload this does not recognise, because a herdr that answered in a shape we cannot
211
+ * read has told us nothing, and reporting `none` for every citizen on that basis
212
+ * would be a fabricated measurement. A row missing `pane_id` is the one hard error:
213
+ * the whole payload is unreadable rather than partially trusted.
214
+ */
215
+ export function parseHerdrPaneList(stdout: string): HerdrPaneRow[] | null {
216
+ let raw: unknown;
217
+ try {
218
+ raw = JSON.parse(stdout);
219
+ } catch {
220
+ return null;
221
+ }
222
+ if (typeof raw !== "object" || raw === null) return null;
223
+ const result = (raw as { result?: unknown }).result;
224
+ if (typeof result !== "object" || result === null) return null;
225
+ const panes = (result as { panes?: unknown }).panes;
226
+ if (!Array.isArray(panes)) return null;
227
+ const rows: HerdrPaneRow[] = [];
228
+ for (const pane of panes) {
229
+ if (typeof pane !== "object" || pane === null) return null;
230
+ const paneId = (pane as { pane_id?: unknown }).pane_id;
231
+ if (typeof paneId !== "string" || paneId.length === 0) return null;
232
+ const session = (pane as { agent_session?: unknown }).agent_session;
233
+ if (typeof session !== "object" || session === null) {
234
+ rows.push({ paneId, agent: null, sessionSource: null, sessionKind: null, sessionValue: null });
235
+ continue;
236
+ }
237
+ const agent = (session as { agent?: unknown }).agent;
238
+ const source = (session as { source?: unknown }).source;
239
+ const kind = (session as { kind?: unknown }).kind;
240
+ const value = (session as { value?: unknown }).value;
241
+ rows.push({
242
+ paneId,
243
+ agent: typeof agent === "string" ? agent : null,
244
+ sessionSource: typeof source === "string" ? source : null,
245
+ sessionKind: kind === "id" || kind === "path" ? kind : null,
246
+ sessionValue: typeof value === "string" ? value : null,
247
+ });
248
+ }
249
+ return rows;
250
+ }
251
+
252
+ /**
253
+ * The whole decision, for one citizen. A `null` index is the no-herdr host and every
254
+ * citizen on it reads `unobserved` — which is why this takes the index rather than a
255
+ * reader: the read happened once, above, and this stays pure.
256
+ */
257
+ export function resolvePlacement(index: HerdrPlacementIndex | null, identity: MetaIdentity): PlacementObservation {
258
+ if (index === null) return UNOBSERVED_PLACEMENT;
259
+ if (index.ambiguous.has(identity.nativeSessionId)) return AMBIGUOUS_PLACEMENT;
260
+ const entry = index.byNativeSessionId.get(identity.nativeSessionId);
261
+ if (entry !== undefined) {
262
+ // The reporting integration must speak for THIS citizen's backend. A pi
263
+ // integration reporting an id that collides with a claude-code record is a
264
+ // contradiction, not a placement, and we decline to claim either way.
265
+ return entry.backend === identity.backend ? { kind: "herdr-pane", paneId: entry.paneId } : UNOBSERVED_PLACEMENT;
266
+ }
267
+ // No join. `none` is only honest when the read was COMPLETE — if any report was
268
+ // declined, the thing we did not read might have been this one.
269
+ return index.declinedReports > 0 ? UNOBSERVED_PLACEMENT : NO_PLACEMENT;
270
+ }
271
+
272
+ /** Render one observation as the listing's column value. `herdr <pane>` says WHERE it
273
+ * was seen and by WHOM, so a reader never mistakes the pane id for one of ours. */
274
+ export function renderPlacement(placement: PlacementObservation): string {
275
+ return placement.kind === "herdr-pane" ? `herdr ${placement.paneId}` : placement.kind;
276
+ }