zob-harness 0.9.1 → 0.9.2

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.
@@ -97,6 +97,21 @@ function readLeasesFromDir(dir: string, nowMs: number, teamName?: string): ZobLi
97
97
  .map((lease) => leaseToPeerCard(lease, nowMs));
98
98
  }
99
99
 
100
+ function peerRegistryKey(peer: Pick<ZobLivePeerCard, "projectId" | "roleId" | "sessionHash">): string {
101
+ return `${peer.projectId}:${peer.roleId}:${peer.sessionHash}`;
102
+ }
103
+
104
+ function mergeLeaseBackedAndAdhocPeers(leasePeers: ZobLivePeerCard[], agentPeers: ZobLivePeerCard[]): ZobLivePeerCard[] {
105
+ const byKey = new Map<string, ZobLivePeerCard>();
106
+ for (const peer of leasePeers) byKey.set(peerRegistryKey(peer), peer);
107
+ for (const peer of agentPeers) {
108
+ if (peer.zpeerAdhoc !== true) continue;
109
+ const key = peerRegistryKey(peer);
110
+ if (!byKey.has(key)) byKey.set(key, peer);
111
+ }
112
+ return [...byKey.values()];
113
+ }
114
+
100
115
  function boundedOfflinePeerRetentionMs(value: number | undefined): number {
101
116
  const env = Number.parseInt(process.env.ZOB_COMS_OFFLINE_PEER_RETENTION_MS ?? "", 10);
102
117
  const raw = typeof value === "number" && Number.isFinite(value) ? value : Number.isFinite(env) ? env : DEFAULT_OFFLINE_PEER_RETENTION_MS;
@@ -514,8 +529,13 @@ export function unregisterCurrentZobLivePeer(repoRoot: string, teamName = "zob-c
514
529
  export function readZobLiveRegistrySnapshot(repoRoot: string, teamName?: string): ZobLiveRegistrySnapshot {
515
530
  const { dir, projectId, kind } = projectLeasesDir(repoRoot);
516
531
  const nowMs = Date.now();
517
- if (existsSync(dir)) return buildSnapshot(projectId, kind, readLeasesFromDir(dir, nowMs, teamName), teamName);
518
532
  const agents = projectAgentsDir(repoRoot);
533
+ if (existsSync(dir)) {
534
+ return buildSnapshot(projectId, kind, mergeLeaseBackedAndAdhocPeers(
535
+ readLeasesFromDir(dir, nowMs, teamName),
536
+ readPeerCardsFromAgentsDir(agents.dir, nowMs, teamName),
537
+ ), teamName);
538
+ }
519
539
  return buildSnapshot(agents.projectId, agents.kind, readPeerCardsFromAgentsDir(agents.dir, nowMs, teamName), teamName);
520
540
  }
521
541
 
@@ -523,9 +543,10 @@ export function readZobLiveRegistryAllProjectsSnapshot(repoRoot: string, teamNam
523
543
  const { projectId, kind } = projectAgentsDir(repoRoot);
524
544
  const nowMs = Date.now();
525
545
  const leaseDirs = allProjectLeasesDirs();
546
+ const agentPeers = allProjectAgentsDirs().flatMap((dir) => readPeerCardsFromAgentsDir(dir, nowMs, teamName));
526
547
  const hasLeaseDomain = leaseDirs.some((dir) => existsSync(dir));
527
548
  const peers = hasLeaseDomain
528
- ? leaseDirs.flatMap((dir) => readLeasesFromDir(dir, nowMs, teamName))
529
- : allProjectAgentsDirs().flatMap((dir) => readPeerCardsFromAgentsDir(dir, nowMs, teamName));
549
+ ? mergeLeaseBackedAndAdhocPeers(leaseDirs.flatMap((dir) => readLeasesFromDir(dir, nowMs, teamName)), agentPeers)
550
+ : agentPeers;
530
551
  return buildSnapshot(projectId, kind, peers, teamName);
531
552
  }
@@ -109,6 +109,7 @@ export interface ZobLivePeerCard {
109
109
  zpeerActiveRoomId?: string;
110
110
  zpeerMemberships?: ZpeerRoomMembership[];
111
111
  zpeerLocalOnly?: true;
112
+ zpeerAdhoc?: true;
112
113
  staleAfterMs: number;
113
114
  offlineAfterMs: number;
114
115
  bodyStored: false;
@@ -234,7 +234,7 @@ export function refreshZpeerSelf(repoRoot: string, peer: ZobLivePeerCard, roomId
234
234
  const ensured = ensureZpeerFields(repoRoot, peer, roomId, alias, restoredMemberships);
235
235
  if (!hasLocalSocketEndpointEvidence(ensured)) return ensured;
236
236
  const refreshed = writeZobLivePeerCard(repoRoot, { ...ensured, heartbeatAt: new Date().toISOString(), status: "online" });
237
- writeZobLiveTeamAgentLease(repoRoot, refreshed, { reason: "zpeer_refresh" });
237
+ if (refreshed.zpeerAdhoc !== true) writeZobLiveTeamAgentLease(repoRoot, refreshed, { reason: "zpeer_refresh" });
238
238
  return refreshed;
239
239
  }
240
240
 
@@ -479,8 +479,10 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
479
479
  };
480
480
 
481
481
  if (!selfMembership) return finish("attempt", { status: "blocked", reason: `current peer is not a member of room '${roomId}'`, targetAlias: targetAlias ?? undefined, taskHash, bodyStored: false });
482
- const leaseOwnership = ownsZobLiveTeamAgentLease(repoRoot, self);
483
- if (!leaseOwnership.owned) return finish("attempt", { status: "blocked", reason: `current peer does not own stable team-agent lease (${leaseOwnership.reason})`, targetAlias: targetAlias ?? undefined, taskHash, bodyStored: false });
482
+ if (self.zpeerAdhoc !== true) {
483
+ const leaseOwnership = ownsZobLiveTeamAgentLease(repoRoot, self);
484
+ if (!leaseOwnership.owned) return finish("attempt", { status: "blocked", reason: `current peer does not own stable team-agent lease (${leaseOwnership.reason})`, targetAlias: targetAlias ?? undefined, taskHash, bodyStored: false });
485
+ }
484
486
  if (selfMembership.role === "observer") return finish("attempt", { status: "blocked", reason: `current peer is observer-only in room '${roomId}'`, targetAlias: targetAlias ?? undefined, taskHash, bodyStored: false });
485
487
  if (!targetAlias) return finish("attempt", { status: "blocked", reason: "invalid target alias", bodyStored: false });
486
488
  if (!transientPrompt.trim()) return finish("attempt", { status: "blocked", reason: "empty peer prompt", targetAlias, bodyStored: false });
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync } from "node:fs";
3
3
  import { mkdtemp, rm } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
6
+ import { getAgentDir, type ExtensionContext } from "@earendil-works/pi-coding-agent";
7
7
 
8
8
  import { discoverAgents } from "./agents.js";
9
9
  import { SUPERVISED_READONLY_CHILD_TOOLS } from "../../core/constants.js";
@@ -24,6 +24,28 @@ function getPiInvocation(args: string[]): { command: string; args: string[] } {
24
24
  return { command: "pi", args };
25
25
  }
26
26
 
27
+ function childModelPattern(ctx: ExtensionContext, agent: HarnessAgent, modelOverride: string | undefined): string | undefined {
28
+ if (modelOverride?.trim()) return modelOverride.trim();
29
+ if (agent.model?.trim()) return agent.model.trim();
30
+ const model = ctx.model;
31
+ if (!model?.provider || !model.id) return undefined;
32
+ return `${model.provider}/${model.id}`;
33
+ }
34
+
35
+ function providerFromModelPattern(model: string | undefined): string | undefined {
36
+ if (!model) return undefined;
37
+ const [provider] = model.split("/");
38
+ return provider && provider !== model ? provider : undefined;
39
+ }
40
+
41
+ function resolveCodexFastModeExtension(ctx: ExtensionContext, model: string | undefined): string | undefined {
42
+ const provider = ctx.model?.provider ?? providerFromModelPattern(model);
43
+ const usesCodexProvider = provider === "openai-codex" || provider === "codex-auto" || provider?.startsWith("codex-") === true;
44
+ if (!usesCodexProvider) return undefined;
45
+ const extensionPath = join(getAgentDir(), "extensions", "codex-fast-mode.ts");
46
+ return existsSync(extensionPath) ? extensionPath : undefined;
47
+ }
48
+
27
49
  const CHILD_THINKING_LEVELS = new Set<string>(["low", "medium", "high", "xhigh"]);
28
50
 
29
51
  function validateChildThinkingOverride(thinking: string | undefined, fieldName = "thinking"): string[] {
@@ -90,13 +112,14 @@ async function runChildAgent(
90
112
  pathPolicy?: { allowedPaths?: string[]; forbiddenPaths?: string[]; sandboxRoot?: string },
91
113
  thinkingOverride?: ChildThinkingLevel,
92
114
  ): Promise<ChildResult> {
115
+ const resolvedModel = childModelPattern(ctx, agent, modelOverride);
93
116
  const result: ChildResult = {
94
117
  agent: agent.name,
95
118
  task,
96
119
  exitCode: 0,
97
120
  output: "",
98
121
  stderr: "",
99
- model: modelOverride ?? agent.model,
122
+ model: resolvedModel,
100
123
  usage: usageEmpty(),
101
124
  };
102
125
 
@@ -123,10 +146,12 @@ async function runChildAgent(
123
146
 
124
147
  try {
125
148
  const childSafetyExtension = join(ctx.cwd, ".pi", "extensions", "zob-child-safety", "index.ts");
149
+ const childCodexFastModeExtension = resolveCodexFastModeExtension(ctx, resolvedModel);
126
150
  const args = ["--mode", "json", "-p", "--no-extensions"];
151
+ if (childCodexFastModeExtension) args.push("-e", childCodexFastModeExtension);
127
152
  if (existsSync(childSafetyExtension)) args.push("-e", childSafetyExtension);
128
153
  args.push("--session", sessionPath);
129
- const model = modelOverride ?? agent.model;
154
+ const model = resolvedModel;
130
155
  if (model) args.push("--model", model);
131
156
  const thinking = resolveChildThinking(agent, thinkingOverride);
132
157
  if (thinking) args.push("--thinking", thinking);
@@ -132,6 +132,19 @@ function activeZagentState(state: HarnessRuntimeState): ActiveZagentState | unde
132
132
  return state.zagent.id ? state.zagent as ActiveZagentState : undefined;
133
133
  }
134
134
 
135
+ function zpeerStableTeamAgentLeaseRequired(zagent: ActiveZagentState | undefined): boolean {
136
+ return Boolean(
137
+ zagent?.id
138
+ || process.env.ZOB_ZAGENT_ID?.trim()
139
+ || process.env.ZOB_ZTEAM_ID?.trim()
140
+ || process.env.ZOB_COMS_ROLE_ID?.trim(),
141
+ );
142
+ }
143
+
144
+ function withZpeerLeaseMode<T extends NonNullable<HarnessRuntimeState["zobLive"]["peerCard"]>>(peer: T, useStableTeamAgentLease: boolean): T {
145
+ return { ...peer, zpeerAdhoc: useStableTeamAgentLease ? undefined : true } as T;
146
+ }
147
+
135
148
  function formatScopedZteamModeLabel(scopedMode: ActiveZagentState["scopedMode"]): string | undefined {
136
149
  if (!scopedMode?.active || !scopedMode.teamId || !scopedMode.modeId || !scopedMode.baseMode) return undefined;
137
150
  return `zob:${scopedMode.baseMode}@${scopedMode.teamId}/${scopedMode.modeId}`;
@@ -511,6 +524,7 @@ async function startOrRefreshZobLiveRuntime(pi: ExtensionAPI, state: HarnessRunt
511
524
  if (carryoverProfile?.zagentId && state.zagent.id !== carryoverProfile.zagentId) loadActiveZagentById(state, repoRoot, carryoverProfile.zagentId);
512
525
  const sharedZpeerProfile = zpeerProfile ? zpeerProfileIdIsSharedFallback(zpeerProfile.profileId) : false;
513
526
  const zagent = activeZagentState(state);
527
+ const useStableTeamAgentLease = zpeerStableTeamAgentLeaseRequired(zagent);
514
528
  const zagentMemberships = zagent ? zagentRoomMemberships(zagent) : undefined;
515
529
  const zagentActiveRoomId = zagent?.activeRoom ?? zagentMemberships?.find((membership) => membership.roomId)?.roomId;
516
530
  const zpeerProfileRoomId = zagentActiveRoomId ?? zpeerProfile?.activeRoomId ?? zpeerProfile?.roomId ?? carryoverProfile?.activeRoomId ?? carryoverProfile?.roomId;
@@ -579,7 +593,7 @@ async function startOrRefreshZobLiveRuntime(pi: ExtensionAPI, state: HarnessRunt
579
593
  }, { triggerTurn: true, deliverAs: "followUp" });
580
594
  return buildZobLiveAckEnvelope(envelope);
581
595
  });
582
- const peerCard = ensureZpeerFields(repoRoot, {
596
+ const peerCard = withZpeerLeaseMode(ensureZpeerFields(repoRoot, {
583
597
  ...basePeer,
584
598
  team: zagent?.team ?? basePeer.team,
585
599
  roleId: zagent?.id ?? basePeer.roleId,
@@ -588,58 +602,78 @@ async function startOrRefreshZobLiveRuntime(pi: ExtensionAPI, state: HarnessRunt
588
602
  endpoint,
589
603
  endpointHash: sha256(endpoint),
590
604
  status: "online" as const,
591
- }, zpeerProfileRoomId, zpeerProfileAlias, zpeerProfileMemberships);
605
+ }, zpeerProfileRoomId, zpeerProfileAlias, zpeerProfileMemberships), useStableTeamAgentLease);
592
606
  state.zobLive.server = server;
593
- const leaseClaim = await claimZobLiveTeamAgentLease(repoRoot, peerCard, { reason: "runtime_start" });
594
- if (leaseClaim.ok) {
607
+ if (!useStableTeamAgentLease) {
608
+ releaseZobLiveTeamAgentLease(repoRoot, peerCard, { reason: "runtime_adhoc" });
595
609
  state.zobLive.peerCard = refreshZpeerSelf(repoRoot, peerCard);
596
- state.zobLive.leaseOwned = true;
597
- state.zobLive.leaseStatus = "owned";
610
+ state.zobLive.leaseOwned = false;
611
+ state.zobLive.leaseStatus = "unavailable";
598
612
  state.zobLive.leaseBlockReason = undefined;
599
613
  state.zobLive.lastHeartbeatMs = Date.now();
600
614
  scheduleZpeerHeartbeat(pi, state, ctx);
601
615
  } else {
602
- state.zobLive.peerCard = await stopLeaseBlockedLocalEndpoint(state, repoRoot, peerCard, server);
603
- setZpeerLastEvent(state, {
604
- kind: "blocked",
605
- roomId: state.zobLive.peerCard.zpeerActiveRoomId ?? state.zobLive.peerCard.zpeerRoomId,
606
- fromAlias: state.zobLive.peerCard.zpeerAlias,
607
- status: "lease_blocked",
608
- reason: "stable team-agent lease is held by a responsive live endpoint; duplicate local endpoint stopped and peer marked offline",
609
- });
616
+ const leaseClaim = await claimZobLiveTeamAgentLease(repoRoot, peerCard, { reason: "runtime_start" });
617
+ if (leaseClaim.ok) {
618
+ state.zobLive.peerCard = refreshZpeerSelf(repoRoot, peerCard);
619
+ state.zobLive.leaseOwned = true;
620
+ state.zobLive.leaseStatus = "owned";
621
+ state.zobLive.leaseBlockReason = undefined;
622
+ state.zobLive.lastHeartbeatMs = Date.now();
623
+ scheduleZpeerHeartbeat(pi, state, ctx);
624
+ } else {
625
+ state.zobLive.peerCard = await stopLeaseBlockedLocalEndpoint(state, repoRoot, peerCard, server);
626
+ setZpeerLastEvent(state, {
627
+ kind: "blocked",
628
+ roomId: state.zobLive.peerCard.zpeerActiveRoomId ?? state.zobLive.peerCard.zpeerRoomId,
629
+ fromAlias: state.zobLive.peerCard.zpeerAlias,
630
+ status: "lease_blocked",
631
+ reason: "stable team-agent lease is held by a responsive live endpoint; duplicate local endpoint stopped and peer marked offline",
632
+ });
633
+ }
610
634
  }
611
635
  try { writeZpeerLocalProfileFromPeer(repoRoot, state.zobLive.peerCard, profileId); } catch { /* best-effort reload continuity; live runtime must remain available */ }
612
636
  } else {
613
- const peerCard = ensureZpeerFields(repoRoot, {
637
+ const peerCard = withZpeerLeaseMode(ensureZpeerFields(repoRoot, {
614
638
  ...state.zobLive.peerCard,
615
639
  team: zagent?.team ?? state.zobLive.peerCard.team,
616
640
  roleId: zagent?.id ?? state.zobLive.peerCard.roleId,
617
641
  agent: zagent?.id ?? state.zobLive.peerCard.agent,
618
642
  heartbeatAt: new Date().toISOString(),
619
643
  status: "online",
620
- }, zpeerProfileRoomId, zpeerProfileAlias, zpeerProfileMemberships);
621
- const leaseClaim = await claimZobLiveTeamAgentLease(repoRoot, peerCard, { reason: "runtime_refresh" });
622
- if (leaseClaim.ok) {
644
+ }, zpeerProfileRoomId, zpeerProfileAlias, zpeerProfileMemberships), useStableTeamAgentLease);
645
+ if (!useStableTeamAgentLease) {
646
+ releaseZobLiveTeamAgentLease(repoRoot, peerCard, { reason: "runtime_adhoc" });
623
647
  state.zobLive.peerCard = refreshZpeerSelf(repoRoot, peerCard);
624
- state.zobLive.leaseOwned = true;
625
- state.zobLive.leaseStatus = "owned";
648
+ state.zobLive.leaseOwned = false;
649
+ state.zobLive.leaseStatus = "unavailable";
626
650
  state.zobLive.leaseBlockReason = undefined;
627
651
  state.zobLive.lastHeartbeatMs = Date.now();
628
652
  scheduleZpeerHeartbeat(pi, state, ctx);
629
- } else if (state.zobLive.server) {
630
- state.zobLive.peerCard = await stopLeaseBlockedLocalEndpoint(state, repoRoot, peerCard, state.zobLive.server);
631
- setZpeerLastEvent(state, {
632
- kind: "blocked",
633
- roomId: state.zobLive.peerCard.zpeerActiveRoomId ?? state.zobLive.peerCard.zpeerRoomId,
634
- fromAlias: state.zobLive.peerCard.zpeerAlias,
635
- status: "lease_blocked",
636
- reason: "stable team-agent lease is held by a responsive live endpoint; duplicate local endpoint stopped and peer marked offline",
637
- });
638
653
  } else {
639
- state.zobLive.peerCard = writeZobLivePeerCard(repoRoot, { ...peerCard, heartbeatAt: new Date().toISOString(), status: "offline" });
640
- state.zobLive.leaseOwned = false;
641
- state.zobLive.leaseStatus = "blocked";
642
- state.zobLive.leaseBlockReason = "blocked_live_owner";
654
+ const leaseClaim = await claimZobLiveTeamAgentLease(repoRoot, peerCard, { reason: "runtime_refresh" });
655
+ if (leaseClaim.ok) {
656
+ state.zobLive.peerCard = refreshZpeerSelf(repoRoot, peerCard);
657
+ state.zobLive.leaseOwned = true;
658
+ state.zobLive.leaseStatus = "owned";
659
+ state.zobLive.leaseBlockReason = undefined;
660
+ state.zobLive.lastHeartbeatMs = Date.now();
661
+ scheduleZpeerHeartbeat(pi, state, ctx);
662
+ } else if (state.zobLive.server) {
663
+ state.zobLive.peerCard = await stopLeaseBlockedLocalEndpoint(state, repoRoot, peerCard, state.zobLive.server);
664
+ setZpeerLastEvent(state, {
665
+ kind: "blocked",
666
+ roomId: state.zobLive.peerCard.zpeerActiveRoomId ?? state.zobLive.peerCard.zpeerRoomId,
667
+ fromAlias: state.zobLive.peerCard.zpeerAlias,
668
+ status: "lease_blocked",
669
+ reason: "stable team-agent lease is held by a responsive live endpoint; duplicate local endpoint stopped and peer marked offline",
670
+ });
671
+ } else {
672
+ state.zobLive.peerCard = writeZobLivePeerCard(repoRoot, { ...peerCard, heartbeatAt: new Date().toISOString(), status: "offline" });
673
+ state.zobLive.leaseOwned = false;
674
+ state.zobLive.leaseStatus = "blocked";
675
+ state.zobLive.leaseBlockReason = "blocked_live_owner";
676
+ }
643
677
  }
644
678
  try { writeZpeerLocalProfileFromPeer(repoRoot, state.zobLive.peerCard, profileId); } catch { /* best-effort reload continuity; live runtime must remain available */ }
645
679
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zob-harness",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
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",
@@ -200,6 +200,8 @@ async function main() {
200
200
  const gammaEndpoint = join(root, 'gamma.sock');
201
201
  const workerOneEndpoint = join(root, 'worker-one.sock');
202
202
  const workerTwoEndpoint = join(root, 'worker-two.sock');
203
+ const adhocOneEndpoint = join(root, 'adhoc-one.sock');
204
+ const adhocTwoEndpoint = join(root, 'adhoc-two.sock');
203
205
  const pendingReplies = new Map();
204
206
  const receivedPrompts = [];
205
207
  const receivedResponses = [];
@@ -244,6 +246,11 @@ async function main() {
244
246
  receivedPrompts.push(incoming);
245
247
  return envelope.buildZobLiveAckEnvelope(incoming);
246
248
  }));
249
+ servers.push(await localTransport.bindZobLocalEndpoint(adhocOneEndpoint, async (incoming) => envelope.buildZobLiveAckEnvelope(incoming)));
250
+ servers.push(await localTransport.bindZobLocalEndpoint(adhocTwoEndpoint, async (incoming) => {
251
+ receivedPrompts.push(incoming);
252
+ return envelope.buildZobLiveAckEnvelope(incoming);
253
+ }));
247
254
 
248
255
  const oldHeartbeatAt = new Date(Date.now() - 180_000).toISOString();
249
256
  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');
@@ -279,6 +286,17 @@ async function main() {
279
286
  });
280
287
  });
281
288
 
289
+ const adhocOne = zpeer.refreshZpeerSelf(repoRoot, { ...zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'adhocone', roomId: 'adhoc-room', endpoint: adhocOneEndpoint, endpointHash: hashing.sha256(adhocOneEndpoint), sha256: hashing.sha256 }), 'adhoc-room', 'adhocone'), zpeerAdhoc: true });
290
+ const adhocTwo = zpeer.refreshZpeerSelf(repoRoot, { ...zpeer.ensureZpeerFields(repoRoot, makePeer({ alias: 'adhoctwo', roomId: 'adhoc-room', endpoint: adhocTwoEndpoint, endpointHash: hashing.sha256(adhocTwoEndpoint), sha256: hashing.sha256 }), 'adhoc-room', 'adhoctwo'), zpeerAdhoc: true });
291
+ assert(adhocOne.zpeerAdhoc === true && adhocTwo.zpeerAdhoc === true, 'direct/ad-hoc zpeer peers must carry the ad-hoc marker');
292
+ const adhocSummary = zpeer.buildZpeerRoomSummary(repoRoot, adhocOne, 'adhoc-room');
293
+ assert(adhocSummary.peerCount === 2 && adhocSummary.online === 2, `ad-hoc room summary expected 2 online peers even with lease domain present, got ${adhocSummary.online}/${adhocSummary.peerCount}`);
294
+ assert(adhocSummary.aliases.includes('adhocone') && adhocSummary.aliases.includes('adhoctwo'), 'ad-hoc room summary must include both direct peers');
295
+ const adhocPromptCountBefore = receivedPrompts.length;
296
+ const adhocAsync = await zpeer.sendZpeerPrompt(repoRoot, adhocOne, 'adhoctwo', rawPrompt, waitForReply, { mode: 'async' });
297
+ assert(adhocAsync.status === 'waiting', `ad-hoc same-room send expected waiting after ACK, got ${adhocAsync.status}${adhocAsync.reason ? `: ${adhocAsync.reason}` : ''}`);
298
+ 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');
299
+
282
300
  const joinedAlpha = await zpeer.joinZpeerRoom(repoRoot, alpha, 'shared-room', 'sharedalpha', 'bridge');
