@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,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
|
+
}
|