zob-harness 0.15.0 → 0.15.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.
@@ -246,6 +246,7 @@ function buildTeamAgentLease(repoRoot: string, peer: ZobLivePeerCard, input: { n
246
246
  contextUsedPct: peer.contextUsedPct,
247
247
  queueDepth: peer.queueDepth,
248
248
  status: peer.status === "offline" ? "offline" : "online",
249
+ socketVerifiedAt: peer.socketVerifiedAt,
249
250
  zpeerRoomId: peer.zpeerRoomId,
250
251
  zpeerAlias: peer.zpeerAlias,
251
252
  zpeerActiveRoomId: peer.zpeerActiveRoomId,
@@ -291,6 +292,7 @@ function leaseToPeerCard(lease: ZobLiveTeamAgentLease, nowMs: number): ZobLivePe
291
292
  contextUsedPct: lease.contextUsedPct,
292
293
  queueDepth: lease.queueDepth,
293
294
  status: deriveLeaseStatus(lease, nowMs),
295
+ socketVerifiedAt: lease.socketVerifiedAt,
294
296
  zpeerRoomId: lease.zpeerRoomId,
295
297
  zpeerAlias: lease.zpeerAlias,
296
298
  zpeerActiveRoomId: lease.zpeerActiveRoomId,
@@ -578,8 +580,9 @@ export async function sweepZobLivePeerHealth(repoRoot: string, input: { teamName
578
580
  }
579
581
  }
580
582
  if (responds) {
583
+ const livePeer = { ...peer, heartbeatAt: nowIso, status: "online", socketVerifiedAt: nowIso } as ZobLivePeerCard;
584
+ applySweepPeerUpdate(repoRoot, livePeer, nowMs);
581
585
  if (peer.status !== "online") {
582
- applySweepPeerUpdate(repoRoot, { ...peer, heartbeatAt: nowIso, status: "online", socketVerifiedAt: nowIso } as ZobLivePeerCard, nowMs);
583
586
  revived += 1;
584
587
  } else {
585
588
  retainedLive += 1;
@@ -104,6 +104,7 @@ export interface ZobLivePeerCard {
104
104
  contextUsedPct: number;
105
105
  queueDepth: number;
106
106
  status: ZobLivePeerStatus;
107
+ socketVerifiedAt?: string;
107
108
  zpeerRoomId?: string;
108
109
  zpeerAlias?: string;
109
110
  zpeerActiveRoomId?: string;
@@ -141,6 +142,7 @@ export interface ZobLiveTeamAgentLease {
141
142
  contextUsedPct: number;
142
143
  queueDepth: number;
143
144
  status: ZobLivePeerStatus;
145
+ socketVerifiedAt?: string;
144
146
  zpeerRoomId?: string;
145
147
  zpeerAlias?: string;
146
148
  zpeerActiveRoomId?: string;
@@ -75,6 +75,19 @@ export interface ZpeerSendFeedback {
75
75
  result: ZpeerSendResult;
76
76
  }
77
77
 
78
+ export type ZpeerFallbackDelivery = (input: {
79
+ targetAlias: string;
80
+ senderAlias: string;
81
+ roomId: string;
82
+ taskHash?: string;
83
+ prompt: string;
84
+ priority: ZpeerInterruptPriority;
85
+ interruptMode: ZpeerInterruptMode;
86
+ requireResponse: boolean;
87
+ responseTimeoutMs?: number;
88
+ maxReinjects?: number;
89
+ }) => Promise<{ delivered: boolean; target?: string; reason?: string }>;
90
+
78
91
  export interface ZpeerSendOptions {
79
92
  mode?: ZpeerSendMode;
80
93
  roomId?: string;
@@ -90,8 +103,8 @@ export interface ZpeerSendOptions {
90
103
  // the target agent's pane). This keeps coms-v2 IO-free; the app supplies the tmux
91
104
  // delivery. A fallback delivery is NOT verified delivery: outputHash stays absent,
92
105
  // confirmation_ref stays null, and bodyStored stays false (coms-safety: append-only /
93
- // best-effort is not delivery success).
94
- fallbackDelivery?: (input: { targetAlias: string; roomId: string; taskHash?: string; prompt: string }) => Promise<{ delivered: boolean; target?: string; reason?: string }>;
106
+ // best-effort is not delivery success). Force/abort is never eligible for fallback.
107
+ fallbackDelivery?: ZpeerFallbackDelivery;
95
108
  }
96
109
 
97
110
  interface ZpeerRoomPeer {
@@ -109,6 +122,37 @@ export function safeZpeerAlias(value: string | undefined): string | undefined {
109
122
  return trimmed;
110
123
  }
111
124
 
125
+ export function zpeerAliasLookupKey(value: string | undefined): string | undefined {
126
+ const alias = safeZpeerAlias(value);
127
+ return alias ? alias.replaceAll("-", "_") : undefined;
128
+ }
129
+
130
+ export function zpeerAliasesEquivalent(left: string | undefined, right: string | undefined): boolean {
131
+ const leftKey = zpeerAliasLookupKey(left);
132
+ const rightKey = zpeerAliasLookupKey(right);
133
+ return Boolean(leftKey && rightKey && leftKey === rightKey);
134
+ }
135
+
136
+ export function zpeerAliasIncluded(aliases: readonly string[] | undefined, alias: string | undefined): boolean {
137
+ const key = zpeerAliasLookupKey(alias);
138
+ return Boolean(key && aliases?.some((candidate) => zpeerAliasLookupKey(candidate) === key));
139
+ }
140
+
141
+ export function duplicateZpeerAliasLookupKeys(aliases: readonly string[]): string[] {
142
+ const seen = new Set<string>();
143
+ const duplicates = new Set<string>();
144
+ for (const alias of aliases) {
145
+ const key = zpeerAliasLookupKey(alias);
146
+ if (!key) continue;
147
+ if (seen.has(key)) duplicates.add(alias);
148
+ else seen.add(key);
149
+ }
150
+ return aliases.filter((alias) => {
151
+ const key = zpeerAliasLookupKey(alias);
152
+ return Boolean(key && duplicates.has(alias)) || Boolean(key && aliases.filter((candidate) => zpeerAliasLookupKey(candidate) === key).length > 1);
153
+ }).filter((alias, index, all) => all.indexOf(alias) === index).sort();
154
+ }
155
+
112
156
  export function safeZpeerRoomId(value: string | undefined): string | undefined {
113
157
  const trimmed = value?.trim();
114
158
  if (!trimmed || !ROOM_PATTERN.test(trimmed)) return undefined;
@@ -198,8 +242,12 @@ function withZpeerMembershipState(repoRoot: string, peer: ZobLivePeerCard, membe
198
242
  });
199
243
  }
200
244
 
245
+ function zpeerStrictSocketEvidenceRequired(): boolean {
246
+ return /^(1|true|yes|on|strict)$/i.test(process.env.ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET ?? "");
247
+ }
248
+
201
249
  function zpeerReachableStatus(peer: ZobLivePeerCard): ZobLivePeerStatus {
202
- if (peer.status === "online" && hasLocalSocketEndpointEvidence(peer)) return "online";
250
+ if (peer.status === "online" && hasLocalSocketEndpointEvidence(peer, { requireVerified: zpeerStrictSocketEvidenceRequired() })) return "online";
203
251
  if (peer.status === "stale") return "stale";
204
252
  return "offline";
205
253
  }
@@ -221,9 +269,34 @@ async function peerRespondsToAliasPing(peer: ZobLivePeerCard): Promise<boolean>
221
269
  }
222
270
  }
223
271
 
272
+ async function probeAndReviveZpeerCandidate(repoRoot: string, entry: ZpeerRoomPeer): Promise<ZpeerRoomPeer | undefined> {
273
+ if (!hasLocalSocketEndpointEvidence(entry.peer)) return undefined;
274
+ const nowIso = new Date().toISOString();
275
+ try {
276
+ const response = await sendZobLocalEnvelope(entry.peer.endpoint, {
277
+ schema: "zob.live-envelope.v1",
278
+ type: "ping",
279
+ msgId: `zpeer-candidate-probe:${entry.peer.roleId}:${Date.now()}`,
280
+ hops: 0,
281
+ timestamp: nowIso,
282
+ bodyStored: false,
283
+ }, { timeoutMs: 1_000 });
284
+ if (response.type !== "pong" && response.type !== "ack") return undefined;
285
+ const revived = { ...entry.peer, heartbeatAt: nowIso, status: "online", socketVerifiedAt: nowIso } as ZobLivePeerCard;
286
+ if (revived.zpeerAdhoc === true || revived.projectId !== buildZobComsProjectId(repoRoot)) {
287
+ writeZobLivePeerCardToProjectId(revived);
288
+ } else {
289
+ writeZobLiveTeamAgentLease(repoRoot, revived, { reason: "zpeer_candidate_probe" });
290
+ }
291
+ return { ...entry, peer: revived };
292
+ } catch {
293
+ return undefined;
294
+ }
295
+ }
296
+
224
297
  async function activeAliasCollision(repoRoot: string, self: ZobLivePeerCard, roomId: string, alias: string): Promise<ZpeerRoomPeer | undefined> {
225
298
  for (const entry of peersInRoom(repoRoot, roomId)) {
226
- if (entry.peer.sessionHash === self.sessionHash || entry.membership.alias !== alias) continue;
299
+ if (entry.peer.sessionHash === self.sessionHash || !zpeerAliasesEquivalent(entry.membership.alias, alias)) continue;
227
300
  if (await peerRespondsToAliasPing(entry.peer)) return entry;
228
301
  if (zpeerReachableStatus(entry.peer) === "online") {
229
302
  try { writeZobLivePeerCard(repoRoot, { ...entry.peer, heartbeatAt: new Date().toISOString(), status: "offline" }); } catch { /* best-effort stale alias release */ }
@@ -290,7 +363,7 @@ function buildZpeerRoomSummaryFromPeers(projectId: string, self: ZobLivePeerCard
290
363
  statusAliases[status].push(entry.membership.alias);
291
364
  }
292
365
  const onlineAliases = statusAliases.online.sort();
293
- const duplicateAliases = onlineAliases.filter((alias, index) => onlineAliases.indexOf(alias) !== index).filter((alias, index, all) => all.indexOf(alias) === index);
366
+ const duplicateAliases = duplicateZpeerAliasLookupKeys(onlineAliases);
294
367
  return {
295
368
  schema: "zob.zpeer-room-summary.v1",
296
369
  projectId,
@@ -560,8 +633,8 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
560
633
  if (priority === "force" && !ZPEER_FORCE_ALLOWED_SENDER_ROLE_TYPES.has(self.roleType)) return finish("attempt", { status: "blocked", reason: `force interrupt not allowed from role type ${self.roleType}`, targetAlias: targetAlias ?? undefined, taskHash, interruptStatus: "force_blocked", bodyStored: false });
561
634
  if (!targetAlias) return finish("attempt", { status: "blocked", reason: "invalid target alias", bodyStored: false });
562
635
  if (!transientPrompt.trim()) return finish("attempt", { status: "blocked", reason: "empty peer prompt", targetAlias, bodyStored: false });
563
- const candidates = peersInRoom(repoRoot, roomId).filter((entry) => entry.membership.alias === targetAlias && entry.peer.sessionHash !== self.sessionHash);
564
- if (targetAlias === senderAlias) return finish("attempt", { status: "blocked", reason: "cannot send to self", targetAlias, taskHash, bodyStored: false });
636
+ const candidates = peersInRoom(repoRoot, roomId).filter((entry) => zpeerAliasesEquivalent(entry.membership.alias, targetAlias) && entry.peer.sessionHash !== self.sessionHash);
637
+ if (zpeerAliasesEquivalent(targetAlias, senderAlias)) return finish("attempt", { status: "blocked", reason: "cannot send to self", targetAlias, taskHash, bodyStored: false });
565
638
  if (candidates.length === 0) return finish("attempt", { status: "blocked", reason: `peer @${targetAlias} not found in room '${roomId}'`, targetAlias, taskHash, bodyStored: false }, 0);
566
639
  let liveCandidates = candidates.filter((entry) => zpeerReachableStatus(entry.peer) === "online");
567
640
  if (liveCandidates.length > 1) {
@@ -575,6 +648,14 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
575
648
  }
576
649
  liveCandidates = responsiveCandidates;
577
650
  }
651
+ if (liveCandidates.length === 0) {
652
+ const probedCandidates: ZpeerRoomPeer[] = [];
653
+ for (const entry of candidates) {
654
+ const revived = await probeAndReviveZpeerCandidate(repoRoot, entry);
655
+ if (revived) probedCandidates.push(revived);
656
+ }
657
+ liveCandidates = probedCandidates;
658
+ }
578
659
  if (liveCandidates.length === 0) {
579
660
  const statuses = [...new Set(candidates.map((entry) => zpeerReachableStatus(entry.peer)))].sort().join("/") || "offline";
580
661
  // WS-ZH4: last-resort fallback delivery. When local-socket transport is blocked
@@ -582,14 +663,15 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
582
663
  // pane), deliver the prompt best-effort. This is NOT verified delivery:
583
664
  // outputHash/confirmation_ref stay absent and bodyStored stays false; the receiver,
584
665
  // if it answers at all, answers via its own local_socket later (that ledgered reply
585
- // is what WS-Q5 counts). If no fallback is supplied or it fails/declines, fall
586
- // through to the standard blocked result unchanged.
587
- if (options.fallbackDelivery && targetAlias && priority === "normal") {
666
+ // is what WS-Q5 counts). Urgent/steer is eligible when the caller explicitly wires
667
+ // a safe fallback hook; force/abort is never eligible. If no fallback is supplied or
668
+ // it fails/declines, fall through to the standard blocked result unchanged.
669
+ if (options.fallbackDelivery && targetAlias && priority !== "force") {
588
670
  try {
589
- const fallback = await options.fallbackDelivery({ targetAlias, roomId, taskHash, prompt: transientPrompt });
671
+ const fallback = await options.fallbackDelivery({ targetAlias, senderAlias, roomId, taskHash, prompt: transientPrompt, priority, interruptMode, requireResponse, responseTimeoutMs: requireResponse ? responseTimeoutMs : undefined, maxReinjects: requireResponse ? maxReinjects : undefined });
590
672
  if (fallback.delivered) {
591
673
  const fallbackMsgId = `zpeer-fallback:${self.sessionHash.slice(0, 8)}:${Date.now()}`;
592
- return finish("attempt", { status: "delivered", reason: `tmux_sendkeys_fallback: peer @${targetAlias} socket ${statuses}; best-effort delivery to ${fallback.target ?? targetAlias} (not verified)`, msgId: fallbackMsgId, targetAlias, taskHash, deliveryMethod: "tmux_sendkeys", fallback_delivery: true, best_effort: true, bodyStored: false }, candidates.length);
674
+ return finish("attempt", { status: "delivered", reason: `tmux_sendkeys_fallback: peer @${targetAlias} socket ${statuses}; best-effort delivery to ${fallback.target ?? targetAlias} (not verified)`, msgId: fallbackMsgId, targetAlias, taskHash, deliveryStatus: "blocked", deliveryMethod: "tmux_sendkeys", fallback_delivery: true, best_effort: true, responseReceived: requireResponse ? false : undefined, bodyStored: false }, candidates.length);
593
675
  }
594
676
  } catch {
595
677
  // best-effort fallback failed; fall through to the standard blocked result.
@@ -3,7 +3,7 @@ import { basename, join, relative, resolve } from "node:path";
3
3
 
4
4
  import type { ModeName } from "../../core/types/core.js";
5
5
  import { MODE_TOOLS } from "../../core/constants.js";
6
- import { safeZpeerAlias, safeZpeerRoomId } from "./coms-v2/zpeer.js";
6
+ import { safeZpeerAlias, safeZpeerRoomId, zpeerAliasIncluded, zpeerAliasesEquivalent } from "./coms-v2/zpeer.js";
7
7
  import { parseJsonFile } from "../../core/utils/json.js";
8
8
  import { isSafeArtifactName } from "../../core/utils/paths.js";
9
9
  import { isRecord } from "../../core/utils/records.js";
@@ -438,7 +438,7 @@ function communicationPolicyNarrowingErrors(base: ZAgentCommunicationPolicy | un
438
438
  for (const room of overlay.allowedRooms ?? []) if (!base.allowedRooms.includes(room)) errors.push(`${label}.allowedRooms must be a subset of the base policy: ${room}`);
439
439
  }
440
440
  if (base?.allowedAliases) {
441
- for (const alias of overlay.allowedAliases ?? []) if (!base.allowedAliases.includes(alias)) errors.push(`${label}.allowedAliases must be a subset of the base policy: ${alias}`);
441
+ for (const alias of overlay.allowedAliases ?? []) if (!zpeerAliasIncluded(base.allowedAliases, alias)) errors.push(`${label}.allowedAliases must be a subset of the base policy: ${alias}`);
442
442
  }
443
443
  return errors;
444
444
  }
@@ -865,7 +865,7 @@ function policyAllowsZpeerContact(policy: ZAgentCommunicationPolicy | undefined,
865
865
  if (!policy) return true;
866
866
  if (policy.zpeerContact === false || policy.allowZpeerContact === false) return false;
867
867
  if (roomId && policy.allowedRooms && !policy.allowedRooms.includes(roomId)) return false;
868
- if (alias && policy.allowedAliases && !policy.allowedAliases.includes(alias)) return false;
868
+ if (alias && policy.allowedAliases && !zpeerAliasIncluded(policy.allowedAliases, alias)) return false;
869
869
  if (policy.requireActiveRoom && !roomId) return false;
870
870
  return true;
871
871
  }
@@ -876,7 +876,7 @@ export function zteamAllowsZpeerContact(team: ZTeamManifest, zagentId: string, r
876
876
  ...(team.members ?? []),
877
877
  ...(team.agents ?? []),
878
878
  ];
879
- const member = members.find((candidate) => zteamMemberAgentId(candidate) === zagentId || candidate.alias === alias);
879
+ const member = members.find((candidate) => zteamMemberAgentId(candidate) === zagentId || zpeerAliasesEquivalent(candidate.alias, alias));
880
880
  if (!member) return false;
881
881
  if (!policyAllowsZpeerContact(member.communicationPolicy, roomId, alias ?? member.alias)) return false;
882
882
  const rooms = zteamMemberRooms(member, team.defaultRoom);
@@ -11,7 +11,7 @@ import { bindZobLocalEndpoint, makeZobLocalEndpoint, sendZobLocalEnvelope } from
11
11
  import { readZobComsV2Policy } from "../domains/coms/coms-v2/policy.js";
12
12
  import { claimZobLiveTeamAgentLease, pruneExpiredZobLivePeers, registerCurrentZobLivePeer, releaseZobLiveTeamAgentLease, touchCurrentZobLivePeer, unregisterCurrentZobLivePeer, writeZobLivePeerCard } from "../domains/coms/coms-v2/registry.js";
13
13
  import { clearZpeerNewCarryoverProfile, readZpeerLocalProfile, readZpeerNewCarryoverProfile, writeZpeerLocalProfileFromPeer, writeZpeerNewCarryoverProfile, zpeerProfileIdIsSharedFallback } from "../domains/coms/coms-v2/zpeer-profile.js";
14
- import { buildZpeerPeerRoomSummaries, ensureZpeerFields, refreshZpeerSelf } from "../domains/coms/coms-v2/zpeer.js";
14
+ import { buildZpeerPeerRoomSummaries, ensureZpeerFields, refreshZpeerSelf, zpeerAliasesEquivalent } from "../domains/coms/coms-v2/zpeer.js";
15
15
  import type { ZpeerRoomMembership } from "../domains/coms/coms-v2/types.js";
16
16
  import { buildZobLiveResponseEnvelope } from "../domains/coms/coms-v2/response-capture.js";
17
17
  import { writeZobComsRedactedCapture } from "../domains/coms/coms-v2/transcript-capture.js";
@@ -69,7 +69,7 @@ function clearPassivePeerWaitForResponse(state: HarnessRuntimeState, envelope: {
69
69
  return;
70
70
  }
71
71
  const responseRoomId = envelope.runId?.startsWith("zpeer:") ? envelope.runId.slice("zpeer:".length) : undefined;
72
- if (!wait.msgId && wait.targetAlias && envelope.sender === wait.targetAlias && (!wait.roomId || !responseRoomId || wait.roomId === responseRoomId)) {
72
+ if (!wait.msgId && wait.targetAlias && zpeerAliasesEquivalent(envelope.sender, wait.targetAlias) && (!wait.roomId || !responseRoomId || wait.roomId === responseRoomId)) {
73
73
  state.zobLive.passivePeerWait = undefined;
74
74
  }
75
75
  }
@@ -1,7 +1,10 @@
1
+ import { isAbsolute, join } from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+
1
4
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
5
  import { readZobComsV2Policy } from "../domains/coms/coms-v2/policy.js";
3
6
  import { readZobLiveRegistrySnapshot } from "../domains/coms/coms-v2/registry.js";
4
- import { peerAliasInRoom, refreshZpeerSelf, safeZpeerAlias, safeZpeerRoomId, sendZpeerPrompt, type ZpeerSendMode, type ZpeerSendResult } from "../domains/coms/coms-v2/zpeer.js";
7
+ import { peerAliasInRoom, refreshZpeerSelf, safeZpeerAlias, safeZpeerRoomId, sendZpeerPrompt, zpeerAliasesEquivalent, zpeerAliasLookupKey, type ZpeerFallbackDelivery, type ZpeerSendMode, type ZpeerSendResult } from "../domains/coms/coms-v2/zpeer.js";
5
8
  import { buildZobLiveEnvelope, type ZpeerInterruptMode, type ZpeerInterruptPriority, type ZpeerInterruptStatus } from "../domains/coms/coms-v2/envelope.js";
6
9
  import { sendZobLocalEnvelope } from "../domains/coms/coms-v2/local-transport.js";
7
10
  import { buildZobLiveResponseEnvelope } from "../domains/coms/coms-v2/response-capture.js";
@@ -37,6 +40,44 @@ const ZPEER_AGENT_ASK_RATE_LIMIT_PER_MINUTE = 50;
37
40
  const ZPEER_AGENT_URGENT_RATE_LIMIT_PER_MINUTE = 10;
38
41
  const ZPEER_AGENT_FORCE_RATE_LIMIT_PER_MINUTE = 3;
39
42
 
43
+ type ZpeerFallbackHookFn = ZpeerFallbackDelivery;
44
+
45
+ function isObjectRecord(value: unknown): value is Record<string, unknown> {
46
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
47
+ }
48
+
49
+ function asZpeerFallbackHookFn(moduleValue: unknown): ZpeerFallbackHookFn | undefined {
50
+ if (typeof moduleValue === "function") return moduleValue as ZpeerFallbackHookFn;
51
+ if (!isObjectRecord(moduleValue)) return undefined;
52
+ const direct = moduleValue.zpeerFallbackDelivery ?? moduleValue.fallbackDelivery;
53
+ if (typeof direct === "function") return direct as ZpeerFallbackHookFn;
54
+ const defaultExport = moduleValue.default;
55
+ if (typeof defaultExport === "function") return defaultExport as ZpeerFallbackHookFn;
56
+ if (isObjectRecord(defaultExport)) {
57
+ const nested = defaultExport.zpeerFallbackDelivery ?? defaultExport.fallbackDelivery;
58
+ if (typeof nested === "function") return nested as ZpeerFallbackHookFn;
59
+ }
60
+ return undefined;
61
+ }
62
+
63
+ function buildProjectTransposerFallbackDelivery(repoRoot: string): ZpeerFallbackDelivery | undefined {
64
+ const hookPath = process.env.PROJECT_TRANSPOSER_ZPEER_FALLBACK_HOOK?.trim();
65
+ if (!hookPath) return undefined;
66
+ const resolvedHookPath = isAbsolute(hookPath) ? hookPath : join(repoRoot, hookPath);
67
+ return async (input) => {
68
+ const imported = await import(pathToFileURL(resolvedHookPath).href);
69
+ const fn = asZpeerFallbackHookFn(imported);
70
+ if (!fn) return { delivered: false, reason: "fallback_hook_missing_function" };
71
+ const result = await fn(input);
72
+ if (!isObjectRecord(result)) return { delivered: false, reason: "fallback_hook_invalid_result" };
73
+ return {
74
+ delivered: result.delivered === true,
75
+ target: typeof result.target === "string" ? result.target : undefined,
76
+ reason: typeof result.reason === "string" ? result.reason : undefined,
77
+ };
78
+ };
79
+ }
80
+
40
81
  function breakGlassApprovalPresent(): boolean {
41
82
  return SHA256_HEX.test(process.env.ZOB_COMS_BREAK_GLASS_APPROVAL_HASH ?? "");
42
83
  }
@@ -97,7 +138,7 @@ function normalizeZpeerInterrupt(params: ZpeerAskToolParams): { priority: ZpeerI
97
138
  function zpeerAskGuardBlock(state: HarnessRuntimeState, params: ZpeerAskToolParams, selfAlias?: string, currentRoomId = "default", priority: ZpeerInterruptPriority = "normal"): string | undefined {
98
139
  const targetAlias = safeZpeerAlias(params.targetAlias);
99
140
  if (!targetAlias) return "invalid target alias";
100
- if (selfAlias && targetAlias === selfAlias) return "cannot send to self";
141
+ if (selfAlias && zpeerAliasesEquivalent(targetAlias, selfAlias)) return "cannot send to self";
101
142
  const roomId = safeZpeerRoomId(params.roomId) ?? currentRoomId;
102
143
  if (params.roomId && !safeZpeerRoomId(params.roomId)) return "invalid room id";
103
144
  if (/\b(zpeer_ask|\/zpeer)\b/i.test(params.message)) return "loop guard blocked recursive ZPeer instruction";
@@ -111,8 +152,9 @@ function zpeerAskGuardBlock(state: HarnessRuntimeState, params: ZpeerAskToolPara
111
152
  if (guard.count >= ZPEER_AGENT_ASK_RATE_LIMIT_PER_MINUTE) return `rate guard blocked: max ${ZPEER_AGENT_ASK_RATE_LIMIT_PER_MINUTE} agent-initiated ZPeer asks per 60s window`;
112
153
  if (priority === "urgent" && urgentCount >= ZPEER_AGENT_URGENT_RATE_LIMIT_PER_MINUTE) return `rate guard blocked: max ${ZPEER_AGENT_URGENT_RATE_LIMIT_PER_MINUTE} urgent ZPeer asks per 60s window`;
113
154
  if (priority === "force" && forceCount >= ZPEER_AGENT_FORCE_RATE_LIMIT_PER_MINUTE) return `rate guard blocked: max ${ZPEER_AGENT_FORCE_RATE_LIMIT_PER_MINUTE} force ZPeer asks per 60s window`;
114
- if (guard.lastRoomId === roomId && guard.lastTargetAlias === targetAlias && guard.lastMessageHash === messageHash) return "loop guard blocked duplicate room/target/message in ask window";
115
- state.zobLive.zpeerAskGuard = { windowStartedMs: guard.windowStartedMs, count: guard.count + 1, urgentCount: priority === "urgent" ? urgentCount + 1 : urgentCount, forceCount: priority === "force" ? forceCount + 1 : forceCount, lastRoomId: roomId, lastTargetAlias: targetAlias, lastMessageHash: messageHash };
155
+ const targetAliasLookupKey = zpeerAliasLookupKey(targetAlias) ?? targetAlias;
156
+ if (guard.lastRoomId === roomId && zpeerAliasLookupKey(guard.lastTargetAlias) === targetAliasLookupKey && guard.lastMessageHash === messageHash) return "loop guard blocked duplicate room/target/message in ask window";
157
+ state.zobLive.zpeerAskGuard = { windowStartedMs: guard.windowStartedMs, count: guard.count + 1, urgentCount: priority === "urgent" ? urgentCount + 1 : urgentCount, forceCount: priority === "force" ? forceCount + 1 : forceCount, lastRoomId: roomId, lastTargetAlias: targetAliasLookupKey, lastMessageHash: messageHash };
116
158
  return undefined;
117
159
  }
118
160
 
@@ -266,6 +308,7 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
266
308
  requireResponse,
267
309
  responseTimeoutMs: timeoutMs,
268
310
  maxReinjects,
311
+ fallbackDelivery: buildProjectTransposerFallbackDelivery(ctx.cwd),
269
312
  onFeedback: (feedback) => {
270
313
  feedbackEmittedTerminal = feedback.result.status === "waiting" || feedback.result.status === "reply" || feedback.result.status === "completed" || feedback.result.status === "blocked" || feedback.result.status === "error" || feedback.result.status === "timeout" || feedback.result.status === "expired" || feedback.result.status === "required_response_expired";
271
314
  emitZpeerAskEvent({ kind: feedback.kind, roomId: feedback.result.roomId, status: feedback.result.status, reason: feedback.result.reason, msgId: feedback.result.msgId, taskHash: feedback.result.taskHash, outputHash: feedback.result.outputHash, interruptStatus: feedback.result.interruptStatus });
@@ -273,7 +316,7 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
273
316
  });
274
317
  if (!feedbackEmittedTerminal) emitZpeerAskEvent({ kind: zpeerTerminalKind(result.status), roomId: result.roomId, status: result.status, reason: result.reason, msgId: result.msgId, taskHash: result.taskHash, outputHash: result.outputHash, interruptStatus: result.interruptStatus });
275
318
  updatePassivePeerWaitState(state, result, { roomId: requestedRoomId, targetAlias });
276
- pi.appendEntry("zob-zpeer", { schema: "zob.zpeer-ask.v1", action: "agent_request", mode, status: result.status, priority: interrupt.priority, interruptMode: interrupt.interruptMode, interruptStatus: result.interruptStatus, reasonHash: result.reason ? sha256(result.reason) : undefined, msgId: result.msgId, targetAliasHash: result.targetAlias ? sha256(result.targetAlias) : sha256(targetAlias), roomIdHash: sha256(result.roomId ?? requestedRoomId), taskHash: result.taskHash, outputHash: result.outputHash, reasonInputHash: params.reason ? sha256(params.reason) : undefined, interruptReasonHash: interrupt.interruptReasonHash, requireResponse: requireResponse || undefined, responseRequiredBy: result.responseRequiredBy, responseTimeoutMs: result.responseTimeoutMs, maxReinjects: result.maxReinjects, responseReceived: result.responseReceived, deliveryStatus: result.deliveryStatus, localOnly: true, networkEnabled: false, bodyStored: false, promptBodiesStored: false, outputBodiesStored: false, generatedAt: new Date().toISOString() });
319
+ pi.appendEntry("zob-zpeer", { schema: "zob.zpeer-ask.v1", action: "agent_request", mode, status: result.status, priority: interrupt.priority, interruptMode: interrupt.interruptMode, interruptStatus: result.interruptStatus, reasonHash: result.reason ? sha256(result.reason) : undefined, msgId: result.msgId, targetAliasHash: result.targetAlias ? sha256(result.targetAlias) : sha256(targetAlias), roomIdHash: sha256(result.roomId ?? requestedRoomId), taskHash: result.taskHash, outputHash: result.outputHash, reasonInputHash: params.reason ? sha256(params.reason) : undefined, interruptReasonHash: interrupt.interruptReasonHash, requireResponse: requireResponse || undefined, responseRequiredBy: result.responseRequiredBy, responseTimeoutMs: result.responseTimeoutMs, maxReinjects: result.maxReinjects, responseReceived: result.responseReceived, deliveryStatus: result.deliveryStatus, deliveryMethod: result.deliveryMethod, fallbackDelivery: result.fallback_delivery, bestEffort: result.best_effort, localOnly: true, networkEnabled: false, bodyStored: false, promptBodiesStored: false, outputBodiesStored: false, generatedAt: new Date().toISOString() });
277
320
  const ok = result.status === "reply" || result.status === "completed" || result.status === "waiting" || result.status === "delivered";
278
321
  const passiveWaitSuffix = result.status === "waiting" ? " · idle/passive wait: no follow-up turn queued; stop if no other action is actionable" : "";
279
322
  const transientReplyText = (result.status === "reply" || result.status === "completed") && result.transientResponse
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zob-harness",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "type": "module",
5
5
  "description": "A governed Agent Factory for Pi: launch communicating agent teams, run tmux-backed factories, validate artifacts, and package repeatable workflows.",
6
6
  "license": "MIT",
@@ -19,6 +19,7 @@ const previousZpeerProfile = process.env.ZPEER_PROFILE;
19
19
  const previousComsSessionId = process.env.ZOB_COMS_SESSION_ID;
20
20
  const previousTmuxPane = process.env.TMUX_PANE;
21
21
  const previousZobComsRoleId = process.env.ZOB_COMS_ROLE_ID;
22
+ const previousZpeerStrict = process.env.ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET;
22
23
  const servers = [];
23
24
 
24
25
  function fail(message) {
@@ -101,6 +102,7 @@ async function main() {
101
102
  const zpeerProfile = await import(`${compiledComsV2}/zpeer-profile.js`);
102
103
  const toolsComs = await import(`${compiledSrc}/runtime/tools-coms.js`);
103
104
  const localTransport = await import(`${compiledComsV2}/local-transport.js`);
105
+ const liveRegistry = await import(`${compiledComsV2}/registry.js`);
104
106
  const envelope = await import(`${compiledComsV2}/envelope.js`);
105
107
  const pendingModule = await import(`${compiledComsV2}/pending-replies.js`);
106
108
  const hashing = await import(`${compiledSrc}/core/utils/hashing.js`);
@@ -203,6 +205,10 @@ async function main() {
203
205
  const workerTwoEndpoint = join(root, 'worker-two.sock');
204
206
  const adhocOneEndpoint = join(root, 'adhoc-one.sock');
205
207
  const adhocTwoEndpoint = join(root, 'adhoc-two.sock');
208
+ const planningLeadEndpoint = join(root, 'planning-lead.sock');
209
+ const equivalenceSenderEndpoint = join(root, 'equivalence-sender.sock');
210
+ const hyphenWorkerEndpoint = join(root, 'hyphen-worker.sock');
211
+ const underscoreWorkerEndpoint = join(root, 'underscore-worker.sock');
206
212
  const pendingReplies = new Map();
207
213
  const receivedPrompts = [];
208
214
  const receivedResponses = [];
@@ -258,6 +264,13 @@ async function main() {
258
264
  receivedPrompts.push(incoming);
259
265
  return envelope.buildZobLiveAckEnvelope(incoming);
260
266
  }));
267
+ servers.push(await localTransport.bindZobLocalEndpoint(planningLeadEndpoint, async (incoming) => {
268
+ receivedPrompts.push(incoming);
269
+ return envelope.buildZobLiveAckEnvelope(incoming);
270
+ }));
271
+ servers.push(await localTransport.bindZobLocalEndpoint(equivalenceSenderEndpoint, async (incoming) => envelope.buildZobLiveAckEnvelope(incoming)));
272
+ servers.push(await localTransport.bindZobLocalEndpoint(hyphenWorkerEndpoint, async (incoming) => envelope.buildZobLiveAckEnvelope(incoming)));
273
+ servers.push(await localTransport.bindZobLocalEndpoint(underscoreWorkerEndpoint, async (incoming) => envelope.buildZobLiveAckEnvelope(incoming)));
261
274
 
262
275
  const oldHeartbeatAt = new Date(Date.now() - 180_000).toISOString();
263
276
  let alpha = zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'alpha', roomId: 'room-one', endpoint: alphaEndpoint, endpointHash: hashing.sha256(alphaEndpoint), sha256: hashing.sha256, heartbeatAt: oldHeartbeatAt }), 'room-one', 'alpha');
@@ -273,15 +286,26 @@ async function main() {
273
286
  assert(staleSummary.online === 0, `stale room-one summary expected 0 online peers before refresh, got ${staleSummary.online}`);
274
287
  assert(staleSummary.aliases.includes('alpha') && staleSummary.aliases.includes('beta') && !staleSummary.aliases.includes('gamma'), 'stale room-one aliases must include alpha/beta only');
275
288
 
276
- alpha = zpeer.refreshZpeerSelf(repoRoot, alpha);
289
+ const alphaSocketVerifiedAtMs = Date.now();
290
+ alpha = zpeer.refreshZpeerSelf(repoRoot, alpha, undefined, undefined, undefined, { socketVerifiedAtMs: alphaSocketVerifiedAtMs });
277
291
  beta = zpeer.refreshZpeerSelf(peerRepoRoot, beta);
278
292
  workerOne = zpeer.refreshZpeerSelf(repoRoot, workerOne);
279
293
  workerTwo = zpeer.refreshZpeerSelf(repoRoot, workerTwo);
294
+ const leaseBackedAlpha = liveRegistry.readZobLiveRegistrySnapshot(repoRoot).peers.find((peer) => peer.sessionHash === alpha.sessionHash);
295
+ assert(typeof leaseBackedAlpha?.socketVerifiedAt === 'string' && Date.parse(leaseBackedAlpha.socketVerifiedAt) >= alphaSocketVerifiedAtMs, 'stable lease-backed registry snapshot must preserve peer socketVerifiedAt');
280
296
  const initialSummary = zpeer.buildZpeerRoomSummary(repoRoot, alpha);
281
297
  assert(initialSummary.peerCount === 2, `room-one summary expected 2 peers, got ${initialSummary.peerCount}`);
282
298
  assert(initialSummary.online === 2, `room-one summary expected 2 online peers after refresh, got ${initialSummary.online}`);
283
299
  assert(initialSummary.aliases.includes('alpha') && initialSummary.aliases.includes('beta') && !initialSummary.aliases.includes('gamma'), 'room-one aliases must include alpha/beta only');
284
300
 
301
+ const sweepRetainedPeer = zpeer.refreshZpeerSelf(repoRoot, zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'sweepretained', roomId: 'sweep-room', endpoint: gammaEndpoint, endpointHash: hashing.sha256(gammaEndpoint), sha256: hashing.sha256, roleId: 'sweep-lead', roleType: 'lead' }), 'sweep-room', 'sweepretained'));
302
+ const sweepBefore = liveRegistry.readZobLiveRegistrySnapshot(repoRoot).peers.find((peer) => peer.sessionHash === sweepRetainedPeer.sessionHash);
303
+ assert(sweepBefore?.status === 'online' && sweepBefore.socketVerifiedAt === undefined, 'retained-live sweep fixture must start online without socketVerifiedAt');
304
+ const sweepResult = await liveRegistry.sweepZobLivePeerHealth(repoRoot, { teamName: 'zob-core', sample: 99 });
305
+ const sweepAfter = liveRegistry.readZobLiveRegistrySnapshot(repoRoot).peers.find((peer) => peer.sessionHash === sweepRetainedPeer.sessionHash);
306
+ assert(sweepResult.retainedLive >= 1, `health sweep must count responsive already-online peers as retainedLive, got ${sweepResult.retainedLive}`);
307
+ assert(sweepAfter?.status === 'online' && typeof sweepAfter.socketVerifiedAt === 'string', 'health sweep must stamp heartbeat/status/socketVerifiedAt for responsive retained-live peers');
308
+
285
309
  const waitForReply = (msgId) => new Promise((resolve) => {
286
310
  const timer = setTimeout(() => resolve({ status: 'timeout' }), 5_000);
287
311
  pendingReplies.set(msgId, {
@@ -304,6 +328,22 @@ async function main() {
304
328
  assert(adhocAsync.status === 'waiting', `ad-hoc same-room send expected waiting after ACK, got ${adhocAsync.status}${adhocAsync.reason ? `: ${adhocAsync.reason}` : ''}`);
305
329
  assert(receivedPrompts.length === adhocPromptCountBefore + 1 && receivedPrompts.at(-1).receiver === 'adhoctwo', 'ad-hoc same-room send must deliver to the target without a stable team-agent lease');
306
330
 
331
+ const planningLead = zpeer.refreshZpeerSelf(repoRoot, { ...zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'planning_lead', roomId: 'equiv-room', endpoint: planningLeadEndpoint, endpointHash: hashing.sha256(planningLeadEndpoint), sha256: hashing.sha256, roleId: 'planning-lead', roleType: 'lead' }), 'equiv-room', 'planning_lead'), zpeerAdhoc: true });
332
+ const equivalenceSender = zpeer.refreshZpeerSelf(repoRoot, { ...zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'equivsender', roomId: 'equiv-room', endpoint: equivalenceSenderEndpoint, endpointHash: hashing.sha256(equivalenceSenderEndpoint), sha256: hashing.sha256, roleId: 'equivalence-sender', roleType: 'lead' }), 'equiv-room', 'equivsender'), zpeerAdhoc: true });
333
+ const planningPromptCountBefore = receivedPrompts.length;
334
+ const planningResult = await zpeer.sendZpeerPrompt(repoRoot, equivalenceSender, 'planning-lead', rawPrompt, waitForReply, { mode: 'async', roomId: 'equiv-room' });
335
+ assert(planningResult.status === 'waiting', `hyphen target lookup must reach underscore live alias, got ${planningResult.status}${planningResult.reason ? `: ${planningResult.reason}` : ''}`);
336
+ assert(receivedPrompts.length === planningPromptCountBefore + 1 && receivedPrompts.at(-1).receiver === 'planning_lead', 'hyphen target lookup must deliver envelope to actual stored room membership alias planning_lead');
337
+ assert(planningLead.zpeerAlias === 'planning_lead' && zpeer.peerAliasInRoom(planningLead, 'equiv-room') === 'planning_lead', 'hyphen lookup must not migrate stored/display alias planning_lead');
338
+ const planningSelfBlocked = await zpeer.sendZpeerPrompt(repoRoot, planningLead, 'planning-lead', rawPrompt, waitForReply, { mode: 'async', roomId: 'equiv-room' });
339
+ assert(planningSelfBlocked.status === 'blocked' && String(planningSelfBlocked.reason).includes('self'), 'self-send guard must treat planning-lead and planning_lead as equivalent');
340
+ zpeer.refreshZpeerSelf(repoRoot, { ...zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'worker-one', roomId: 'equiv-room', endpoint: hyphenWorkerEndpoint, endpointHash: hashing.sha256(hyphenWorkerEndpoint), sha256: hashing.sha256, roleId: 'hyphen-worker', roleType: 'lead' }), 'equiv-room', 'worker-one'), zpeerAdhoc: true });
341
+ zpeer.refreshZpeerSelf(repoRoot, { ...zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'worker_one', roomId: 'equiv-room', endpoint: underscoreWorkerEndpoint, endpointHash: hashing.sha256(underscoreWorkerEndpoint), sha256: hashing.sha256, roleId: 'underscore-worker', roleType: 'lead' }), 'equiv-room', 'worker_one'), zpeerAdhoc: true });
342
+ const ambiguousResult = await zpeer.sendZpeerPrompt(repoRoot, equivalenceSender, 'worker-one', rawPrompt, waitForReply, { mode: 'async', roomId: 'equiv-room' });
343
+ assert(ambiguousResult.status === 'blocked' && String(ambiguousResult.reason).includes('duplicate live alias'), 'worker-one vs worker_one live aliases must block as ambiguous under lookup equivalence');
344
+ const equivalenceSummary = zpeer.buildZpeerRoomSummary(repoRoot, equivalenceSender, 'equiv-room');
345
+ assert(equivalenceSummary.duplicateAliases.includes('worker-one') && equivalenceSummary.duplicateAliases.includes('worker_one'), 'room summary must report live lookup-equivalent duplicate aliases');
346
+
307
347
  const joinedAlpha = await zpeer.joinZpeerRoom(repoRoot, alpha, 'shared-room', 'sharedalpha', 'bridge');
308
348
  assert(joinedAlpha.ok === true, `alpha multi-room join expected ok, got ${joinedAlpha.reason ?? 'not ok'}`);
309
349
  alpha = joinedAlpha.peer;
@@ -534,6 +574,58 @@ async function main() {
534
574
  assert(String(workerDirectResult.reason).includes('zpeer topology blocked'), 'same-room worker-to-worker send must fall through to legacy topology block');
535
575
  assert(receivedPrompts.length === promptCountBeforeWorkerDirect, 'same-room worker-to-worker send must not deliver a prompt to workertwo');
536
576
 
577
+ let strictPeer = zpeer.refreshZpeerSelf(repoRoot, { ...zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'strictpeer', roomId: 'strict-room', endpoint: gammaEndpoint, endpointHash: hashing.sha256(gammaEndpoint), sha256: hashing.sha256, roleId: 'strict-lead', roleType: 'lead' }), 'strict-room', 'strictpeer'), zpeerAdhoc: true });
578
+ const normalStrictSummary = zpeer.buildZpeerRoomSummary(repoRoot, strictPeer, 'strict-room');
579
+ assert(normalStrictSummary.online === 1, 'without strict mode an online peer with an existing socket may remain reachable by legacy existsSync floor');
580
+ process.env.ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET = '1';
581
+ const strictUnverifiedSummary = zpeer.buildZpeerRoomSummary(repoRoot, strictPeer, 'strict-room');
582
+ assert(strictUnverifiedSummary.online === 0 && strictUnverifiedSummary.offline === 1, 'strict socket mode must require recent socketVerifiedAt evidence');
583
+ strictPeer = zpeer.refreshZpeerSelf(repoRoot, strictPeer, undefined, undefined, undefined, { socketVerifiedAtMs: Date.now() });
584
+ const strictVerifiedSummary = zpeer.buildZpeerRoomSummary(repoRoot, strictPeer, 'strict-room');
585
+ assert(strictVerifiedSummary.online === 1, 'strict socket mode must revive when socketVerifiedAt is freshly stamped');
586
+
587
+ const joinedProbeRoom = await zpeer.joinZpeerRoom(repoRoot, alpha, 'strict-probe-room', 'probealpha');
588
+ assert(joinedProbeRoom.ok === true, `strict probe room join expected ok, got ${joinedProbeRoom.reason ?? 'not ok'}`);
589
+ alpha = joinedProbeRoom.peer;
590
+ const strictProbePeer = zpeer.refreshZpeerSelf(repoRoot, zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'strictprobe', roomId: 'strict-probe-room', endpoint: gammaEndpoint, endpointHash: hashing.sha256(gammaEndpoint), sha256: hashing.sha256, roleId: 'strict-probe-lead', roleType: 'lead' }), 'strict-probe-room', 'strictprobe'));
591
+ const strictProbeSummary = zpeer.buildZpeerRoomSummary(repoRoot, alpha, 'strict-probe-room');
592
+ assert(strictProbeSummary.offline >= 1 && strictProbeSummary.online < strictProbeSummary.peerCount, 'strict mode must classify the unverified local-socket probe candidate offline before send');
593
+ const revivedFallbackInputs = [];
594
+ const revivedViaProbe = await zpeer.sendZpeerPrompt(repoRoot, alpha, 'strictprobe', rawPrompt, waitForReply, {
595
+ roomId: 'strict-probe-room',
596
+ mode: 'async',
597
+ priority: 'urgent',
598
+ interruptMode: 'steer',
599
+ fallbackDelivery: async (input) => { revivedFallbackInputs.push(input); return { delivered: true, target: 'strictprobe-pane' }; },
600
+ });
601
+ const strictProbeStamped = liveRegistry.readZobLiveRegistrySnapshot(repoRoot).peers.find((peer) => peer.sessionHash === strictProbePeer.sessionHash);
602
+ assert(revivedViaProbe.status === 'waiting' && revivedViaProbe.deliveryStatus === 'delivered' && revivedViaProbe.fallback_delivery !== true, `responsive strict-offline candidate must use local_socket, got ${revivedViaProbe.status}/${revivedViaProbe.deliveryStatus}/${revivedViaProbe.deliveryMethod}`);
603
+ assert(revivedFallbackInputs.length === 0, 'responsive strict-offline candidate must be probed/revived before tmux fallback hook is called');
604
+ assert(strictProbeStamped?.status === 'online' && typeof strictProbeStamped.socketVerifiedAt === 'string', 'responsive strict-offline probe must stamp socketVerifiedAt/online into the stable lease-backed registry');
605
+ if (previousZpeerStrict === undefined) delete process.env.ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET;
606
+ else process.env.ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET = previousZpeerStrict;
607
+
608
+ const fallbackPeer = {
609
+ ...zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'fallbackbeta', roomId: 'room-one', endpoint: join(root, 'dead-fallbackbeta.sock'), endpointHash: hashing.sha256(join(root, 'dead-fallbackbeta.sock')), sha256: hashing.sha256, roleId: 'fallback-lead', roleType: 'lead' }), 'room-one', 'fallbackbeta'),
610
+ heartbeatAt: new Date().toISOString(),
611
+ status: 'offline',
612
+ socketVerifiedAt: undefined,
613
+ };
614
+ liveRegistry.writeZobLiveTeamAgentLease(repoRoot, fallbackPeer, { reason: 'smoke_demote_fallback_peer' });
615
+ const fallbackInputs = [];
616
+ const fallbackResult = await zpeer.sendZpeerPrompt(repoRoot, alpha, 'fallbackbeta', rawPrompt, waitForReply, {
617
+ mode: 'async',
618
+ priority: 'urgent',
619
+ interruptMode: 'steer',
620
+ fallbackDelivery: async (input) => { fallbackInputs.push(input); return { delivered: true, target: 'fallbackbeta-pane' }; },
621
+ });
622
+ assert(fallbackResult.status === 'delivered' && fallbackResult.fallback_delivery === true && fallbackResult.best_effort === true && fallbackResult.deliveryMethod === 'tmux_sendkeys', `urgent fallback expected best-effort delivered/tmux_sendkeys, got ${fallbackResult.status}/${fallbackResult.deliveryMethod}`);
623
+ assert(fallbackResult.outputHash === undefined && fallbackResult.deliveryStatus === 'blocked', 'fallback must not carry outputHash and must mark local_socket deliveryStatus blocked/not verified');
624
+ assert(fallbackInputs.length === 1 && fallbackInputs[0].targetAlias === 'fallbackbeta' && fallbackInputs[0].senderAlias === 'alpha' && fallbackInputs[0].priority === 'urgent', 'fallback hook must receive room-scoped sender/target/priority metadata');
625
+ let forceFallbackCalled = false;
626
+ const forceFallbackBlocked = await zpeer.sendZpeerPrompt(repoRoot, alpha, 'fallbackbeta', rawPrompt, waitForReply, { mode: 'async', priority: 'force', interruptMode: 'abort', interruptReasonHash: forceReasonHash, fallbackDelivery: async () => { forceFallbackCalled = true; return { delivered: true }; } });
627
+ assert(forceFallbackBlocked.status === 'blocked' && forceFallbackCalled === false, 'force fallback must be blocked and must not call the fallbackDelivery hook');
628
+
537
629
  const messagesPath = join(repoRoot, '.pi', 'coms', 'peer-messages.jsonl');