283
301
  assert(joinedAlpha.ok === true, `alpha multi-room join expected ok, got ${joinedAlpha.reason ?? 'not ok'}`);
284
302
  alpha = joinedAlpha.peer;
@@ -34,6 +34,7 @@ const files = [
34
34
  '.pi/extensions/zob-harness/src/domains/coms/coms-v2/registry.ts',
35
35
  '.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer-profile.ts',
36
36
  '.pi/extensions/zob-harness/src/domains/coms/coms-v2/transcript-capture.ts',
37
+ '.pi/extensions/zob-harness/src/domains/delegation/child-runner.ts',
37
38
  '.pi/extensions/zob-harness/src/runtime/commands.ts',
38
39
  '.pi/extensions/zob-harness/src/runtime/tools-coms.ts',
39
40
  '.pi/extensions/zob-harness/src/runtime/events.ts',
@@ -82,7 +83,7 @@ for (const forbidden of ['transientPrompt:', 'transientResponse:', 'prompt:', 'o
82
83
  const zpeer = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer.ts'];
83
84
  const liveRegistry = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/registry.ts'];
84
85
  const zpeerProfile = contents['.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer-profile.ts'];
85
- for (const needle of ['networkEnabled: false', 'localOnly: true', 'bodyStored: false', 'sendZobLocalEnvelope', 'taskHash', 'outputHash', 'ZpeerSendMode', 'status: "waiting"', 'status: "reply"', 'zpeerMembershipsForPeer', 'joinZpeerRoom', 'leaveZpeerRoom', 'useZpeerRoom', 'clearZpeerRoom', 'preservedSelf: true', 'roomId?: string', 'buildZpeerPeerRoomSummaries', 'active: membership.roomId === activeRoomId', 'peerRespondsToAliasPing', 'activeAliasCollision', 'zpeer-alias-ping']) {
86
+ for (const needle of ['networkEnabled: false', 'localOnly: true', 'bodyStored: false', 'sendZobLocalEnvelope', 'taskHash', 'outputHash', 'ZpeerSendMode', 'status: "waiting"', 'status: "reply"', 'zpeerMembershipsForPeer', 'joinZpeerRoom', 'leaveZpeerRoom', 'useZpeerRoom', 'clearZpeerRoom', 'preservedSelf: true', 'roomId?: string', 'buildZpeerPeerRoomSummaries', 'active: membership.roomId === activeRoomId', 'peerRespondsToAliasPing', 'activeAliasCollision', 'zpeer-alias-ping', 'zpeerAdhoc']) {
86
87
  if (!zpeer.includes(needle)) failures.push(`zpeer missing ${needle}`);
87
88
  }
88
89
  for (const needle of ['peer-messages.jsonl', 'peer-status.jsonl', 'appendZpeerPeerRecords', 'reasonHash', 'bodyStored: false']) {
@@ -96,7 +97,8 @@ if (!zpeer.includes('const bothPeersAreWorkers = self.roleType === "worker" && t
96
97
  if (hardTeamMismatchIndex !== -1 && (roomFirstTopologyIndex === -1 || hardTeamMismatchIndex < roomFirstTopologyIndex)) failures.push('zpeer topology must not hard-block cross-team peers before same-room allowance');
97
98
  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');
98
99
  if (!liveRegistry.includes('readZobLiveRegistryAllProjectsSnapshot') || !liveRegistry.includes('join(projectsDir, entry.name, "agents")')) failures.push('live registry must expose all-project agents room discovery helper');
99
- 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']) {
100
+ 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');
101
+ 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']) {
100
102
  if (!liveRegistry.includes(needle)) failures.push(`live registry stable lease support missing ${needle}`);
101
103
  }
102
104
  if (!zpeer.includes('readZobLiveRegistryAllProjectsSnapshot(repoRoot)')) failures.push('zpeer room discovery must use all-project registry snapshots');
@@ -172,7 +174,7 @@ for (const needle of ['readZpeerNewCarryoverProfile(repoRoot)', 'const carryover
172
174
  }
173
175
  if (!events.includes('event.source === "extension" && !event.text.trim()') || !events.includes('action: "handled" as const')) failures.push('runtime must handle empty extension follow-ups without continuing the agent');
174
176
  if (!events.includes('ZPeer async reply received from @') || !events.includes('{ triggerTurn: true, deliverAs: "followUp" }')) failures.push('runtime must resume an idle agent with a follow-up when an async ZPeer reply arrives');
175
- for (const needle of ['ZPEER AWARENESS (transient, rebuilt each turn)', 'buildZpeerPeerRoomSummaries(repoRoot, state.zobLive.peerCard)', '- rooms:', 'explicit roomId', 'zpeer_ask with mode=\\"async\\"', 'Passive wait rule', 'stop the turn and remain idle', 'avoid spam', 'Raw ZPeer bodies are transient', 'registerMessageRenderer("zob-zpeer-event"', 'scheduleZpeerHeartbeat', 'clearZpeerHeartbeatTimer', 'refreshZpeerSelf(repoRoot', 'kind: "response_sent"', 'kind: "inbound"']) {
177
+ for (const needle of ['ZPEER AWARENESS (transient, rebuilt each turn)', 'buildZpeerPeerRoomSummaries(repoRoot, state.zobLive.peerCard)', '- rooms:', 'explicit roomId', 'zpeer_ask with mode=\\"async\\"', 'Passive wait rule', 'stop the turn and remain idle', 'avoid spam', 'Raw ZPeer bodies are transient', 'registerMessageRenderer("zob-zpeer-event"', 'scheduleZpeerHeartbeat', 'clearZpeerHeartbeatTimer', 'refreshZpeerSelf(repoRoot', 'kind: "response_sent"', 'kind: "inbound"', 'zpeerStableTeamAgentLeaseRequired', 'withZpeerLeaseMode', 'leaseStatus = "unavailable"', 'runtime_adhoc']) {
176
178
  if (!events.includes(needle)) failures.push(`runtime missing zpeer awareness/event support ${needle}`);
177
179
  }
178
180
  const responseSentBlock = events.match(/setZpeerLastEvent\(state, \{\s*kind: "response_sent"[\s\S]*?customType: "zob-zpeer-event"[\s\S]*?triggerTurn: false[\s\S]*?\}\);/)?.[0] ?? '';
@@ -212,6 +214,11 @@ const goalRuntime = contents['.pi/extensions/zob-harness/src/runtime/goal-runtim
212
214
  for (const needle of ['state.zobLive.passivePeerWait?.suppressGoalContinuation === true', 'clearRuntimeGoalContinuationTimer(state)', 'return;']) {
213
215
  if (!goalRuntime.includes(needle)) failures.push(`goal runtime missing passive peer continuation suppression ${needle}`);
214
216
  }
217
+
218
+ const childRunner = contents['.pi/extensions/zob-harness/src/domains/delegation/child-runner.ts'];
219
+ for (const needle of ['childModelPattern', 'ctx.model', 'resolveCodexFastModeExtension', 'getAgentDir()', 'codex-fast-mode.ts', 'childCodexFastModeExtension', 'args.push("-e", childCodexFastModeExtension)', 'const model = resolvedModel']) {
220
+ if (!childRunner.includes(needle)) failures.push(`delegation child runner missing Codex auto/model inheritance support ${needle}`);
221
+ }
215
222
  for (const needle of ['updatePassivePeerWaitState(state, result', 'result.status !== "waiting"', 'state.zobLive.passivePeerWait = undefined', 'schema: "zob.passive-peer-wait.v1"', 'source: "zpeer_ask"', 'suppressGoalContinuation: true', 'bodyStored: false', 'localOnly: true', 'networkEnabled: false']) {
216
223
  if (!toolsComs.includes(needle)) failures.push(`zpeer_ask missing passive peer wait state handling ${needle}`);
217
224
  }