538
630
  const statusesPath = join(repoRoot, '.pi', 'coms', 'peer-status.jsonl');
539
631
  const messages = readJsonl(messagesPath);
@@ -559,6 +651,7 @@ async function main() {
559
651
  assert(messages.some((record) => record.event === 'ack' && record.priority === 'force' && record.interruptMode === 'abort' && record.interruptStatus === 'force_accepted' && record.interruptReasonHash === forceReasonHash), 'peer ledger must include force accepted interrupt metadata with reason hash only');
560
652
  assert(messages.some((record) => record.event === 'attempt' && record.status === 'blocked' && record.priority === 'force' && record.interruptStatus === 'force_blocked' && record.interruptReasonHash === forceReasonHash && record.targetAliasHash === hashing.sha256('workertwo')), 'peer ledger must include force role-policy blocked metadata');
561
653
  assert(messages.some((record) => record.event === 'attempt' && record.status === 'blocked' && record.targetAliasHash === hashing.sha256('workertwo') && record.reasonHash), 'peer ledger must include hash-only blocked record for same-room worker-to-worker send');
654
+ assert(messages.some((record) => record.event === 'attempt' && record.status === 'delivered' && record.deliveryMethod === 'tmux_sendkeys' && record.fallbackDelivery === true && record.outputHash === undefined), 'peer ledger must include best-effort tmux fallback metadata without outputHash');
562
655
 
563
656
  const realRepoComs = join(process.cwd(), '.pi', 'coms');
564
657
  assert(messagesPath !== join(realRepoComs, 'peer-messages.jsonl'), 'smoke must not target real .pi/coms peer-messages ledger');
@@ -588,5 +681,7 @@ try {
588
681
  else process.env.TMUX_PANE = previousTmuxPane;
589
682
  if (previousZobComsRoleId === undefined) delete process.env.ZOB_COMS_ROLE_ID;
590
683
  else process.env.ZOB_COMS_ROLE_ID = previousZobComsRoleId;
684
+ if (previousZpeerStrict === undefined) delete process.env.ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET;
685
+ else process.env.ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET = previousZpeerStrict;
591
686
  rmSync(root, { recursive: true, force: true });
592
687
  }
@@ -35,8 +35,10 @@ const files = [
35
35
  '.pi/extensions/zob-harness/src/domains/coms/coms-v2/pending-replies.ts',
36
36
  '.pi/extensions/zob-harness/src/domains/coms/coms-v2/response-capture.ts',
37
37
  '.pi/extensions/zob-harness/src/domains/coms/coms-v2/registry.ts',
38
+ '.pi/extensions/zob-harness/src/domains/coms/coms-v2/types.ts',
38
39
  '.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer-profile.ts',
39
40
  '.pi/extensions/zob-harness/src/domains/coms/coms-v2/transcript-capture.ts',
41
+ '.pi/extensions/zob-harness/src/domains/coms/zagents.ts',
40
42
  '.pi/extensions/zob-harness/src/domains/delegation/child-runner.ts',
41
43
  '.pi/extensions/zob-harness/src/runtime/commands.ts',
42
44
  '.pi/extensions/zob-harness/src/runtime/tools-coms.ts',
@@ -59,7 +61,7 @@ const zpeerAskMatches = toolsComs.match(/name: "zpeer_ask"/g) ?? [];
59
61
  if (zpeerAskMatches.length !== 1) failures.push(`expected exactly one zpeer_ask tool, found ${zpeerAskMatches.length}`);
60
62
  const zpeerReplyMatches = toolsComs.match(/name: "zpeer_reply"/g) ?? [];
61
63
  if (zpeerReplyMatches.length !== 1) failures.push(`expected exactly one zpeer_reply tool, found ${zpeerReplyMatches.length}`);
62
- for (const needle of ['parameters: ZpeerAskParams', 'sendZpeerPrompt(ctx.cwd', 'mode = params.mode ?? "async"', 'requireResponse = params.requireResponse === true', 'pendingReplies.wait(msgId, timeoutMs, { requireResponse })', 'maxReinjects', 'zpeerAskGuardBlock', 'ZPEER_AGENT_ASK_RATE_LIMIT_PER_MINUTE = 50', 'ZPEER_AGENT_URGENT_RATE_LIMIT_PER_MINUTE = 10', 'ZPEER_AGENT_FORCE_RATE_LIMIT_PER_MINUTE = 3', 'normalizeZpeerInterrupt', 'force interrupt requires reason', 'max ${ZPEER_AGENT_ASK_RATE_LIMIT_PER_MINUTE} agent-initiated ZPeer asks per 60s window', 'idle/passive wait: no follow-up turn queued', 'customType: "zob-zpeer-event"', 'source: "agent-request"', 'action: "agent_request"', 'feedbackEmittedTerminal', 'reasonInputHash', 'interruptReasonHash', 'bodyStored: false', 'promptBodiesStored: false', 'outputBodiesStored: false']) {
64
+ for (const needle of ['parameters: ZpeerAskParams', 'sendZpeerPrompt(ctx.cwd', 'mode = params.mode ?? "async"', 'requireResponse = params.requireResponse === true', 'pendingReplies.wait(msgId, timeoutMs, { requireResponse })', 'maxReinjects', 'zpeerAskGuardBlock', 'ZPEER_AGENT_ASK_RATE_LIMIT_PER_MINUTE = 50', 'ZPEER_AGENT_URGENT_RATE_LIMIT_PER_MINUTE = 10', 'ZPEER_AGENT_FORCE_RATE_LIMIT_PER_MINUTE = 3', 'normalizeZpeerInterrupt', 'force interrupt requires reason', 'max ${ZPEER_AGENT_ASK_RATE_LIMIT_PER_MINUTE} agent-initiated ZPeer asks per 60s window', 'idle/passive wait: no follow-up turn queued', 'customType: "zob-zpeer-event"', 'source: "agent-request"', 'action: "agent_request"', 'feedbackEmittedTerminal', 'reasonInputHash', 'interruptReasonHash', 'buildProjectTransposerFallbackDelivery(ctx.cwd)', 'PROJECT_TRANSPOSER_ZPEER_FALLBACK_HOOK', 'fallbackDelivery: result.fallback_delivery', 'bestEffort: result.best_effort', 'bodyStored: false', 'promptBodiesStored: false', 'outputBodiesStored: false']) {
63
65
  if (!toolsComs.includes(needle)) failures.push(`zpeer_ask tool missing ${needle}`);
64
66
  }
65
67
  for (const needle of ['parameters: ZpeerReplyParams', 'buildZobLiveResponseEnvelope', 'sendZobLocalEnvelope(replyEndpoint', 'replyToMsgId: inbound.envelope.msgId', 'action: "reply"', 'action: "reply_blocked"', 'status: "response_sent"', 'ZPeer msgId required response already expired']) {
@@ -94,8 +96,10 @@ const envelope = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/e
94
96
  const pendingReplies = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/pending-replies.ts'];
95
97
  const responseCapture = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/response-capture.ts'];
96
98
  const liveRegistry = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/registry.ts'];
99
+ const liveTypes = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/types.ts'];
97
100
  const zpeerProfile = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer-profile.ts'];
98
- for (const needle of ['networkEnabled: false', 'localOnly: true', 'bodyStored: false', 'sendZobLocalEnvelope', 'taskHash', 'outputHash', 'ZpeerSendMode', 'status: "waiting"', 'status: "reply"', 'required_response_expired', 'requireResponse', 'responseRequiredBy', 'responseReceived', 'deliveryStatus', 'zpeerMembershipsForPeer', 'joinZpeerRoom', 'leaveZpeerRoom', 'useZpeerRoom', 'clearZpeerRoom', 'preservedSelf: true', 'roomId?: string', 'buildZpeerPeerRoomSummaries', 'active: membership.roomId === activeRoomId', 'peerRespondsToAliasPing', 'activeAliasCollision', 'zpeer-alias-ping', 'zpeerAdhoc']) {
101
+ const zagents = contents['.pi/extensions/zob-harness/src/domains/coms/zagents.ts'];
102
+ for (const needle of ['networkEnabled: false', 'localOnly: true', 'bodyStored: false', 'sendZobLocalEnvelope', 'taskHash', 'outputHash', 'ZpeerSendMode', 'status: "waiting"', 'status: "reply"', 'required_response_expired', 'requireResponse', 'responseRequiredBy', 'responseReceived', 'deliveryStatus', 'zpeerMembershipsForPeer', 'joinZpeerRoom', 'leaveZpeerRoom', 'useZpeerRoom', 'clearZpeerRoom', 'preservedSelf: true', 'roomId?: string', 'buildZpeerPeerRoomSummaries', 'active: membership.roomId === activeRoomId', 'peerRespondsToAliasPing', 'activeAliasCollision', 'zpeer-alias-ping', 'zpeerAdhoc', 'zpeerStrictSocketEvidenceRequired', 'ZOB_ZPEER_REQUIRE_VERIFIED_SOCKET', 'ZpeerFallbackDelivery', 'probeAndReviveZpeerCandidate', 'zpeer-candidate-probe', 'reason: "zpeer_candidate_probe"']) {
99
103
  if (!zpeer.includes(needle)) failures.push(`zpeer missing ${needle}`);
100
104
  }
101
105
  for (const needle of ['peer-messages.jsonl', 'peer-status.jsonl', 'appendZpeerPeerRecords', 'reasonHash', 'priority', 'interruptMode', 'interruptStatus', 'interruptReasonHash', 'bodyStored: false']) {
@@ -117,11 +121,22 @@ if (roomFirstTopologyIndex === -1) failures.push('zpeer topology must allow same
117
121
  if (!zpeer.includes('const bothPeersAreWorkers = self.roleType === "worker" && target.roleType === "worker";')) failures.push('zpeer topology must preserve worker-to-worker same-room legacy topology block');
118
122
  if (hardTeamMismatchIndex !== -1 && (roomFirstTopologyIndex === -1 || hardTeamMismatchIndex < roomFirstTopologyIndex)) failures.push('zpeer topology must not hard-block cross-team peers before same-room allowance');
119
123
  if (!zpeer.includes('const candidates = peersInRoom(repoRoot, roomId).filter') || !zpeer.includes('if (!selfMembership)') || !zpeer.includes('current peer is observer-only in room')) failures.push('zpeer same-room allowance must preserve room candidate, self membership, and observer guards');
120
- for (const needle of ['ZPEER_FORCE_ALLOWED_SENDER_ROLE_TYPES', 'ZPEER_FORCE_ALLOWED_RECEIVER_ROLE_TYPES', 'force interrupt not allowed from role type', 'force interrupt not allowed to role type', 'priority === "normal"']) {
124
+ for (const needle of ['export function zpeerAliasLookupKey', 'export function zpeerAliasesEquivalent', 'export function zpeerAliasIncluded', 'duplicateZpeerAliasLookupKeys(onlineAliases)', 'zpeerAliasesEquivalent(entry.membership.alias, alias)', 'zpeerAliasesEquivalent(entry.membership.alias, targetAlias)', 'zpeerAliasesEquivalent(targetAlias, senderAlias)', 'receiver: target.membership.alias']) {
125
+ if (!zpeer.includes(needle)) failures.push(`zpeer alias hyphen/underscore equivalence missing ${needle}`);
126
+ }
127
+ if (zpeer.includes('entry.membership.alias === targetAlias') || zpeer.includes('targetAlias === senderAlias') || zpeer.includes('entry.membership.alias !== alias')) failures.push('zpeer must not use literal-only alias matching in send/collision/self guards');
128
+ for (const needle of ['zpeerAliasIncluded(policy.allowedAliases, alias)', 'zpeerAliasesEquivalent(candidate.alias, alias)', 'zpeerAliasIncluded(base.allowedAliases, alias)', 'safeZpeerAlias(alias) !== alias']) {
129
+ if (!zagents.includes(needle)) failures.push(`zagent alias policy equivalence/validation missing ${needle}`);
130
+ }
131
+ for (const needle of ['ZPEER_FORCE_ALLOWED_SENDER_ROLE_TYPES', 'ZPEER_FORCE_ALLOWED_RECEIVER_ROLE_TYPES', 'force interrupt not allowed from role type', 'force interrupt not allowed to role type', 'priority !== "force"']) {
121
132
  if (!zpeer.includes(needle)) failures.push(`zpeer force sender policy/fallback guard missing ${needle}`);
122
133
  }
123
134
  if (!liveRegistry.includes('readZobLiveRegistryAllProjectsSnapshot') || !liveRegistry.includes('join(projectsDir, entry.name, "agents")')) failures.push('live registry must expose all-project agents room discovery helper');
124
135
  if (!liveRegistry.includes('peer.zpeerAdhoc !== true') || !liveRegistry.includes('mergeLeaseBackedAndAdhocPeers(leaseDirs.flatMap')) failures.push('live registry must merge explicit ad-hoc room peer cards into lease-backed summaries');
136
+ if ((liveTypes.match(/socketVerifiedAt\?: string;/g) ?? []).length < 2) failures.push('live peer card and stable team-agent lease types must expose optional socketVerifiedAt');
137
+ for (const needle of ['socketVerifiedAt: peer.socketVerifiedAt', 'socketVerifiedAt: lease.socketVerifiedAt', 'const livePeer = { ...peer, heartbeatAt: nowIso, status: "online", socketVerifiedAt: nowIso }', 'applySweepPeerUpdate(repoRoot, livePeer, nowMs)', 'retainedLive += 1']) {
138
+ if (!liveRegistry.includes(needle)) failures.push(`live registry socket verification propagation/sweep stamping missing ${needle}`);
139
+ }
125
140
  for (const needle of ['ZobLiveTeamAgentLease', 'zob.live-team-agent-lease.v1', 'stableLease: true', 'exclusiveBy: "teamId+agentId"', 'claimZobLiveTeamAgentLease', 'leaseRespondsToPing', 'pingZobLocalEndpoint', 'releaseZobLiveTeamAgentLease', 'ownsZobLiveTeamAgentLease', 'sameLeaseOwner', 'retireInactiveZobLiveTeamAgentLeases', 'readLeasesFromDir', 'hasLeaseDomain', 'mergeLeaseBackedAndAdhocPeers']) {
126
141
  if (!liveRegistry.includes(needle)) failures.push(`live registry stable lease support missing ${needle}`);
127
142
  }
@@ -189,6 +204,8 @@ for (const forbidden of ['transientPrompt:', 'transientResponse:', 'prompt:', 'r
189
204
  if (znewPreDispatchBlock.includes(forbidden)) failures.push(`/new pre-dispatch hook contains forbidden raw/body-like key ${forbidden}`);
190
205
  if (znewShutdownBlock.includes(forbidden)) failures.push(`/new session_shutdown hook contains forbidden raw/body-like key ${forbidden}`);
191
206
  }
207
+ if (!toolsComs.includes('zpeerAliasesEquivalent(targetAlias, selfAlias)') || !toolsComs.includes('zpeerAliasLookupKey(guard.lastTargetAlias)')) failures.push('zpeer_ask guard must apply alias lookup equivalence to self and duplicate loop checks');
208
+ if (!events.includes('zpeerAliasesEquivalent(envelope.sender, wait.targetAlias)')) failures.push('passive ZPeer wait clearing must apply alias lookup equivalence');
192
209
  if (!events.includes('readZpeerLocalProfile(repoRoot, profileId)')) failures.push('runtime must load session-scoped zpeer profile before ensuring/registering fields');
193
210
  if (!events.includes('sharedZpeerProfile ? undefined : zpeerProfile?.alias') || !events.includes('sharedZpeerProfile ? undefined : zpeerProfile?.memberships')) failures.push('runtime must not restore alias/memberships from shared role fallback zpeer profiles');
194
211
  if ((events.match(/writeZpeerLocalProfileFromPeer\(repoRoot, state\.zobLive\.peerCard, profileId\)/g) ?? []).length < 3 || !events.includes('zpeerRuntimeProfileId(ctx)')) failures.push('runtime must persist zpeer self profile under the current Pi session during initial registration, refresh, and shutdown');
@@ -253,7 +270,7 @@ for (const forbidden of ['transientPrompt:', 'transientResponse:', 'message:', '
253
270
  const passiveWaitBlocks = [...toolsComs.matchAll(/state\.zobLive\.passivePeerWait = \{[\s\S]*?\n \};/g)].map((m) => m[0]);
254
271
  if (passiveWaitBlocks.some((block) => block.includes(forbidden))) failures.push(`passivePeerWait block contains forbidden body-like key ${forbidden}`);
255
272
  }
256
- for (const needle of ['clearPassivePeerWaitForResponse', 'envelope.type !== "response"', 'envelope.msgId === wait.msgId', 'state.zobLive.passivePeerWait = undefined', 'envelope.sender === wait.targetAlias']) {
273
+ for (const needle of ['clearPassivePeerWaitForResponse', 'envelope.type !== "response"', 'envelope.msgId === wait.msgId', 'state.zobLive.passivePeerWait = undefined', 'zpeerAliasesEquivalent(envelope.sender, wait.targetAlias)']) {
257
274
  if (!events.includes(needle)) failures.push(`runtime events missing passive wait response clear ${needle}`);
258
275
  }
259
276
  if (!contents['.pi/extensions/zob-harness/src/domains/coms/mission-control.ts'].includes('zpeerRooms')) failures.push('Mission Control missing zpeerRooms summary');