zob-harness 0.15.2 → 0.15.4

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.
@@ -39,7 +39,7 @@ export { pathMatches } from "./src/core/utils/paths.js";
39
39
 
40
40
  export { parseGoalState, validateGoalState, validateStrictGoalSpecAnchor, parseBillableJobIntake, validateBillableJobIntake } from "./src/domains/goal/goal.js";
41
41
  export type { StrictGoalSpecAnchor, StrictGoalSpecAnchorKind } from "./src/domains/goal/goal.js";
42
- export { DEFAULT_GOAL_ACTIVATION_MODE, clearRuntimeGoalContinuationState, clearRuntimeGoalContinuationStateFor, formatGoalActivationMode, formatRuntimeGoalSummary, queueRuntimeGoalContinuation, restoreRuntimeGoalFromBranch, resumeRuntimeGoal, runtimeGoalStatusLine } from "./src/runtime/goal-runtime.js";
42
+ export { DEFAULT_GOAL_ACTIVATION_MODE, clearRuntimeGoalContinuationState, clearRuntimeGoalContinuationStateFor, formatGoalActivationMode, formatRuntimeGoalSummary, hasPendingUndeliveredZpeerInbound, queueRuntimeGoalContinuation, restoreRuntimeGoalFromBranch, resumeRuntimeGoal, runtimeGoalStatusLine } from "./src/runtime/goal-runtime.js";
43
43
  export type { GoalActivationMode, RuntimeGoal, RuntimeGoalStatus, RuntimeGoalOracleStatus, RuntimeGoalOracleVerdict } from "./src/runtime/goal-runtime.js";
44
44
  export { extractModeIntent, looksLikeCompletePlanResponse, stripModeIntentMarkup, validateModeIntent } from "./src/runtime/mode-intent.js";
45
45
  export type { ZobModeIntent, ZobModeIntentConfidence, ZobModeIntentRisk, ZobModeIntentValidation } from "./src/runtime/mode-intent.js";
@@ -1154,7 +1154,7 @@ export function registerZliveCommands(pi: ExtensionAPI, state: HarnessRuntimeSta
1154
1154
  content: `ZPeer ${event.kind} ${event.fromAlias ? `@${event.fromAlias}` : "?"} → ${event.toAlias ? `@${event.toAlias}` : "?"} ${event.status}`,
1155
1155
  display: true,
1156
1156
  details: { ...recorded },
1157
- }, { triggerTurn: false });
1157
+ }, { triggerTurn: false, deliverAs: "nextTurn" });
1158
1158
  return true;
1159
1159
  };
1160
1160
 
@@ -239,7 +239,7 @@ function handleInboundZpeerPrompt(pi: ExtensionAPI, state: HarnessRuntimeState,
239
239
  content: priority === "normal" ? "ZPeer inbound prompt received (transient body delivered only to agent turn)" : `ZPeer ${priority} inbound prompt received (transient body delivered only to agent turn)`,
240
240
  display: true,
241
241
  details: { kind: "inbound", roomId, fromAlias: envelope.sender, toAlias: envelope.receiver, status: interruptStatus ?? "prompt_received", msgId: envelope.msgId, taskHash: envelope.taskHash, priority, interruptMode, interruptStatus, bodyStored: false, localOnly: true, networkEnabled: false },
242
- }, { triggerTurn: false });
242
+ }, { triggerTurn: false, deliverAs: "nextTurn" });
243
243
  void pi.sendMessage({
244
244
  customType: "zob-coms-inbound",
245
245
  content: envelope.transientPrompt ?? "",
@@ -615,7 +615,7 @@ function formatZpeerLastEvent(event: ZobLiveLastEvent | undefined): string {
615
615
 
616
616
  function buildZpeerAwarenessPrompt(state: HarnessRuntimeState, repoRoot: string): string {
617
617
  if (!state.zobLive.peerCard) {
618
- return "\n\nZPEER AWARENESS\n- local peer endpoint: unavailable this turn\n- Use zpeer_ask with mode=\"async\" or /zpeer only when useful or user-requested for peer coordination; avoid spam/loops and do not invent hidden worker-to-worker chat.";
618
+ return "\n\nZPEER AWARENESS\n- local peer endpoint: unavailable this turn\n- Use zpeer_ask with mode=\"async\" or /zpeer only when useful or user-requested for peer coordination; avoid spam/loops and do not invent hidden worker-to-worker chat.\n- If the user explicitly asks for a reply, status, or confirmation, use requireResponse=true with mode=\"await\" or mode=\"long\"; status=waiting is delivery-only, not evidence that the peer is working or done.";
619
619
  }
620
620
  const summaries = buildZpeerPeerRoomSummaries(repoRoot, state.zobLive.peerCard);
621
621
  const activeSummary = summaries.find((summary) => summary.active) ?? summaries[0];
@@ -632,7 +632,7 @@ function buildZpeerAwarenessPrompt(state: HarnessRuntimeState, repoRoot: string)
632
632
  const activePeerAliases = (activeSummary?.onlineAliases ?? []).filter((alias) => alias !== activeSelfAlias).slice(0, 8).map((alias) => `@${alias}`);
633
633
  const activeUnavailable = (activeSummary?.stale ?? 0) + (activeSummary?.offline ?? 0);
634
634
  const activeDuplicateLine = activeSummary && activeSummary.duplicateAliases.length > 0 ? `\n- duplicate live aliases: ${activeSummary.duplicateAliases.map((alias) => `@${alias}`).join(", ")}` : "";
635
- return `\n\nZPEER AWARENESS (transient, rebuilt each turn)\n- active room: ${activeSummary?.roomId ?? "default"}\n- memberships: ${memberships}\n- self: @${activeSelfAlias}\n- online peers: ${activePeerAliases.join(", ") || "none"}\n- unavailable peers: ${activeUnavailable} (stale=${activeSummary?.stale ?? 0}, offline=${activeSummary?.offline ?? 0})${activeDuplicateLine}\n- rooms:\n${roomLines.join("\n") || " - none"}\n- Use zpeer_ask with explicit roomId when targeting a non-active room.\n- posture: local_socket-only, room-scoped, hash-only durable ledgers, bodyStored=false, networkEnabled=false\n- For non-trivial review/debug/planning peer coordination, agents may use zpeer_ask with mode=\"async\" so the request is visible, governed, and non-blocking; /zpeer remains the interactive command path.\n- Passive wait rule: if the only remaining action is waiting for ZPeer/coms replies, stop the turn and remain idle; do not poll, call tools, or continue just to wait.\n- Use ZPeer only when useful or user-requested; avoid spam, duplicate asks, and reply loops; do not use it for hidden free chat or to bypass topology/safety gates.\n- Raw ZPeer bodies are transient; durable records must remain hash-only/bodyStored=false.\n- last ZPeer event: ${formatZpeerLastEvent(state.zobLive.lastEvent)}`;
635
+ return `\n\nZPEER AWARENESS (transient, rebuilt each turn)\n- active room: ${activeSummary?.roomId ?? "default"}\n- memberships: ${memberships}\n- self: @${activeSelfAlias}\n- online peers: ${activePeerAliases.join(", ") || "none"}\n- unavailable peers: ${activeUnavailable} (stale=${activeSummary?.stale ?? 0}, offline=${activeSummary?.offline ?? 0})${activeDuplicateLine}\n- rooms:\n${roomLines.join("\n") || " - none"}\n- Use zpeer_ask with explicit roomId when targeting a non-active room.\n- posture: local_socket-only, room-scoped, hash-only durable ledgers, bodyStored=false, networkEnabled=false\n- For non-trivial review/debug/planning peer coordination, agents may use zpeer_ask with mode=\"async\" so the request is visible, governed, and non-blocking; /zpeer remains the interactive command path.\n- If the user explicitly asks for a reply, status, or confirmation, use requireResponse=true with mode=\"await\" or mode=\"long\"; status=waiting is delivery-only, not evidence that the peer is working or done.\n- Passive wait rule: if the only remaining action is waiting for ZPeer/coms replies, stop the turn and remain idle; do not poll, call tools, or continue just to wait.\n- Use ZPeer only when useful or user-requested; avoid spam, duplicate asks, and reply loops; do not use it for hidden free chat or to bypass topology/safety gates.\n- Raw ZPeer bodies are transient; durable records must remain hash-only/bodyStored=false.\n- last ZPeer event: ${formatZpeerLastEvent(state.zobLive.lastEvent)}`;
636
636
  }
637
637
 
638
638
  const SAME_AGENT_MODE_INTENT_PROMPT = [
@@ -780,7 +780,7 @@ async function startOrRefreshZobLiveRuntime(pi: ExtensionAPI, state: HarnessRunt
780
780
  content: envelope.errorCode === "zpeer_required_response_expired" ? "ZPeer required response expired" : "ZPeer error received",
781
781
  display: true,
782
782
  details: { kind: state.zobLive.lastEvent?.kind, roomId: state.zobLive.lastEvent?.roomId, fromAlias: envelope.sender, toAlias: envelope.receiver, status: state.zobLive.lastEvent?.status, msgId: envelope.msgId, taskHash: envelope.taskHash, errorHash: envelope.errorHash, bodyStored: false, localOnly: true, networkEnabled: false },
783
- }, { triggerTurn: false });
783
+ }, { triggerTurn: false, deliverAs: "nextTurn" });
784
784
  return buildZobLiveAckEnvelope(envelope);
785
785
  }
786
786
  if (envelope.type === "response") {
@@ -802,7 +802,7 @@ async function startOrRefreshZobLiveRuntime(pi: ExtensionAPI, state: HarnessRunt
802
802
  content: "ZPeer reply received (transient response available; durable records remain hash-only)",
803
803
  display: true,
804
804
  details: { kind: "reply", roomId: state.zobLive.lastEvent?.roomId, fromAlias: envelope.sender, toAlias: envelope.receiver, status: "reply", msgId: envelope.msgId, taskHash: envelope.taskHash, outputHash: envelope.outputHash, bodyStored: false, localOnly: true, networkEnabled: false },
805
- }, { triggerTurn: false });
805
+ }, { triggerTurn: false, deliverAs: "nextTurn" });
806
806
  if (!completedActiveWait && envelope.transientResponse) {
807
807
  void pi.sendMessage({
808
808
  customType: "zob-zpeer-response",
@@ -1101,7 +1101,7 @@ async function sendInboundZobLiveResponse(pi: ExtensionAPI, state: HarnessRuntim
1101
1101
  content: "ZPeer response sent (transient response delivered over local socket; durable records remain hash-only)",
1102
1102
  display: true,
1103
1103
  details: { kind: "response_sent", roomId: state.zobLive.lastEvent?.roomId, fromAlias: inbound.envelope.receiver, toAlias: inbound.envelope.sender, status: "response_sent", msgId: inbound.envelope.msgId, taskHash: inbound.envelope.taskHash, outputHash: responseEnvelope.outputHash, priority: activeInbound?.priority, interruptMode: activeInbound?.interruptMode, bodyStored: false, localOnly: true, networkEnabled: false },
1104
- }, { triggerTurn: false });
1104
+ }, { triggerTurn: false, deliverAs: "nextTurn" });
1105
1105
  }
1106
1106
 
1107
1107
  export function registerHarnessEvents(pi: ExtensionAPI, state: HarnessRuntimeState): void {
@@ -529,6 +529,21 @@ export function resumeRuntimeGoal(goal: RuntimeGoal, requestedAdditionalTurns?:
529
529
  return { previousBlocker, additionalTurns: extendWindow ? additionalTurns : undefined };
530
530
  }
531
531
 
532
+ // Pending inbound ZPeer prompts whose transient body has not yet been delivered
533
+ // to a turn starve the goal-continuation loop: the body (deliverAs "followUp")
534
+ // never reaches a turn slot while continuation keeps re-arming every turn. Yield
535
+ // the loop until EVERY pending inbound has started its turn (turnStartedAt) or
536
+ // been answered, mirroring the outbound passivePeerWait suppression.
537
+ export function hasPendingUndeliveredZpeerInbound(state: HarnessRuntimeState): boolean {
538
+ const queue = state.zobLive.inboundQueue;
539
+ const byMsgId = state.zobLive.inboundByMsgId;
540
+ if (!queue || queue.length === 0 || !byMsgId) return false;
541
+ return queue.some((msgId) => {
542
+ const inbound = byMsgId[msgId];
543
+ return Boolean(inbound && !inbound.responseSent && !inbound.turnStartedAt);
544
+ });
545
+ }
546
+
532
547
  export function queueRuntimeGoalContinuation(pi: ExtensionAPI, state: HarnessRuntimeState, ctx: ExtensionContext, options: { userVisible?: boolean; retryMs?: number } = {}): void {
533
548
  const goal = state.runtimeGoal;
534
549
  if (!canContinue(goal)) return;
@@ -536,6 +551,10 @@ export function queueRuntimeGoalContinuation(pi: ExtensionAPI, state: HarnessRun
536
551
  clearRuntimeGoalContinuationTimer(state);
537
552
  return;
538
553
  }
554
+ if (hasPendingUndeliveredZpeerInbound(state)) {
555
+ clearRuntimeGoalContinuationTimer(state);
556
+ return;
557
+ }
539
558
  const humanDecision = pauseIfHumanDecisionRequired(pi, state, goal);
540
559
  if (humanDecision) {
541
560
  ctx.ui.notify(`ZOB /goal paused: ${goal.oracle.blockerSummary}`, "warning");
@@ -1,4 +1,4 @@
1
- export { DEFAULT_GOAL_ACTIVATION_MODE, DEFAULT_GOAL_MAX_TURNS, DEFAULT_GOAL_RESUME_TURN_EXTENSION, HUMAN_DECISION_REQUIRED_THRESHOLD, ZOB_GOAL_MODE_ENTRY_TYPE, ZOB_RUNTIME_GOAL_CONTINUATION_TYPE, ZOB_RUNTIME_GOAL_ENTRY_TYPE, accountRuntimeGoalTurn, asGoalActivationMode, clearRuntimeGoalContinuationState, clearRuntimeGoalContinuationStateFor, clearRuntimeGoalContinuationTimer, continuationGoalIdFromPrompt, formatGoalActivationMode, formatRuntimeGoalSummary, isRuntimeGoal, pauseRuntimeGoalForStop, persistGoalActivationMode, persistRuntimeGoal, queueRuntimeGoalContinuation, restoreGoalActivationModeFromBranch, restoreRuntimeGoalFromBranch, resumeRuntimeGoal, runtimeGoalStatusLine, scoreGoalHumanDecisionRequired } from "./goal-runtime/state.js";
1
+ export { DEFAULT_GOAL_ACTIVATION_MODE, DEFAULT_GOAL_MAX_TURNS, DEFAULT_GOAL_RESUME_TURN_EXTENSION, HUMAN_DECISION_REQUIRED_THRESHOLD, ZOB_GOAL_MODE_ENTRY_TYPE, ZOB_RUNTIME_GOAL_CONTINUATION_TYPE, ZOB_RUNTIME_GOAL_ENTRY_TYPE, accountRuntimeGoalTurn, asGoalActivationMode, clearRuntimeGoalContinuationState, clearRuntimeGoalContinuationStateFor, clearRuntimeGoalContinuationTimer, continuationGoalIdFromPrompt, formatGoalActivationMode, formatRuntimeGoalSummary, hasPendingUndeliveredZpeerInbound, isRuntimeGoal, pauseRuntimeGoalForStop, persistGoalActivationMode, persistRuntimeGoal, queueRuntimeGoalContinuation, restoreGoalActivationModeFromBranch, restoreRuntimeGoalFromBranch, resumeRuntimeGoal, runtimeGoalStatusLine, scoreGoalHumanDecisionRequired } from "./goal-runtime/state.js";
2
2
  export type { GoalActivationMode, HumanDecisionRequiredScore, RuntimeGoal, RuntimeGoalCompletionProposal, RuntimeGoalLoopState, RuntimeGoalOracleState, RuntimeGoalOracleStatus, RuntimeGoalOracleVerdict, RuntimeGoalStatus, RuntimeGoalUsage } from "./goal-runtime/state.js";
3
3
  export { handleGoalCommand, handleGoalGateCommand } from "./goal-runtime/commands.js";
4
4
  export { registerGoalRuntimeTools } from "./goal-runtime/tools.js";
@@ -354,13 +354,13 @@ const ZpeerAskParams = Type.Object({
354
354
  targetAlias: Type.String({ description: "ZPeer target alias in the current local room. May include or omit the leading @." }),
355
355
  message: Type.String({ description: "Transient peer request body. Used only for local live delivery; never persisted in durable ledgers or reports." }),
356
356
  roomId: Type.Optional(Type.String({ description: "Optional ZPeer room id. Defaults to the current active local room." })),
357
- mode: Type.Optional(StringEnum(["async", "await", "long"] as const, { description: "Send mode. Default async for agent-initiated coordination.", default: "async" })),
357
+ mode: Type.Optional(StringEnum(["async", "await", "long"] as const, { description: "Send mode. Default async for non-blocking coordination; when an actual reply/status is required, use await or long with requireResponse=true.", default: "async" })),
358
358
  reason: Type.Optional(Type.String({ description: "Optional transient coordination/interrupt reason. Required for force; hashed only in visible metadata; raw value is not persisted." })),
359
359
  urgency: Type.Optional(StringEnum(["normal", "urgent", "force"] as const, { description: "ZPeer delivery priority. normal=follow-up, urgent=steer, force=controlled abort+steer when policy allows." })),
360
360
  force: Type.Optional(Type.Boolean({ description: "Alias for urgency=force. Requires reason and remains local/hash-only." })),
361
361
  interruptMode: Type.Optional(StringEnum(["none", "steer", "abort"] as const, { description: "Requested interrupt mode. Derived from urgency when omitted; invalid broadening is blocked by runtime." })),
362
362
  timeoutMs: Type.Optional(Type.Number({ description: "Bounded reply wait timeout for await/long modes, and required-response expiration when requireResponse=true; capped by runtime." })),
363
- requireResponse: Type.Optional(Type.Boolean({ description: "Opt in to a real msgId-correlated required response. Default false; when true the sender waits only for the exact msgId reply and expires explicitly." })),
363
+ requireResponse: Type.Optional(Type.Boolean({ description: "Opt in to a real msgId-correlated required response. Set true whenever the user asks for a reply, status, or confirmation; the sender waits only for the exact msgId reply and expires explicitly." })),
364
364
  maxReinjects: Type.Optional(Type.Number({ description: "Bounded receiver reminder budget for requireResponse. Default 1, capped 0..3." })),
365
365
  });
366
366
 
@@ -275,7 +275,11 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
275
275
  name: "zpeer_ask",
276
276
  label: "ZPeer Ask",
277
277
  description: "Ask a visible local ZPeer via the governed room-scoped local_socket path. Defaults to mode=async; raw message/reply bodies are transient and durable metadata is hash-only.",
278
- promptSnippet: "Use zpeer_ask with mode=\"async\" for useful non-trivial peer review/debug/planning coordination; if it returns waiting and nothing else is actionable, stop/idle instead of polling.",
278
+ promptSnippet: "Use zpeer_ask with mode=\"async\" for non-blocking coordination; when an actual reply/status is required, set requireResponse=true and use mode=\"await\" or mode=\"long\".",
279
+ promptGuidelines: [
280
+ "When the user asks for an actual reply, current status, confirmation, or a response requirement, call zpeer_ask with requireResponse=true and mode=\"await\" or mode=\"long\".",
281
+ "Treat zpeer_ask status=waiting as delivery-only: it does not prove the peer read the request, is working, or finished. If nothing else is actionable, stop/idle instead of polling.",
282
+ ],
279
283
  parameters: ZpeerAskParams,
280
284
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
281
285
  if (!state?.zobLive.peerCard) return { content: [{ type: "text", text: "zpeer_ask blocked: current session has not registered a local peer endpoint" }], details: { schema: "zob.zpeer-ask-result.v1", status: "blocked", reason: "local_peer_unavailable", bodyStored: false } };
@@ -296,7 +300,7 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
296
300
  content: queued.content,
297
301
  display: true,
298
302
  details: queued.details,
299
- }, { triggerTurn: false }));
303
+ }, { triggerTurn: false, deliverAs: "nextTurn" }));
300
304
  }
301
305
  };
302
306
  const emitZpeerAskEvent = (event: { kind: NonNullable<HarnessRuntimeState["zobLive"]["lastEvent"]>["kind"]; status: string; reason?: string; msgId?: string; roomId?: string; taskHash?: string; outputHash?: string; interruptStatus?: ZpeerInterruptStatus }): boolean => {
@@ -344,7 +348,7 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
344
348
  updatePassivePeerWaitState(state, result, { roomId: requestedRoomId, targetAlias });
345
349
  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() });
346
350
  const ok = result.status === "reply" || result.status === "completed" || result.status === "waiting" || result.status === "delivered";
347
- const passiveWaitSuffix = result.status === "waiting" ? " · idle/passive wait: no follow-up turn queued; stop if no other action is actionable" : "";
351
+ const passiveWaitSuffix = result.status === "waiting" ? " · delivery-only: no reply received; not evidence the target is working or done · idle/passive wait: no follow-up turn queued; stop if no other action is actionable" : "";
348
352
  const transientReplyText = (result.status === "reply" || result.status === "completed") && result.transientResponse
349
353
  ? `\n\nTransient ZPeer reply (not stored in .pi/coms):\n${result.transientResponse}`
350
354
  : "";
@@ -391,7 +395,7 @@ export function registerComsTools(pi: ExtensionAPI, state?: HarnessRuntimeState)
391
395
  state.zobLive.inboundQueue = (state.zobLive.inboundQueue ?? []).filter((candidate) => candidate !== inbound.envelope.msgId);
392
396
  const roomId = inbound.envelope.runId?.startsWith("zpeer:") ? inbound.envelope.runId.slice("zpeer:".length) : undefined;
393
397
  const recorded = recordZpeerRuntimeEvent(state, { kind: "response_sent", roomId, fromAlias: inbound.envelope.receiver, toAlias: inbound.envelope.sender, status: "response_sent", msgId: inbound.envelope.msgId, taskHash: inbound.envelope.taskHash, outputHash, priority: inbound.priority, interruptMode: inbound.interruptMode });
394
- if (recorded.accepted) void pi.sendMessage({ customType: "zob-zpeer-event", content: "ZPeer explicit reply sent", display: true, details: { ...recorded.event } }, { triggerTurn: false });
398
+ if (recorded.accepted) void pi.sendMessage({ customType: "zob-zpeer-event", content: "ZPeer explicit reply sent", display: true, details: { ...recorded.event } }, { triggerTurn: false, deliverAs: "nextTurn" });
395
399
  pi.appendEntry("zob-zpeer", { schema: "zob.zpeer-reply.v1", action: "reply", status: "response_sent", msgId: inbound.envelope.msgId, taskHash: inbound.envelope.taskHash, outputHash, priority: inbound.priority, interruptMode: inbound.interruptMode, localOnly: true, networkEnabled: false, bodyStored: false, promptBodiesStored: false, outputBodiesStored: false, generatedAt: new Date().toISOString() });
396
400
  return { content: [{ type: "text", text: `zpeer_reply sent: msgId=${inbound.envelope.msgId} outputHash=${outputHash}` }], details: { schema: "zob.zpeer-reply-result.v1", status: "response_sent", msgId: inbound.envelope.msgId, taskHash: inbound.envelope.taskHash, outputHash, bodyStored: false, localOnly: true, networkEnabled: false } };
397
401
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zob-harness",
3
- "version": "0.15.2",
3
+ "version": "0.15.4",
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",
@@ -471,10 +471,11 @@ async function main() {
471
471
  const registeredTools = new Map();
472
472
  const appendEntries = [];
473
473
  const feedMessages = [];
474
+ const feedDeliveries = [];
474
475
  const mockPi = {
475
476
  registerTool: (tool) => registeredTools.set(tool.name, tool),
476
477
  appendEntry: (customType, data) => appendEntries.push({ customType, data }),
477
- sendMessage: (message) => { feedMessages.push(message); return Promise.resolve(); },
478
+ sendMessage: (message, options) => { feedMessages.push(message); feedDeliveries.push({ message, options }); return Promise.resolve(); },
478
479
  };
479
480
  const toolState = { zobLive: { peerCard: alpha, pendingReplies: { wait: waitForReply } } };
480
481
  toolsComs.registerComsTools(mockPi, toolState);
@@ -488,6 +489,9 @@ async function main() {
488
489
  assert(JSON.stringify(toolResult?.content ?? '').includes('idle/passive wait: no follow-up turn queued'), 'zpeer_ask async waiting result must tell the agent to idle instead of polling/continuing just to wait');
489
490
  const asyncWaitingFeed = feedMessages.filter((item) => item.customType === 'zob-zpeer-event' && item.details?.source === 'agent-request' && item.details?.status === 'waiting' && item.details?.msgId === toolResult?.details?.msgId);
490
491
  assert(asyncWaitingFeed.length === 1, `zpeer_ask async must emit exactly one compact waiting feed event, got ${asyncWaitingFeed.length}`);
492
+ const asyncWaitingDelivery = feedDeliveries.find((item) => item.message.details?.source === 'agent-request' && item.message.details?.msgId === toolResult?.details?.msgId);
493
+ assert(asyncWaitingDelivery?.options?.triggerTurn === false && asyncWaitingDelivery?.options?.deliverAs === 'nextTurn', 'zpeer_ask waiting feed must use nextTurn delivery and never create an extra steering/follow-up turn');
494
+ assert(JSON.stringify(toolResult?.content ?? '').includes('delivery-only: no reply received; not evidence the target is working or done'), 'zpeer_ask waiting result must not imply peer progress from delivery-only status');
491
495
  assert(!feedMessages.some((item) => item.customType === 'zob-zpeer-event' && item.details?.source === 'agent-request' && item.details?.kind === 'attempt'), 'zpeer_ask async must not emit a pre-ACK attempt feed event');
492
496
  assert(!containsRawBody(toolResult), 'zpeer_ask async tool result must not echo the full prompt/response body');
493
497
  assert(appendEntries.some((item) => item.customType === 'zob-zpeer' && item.data?.schema === 'zob.zpeer-ask.v1' && item.data?.action === 'agent_request' && item.data?.mode === 'async' && item.data?.bodyStored === false), 'zpeer_ask must append hash-only visible command metadata');
@@ -538,6 +542,8 @@ async function main() {
538
542
  const explicitReplyCountBefore = receivedResponses.length;
539
543
  const explicitReplyResult = await zpeerReply.execute('tool-call-zpeer-reply', { msgId: explicitReplyMsgId, message: rawResponse }, undefined, undefined, { cwd: repoRoot });
540
544
  assert(explicitReplyResult?.details?.status === 'response_sent' && explicitReplyResult?.details?.msgId === explicitReplyMsgId && explicitReplyResult?.details?.outputHash === hashing.sha256(rawResponse), 'zpeer_reply must send a msgId-bound response with outputHash metadata');
545
+ const explicitReplyFeedDelivery = feedDeliveries.find((item) => item.message.details?.kind === 'response_sent' && item.message.details?.msgId === explicitReplyMsgId);
546
+ assert(explicitReplyFeedDelivery?.options?.triggerTurn === false && explicitReplyFeedDelivery?.options?.deliverAs === 'nextTurn', 'zpeer_reply feed must use nextTurn delivery and never create an extra steering/follow-up turn');
541
547
  assert(!toolState.zobLive.inboundByMsgId?.[explicitReplyMsgId] && toolState.zobLive.activeInboundMsgId === undefined && toolState.zobLive.inboundQueue.length === 0, 'zpeer_reply must clear the answered inbound msgId from receiver state');
542
548
  assert(receivedResponses.length > explicitReplyCountBefore && receivedResponses.at(-1)?.replyToMsgId === explicitReplyMsgId && receivedResponses.at(-1)?.responseHash === hashing.sha256(rawResponse), 'zpeer_reply must deliver a local response envelope bound to replyToMsgId');
543
549
  const wrongExplicitReply = await zpeerReply.execute('tool-call-zpeer-reply-wrong', { msgId: 'missing-msgid', message: rawResponse }, undefined, undefined, { cwd: repoRoot });
@@ -61,10 +61,11 @@ const zpeerAskMatches = toolsComs.match(/name: "zpeer_ask"/g) ?? [];
61
61
  if (zpeerAskMatches.length !== 1) failures.push(`expected exactly one zpeer_ask tool, found ${zpeerAskMatches.length}`);
62
62
  const zpeerReplyMatches = toolsComs.match(/name: "zpeer_reply"/g) ?? [];
63
63
  if (zpeerReplyMatches.length !== 1) failures.push(`expected exactly one zpeer_reply tool, found ${zpeerReplyMatches.length}`);
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']) {
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', 'clampRate(process.env.ZOB_ZPEER_ASK_RATE_LIMIT_PER_MINUTE, 50)', 'clampRate(process.env.ZOB_ZPEER_URGENT_RATE_LIMIT_PER_MINUTE, 10)', 'clampRate(process.env.ZOB_ZPEER_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', 'delivery-only: no reply received; not evidence the target is working or done', 'promptGuidelines:', 'requireResponse=true and mode=\\"await\\" or mode=\\"long\\"', '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']) {
65
65
  if (!toolsComs.includes(needle)) failures.push(`zpeer_ask tool missing ${needle}`);
66
66
  }
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']) {
67
+ if (!toolsComs.includes('}, { triggerTurn: false, deliverAs: "nextTurn" }')) failures.push('zpeer tool feed cards must use nextTurn delivery so metadata-only UI cannot create steering turns');
68
+ for (const needle of ['parameters: ZpeerReplyParams', 'buildZobLiveResponseEnvelope', 'sendZobLocalEnvelope(replyEndpoint', 'replyToMsgId: inbound.envelope.msgId', 'action: "reply"', 'action: "reply_blocked"', 'status: "response_sent"', 'late reply after the requireResponse watchdog expired']) {
68
69
  if (!toolsComs.includes(needle)) failures.push(`zpeer_reply tool missing ${needle}`);
69
70
  }
70
71
  if (toolsComs.includes('emitZpeerAskEvent({ kind: "attempt", status: "agent-request"')) failures.push('zpeer_ask async must not emit pre-ACK attempt feed noise');
@@ -178,6 +179,7 @@ for (const needle of ['customType: "zob-zpeer-event"', 'if (sendMode.mode !== "a
178
179
  if (!command.includes(needle)) failures.push(`zpeer command missing UX event/status support ${needle}`);
179
180
  }
180
181
  if (!command.includes('const eventFromAlias = peerAliasInRoom(state.zobLive.peerCard, eventRoomId)') || !command.includes('peerAliasInRoom(state.zobLive.peerCard, resultRoomId)')) failures.push('/zpeer in <room> events must use room-scoped sender alias');
182
+ if (!command.includes('}, { triggerTurn: false, deliverAs: "nextTurn" });')) failures.push('/zpeer feed cards must use nextTurn delivery so metadata-only UI cannot create steering turns');
181
183
  for (const forbidden of ['transientPrompt:', 'transientResponse:', 'prompt:', 'output:', 'content:', 'message:', 'text:', 'diff:', 'patch:']) {
182
184
  const appendBlocks = [...command.matchAll(/appendEntry\("zob-zpeer",\s*\{[\s\S]*?\}\);/g)].map((m) => m[0]);
183
185
  if (appendBlocks.some((block) => block.includes(forbidden))) failures.push(`zpeer appendEntry contains forbidden key ${forbidden}`);
@@ -215,6 +217,9 @@ for (const needle of ['readZpeerNewCarryoverProfile(repoRoot)', 'const carryover
215
217
  }
216
218
  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');
217
219
  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');
220
+ const eventZpeerFeedFragments = events.split('customType: "zob-zpeer-event"').slice(1);
221
+ if (eventZpeerFeedFragments.length === 0 || eventZpeerFeedFragments.some((fragment) => !fragment.slice(0, 800).includes('}, { triggerTurn: false, deliverAs: "nextTurn" });'))) failures.push('every runtime ZPeer feed card must use nextTurn delivery so it cannot race ahead of actionable prompt/response bodies');
222
+ if (!events.includes('status=waiting is delivery-only, not evidence that the peer is working or done')) failures.push('runtime awareness must prevent interpreting waiting as peer progress/completion evidence');
218
223
  for (const needle of ['forceAbortAllowedForCurrentState', 'interruptStatus = "force_blocked"', 'interruptStatus = "force_downgraded"', 'ctx.abort()', 'const deliverAs = priority === "normal" ? "followUp" : "steer"', 'inboundByMsgId', 'activeInboundMsgId', 'state.zobLive.inboundByMsgId && !activeInbound', 'scheduleZpeerRequiredResponseWatchdog', 'required_response_reinject', 'required_response_expired', 'Original transient message:', 'zpeer_required_response_expired', 'pendingReplies.expire', 'forceAbortRepeated: false', 'replyToMsgId !== envelope.msgId', 'wrong or missing replyToMsgId']) {
219
224
  if (!events.includes(needle)) failures.push(`runtime missing urgent/force/msgId-safe inbound support ${needle}`);
220
225
  }
@@ -258,6 +263,9 @@ const goalRuntime = contents['.pi/extensions/zob-harness/src/runtime/goal-runtim
258
263
  for (const needle of ['state.zobLive.passivePeerWait?.suppressGoalContinuation === true', 'clearRuntimeGoalContinuationTimer(state)', 'return;']) {
259
264
  if (!goalRuntime.includes(needle)) failures.push(`goal runtime missing passive peer continuation suppression ${needle}`);
260
265
  }
266
+ for (const needle of ['export function hasPendingUndeliveredZpeerInbound(state: HarnessRuntimeState): boolean', '!inbound.responseSent && !inbound.turnStartedAt', 'if (hasPendingUndeliveredZpeerInbound(state)) {']) {
267
+ if (!goalRuntime.includes(needle)) failures.push(`goal runtime missing inbound ZPeer body-delivery continuation suppression ${needle}`);
268
+ }
261
269
 
262
270
  const childRunner = contents['.pi/extensions/zob-harness/src/domains/delegation/child-runner.ts'];
263
271
  for (const needle of ['childModelPattern', 'ctx.model', 'resolveCodexFastModeExtension', 'getAgentDir()', 'codex-fast-mode.ts', 'childCodexFastModeExtension', 'args.push("-e", childCodexFastModeExtension)', 'const model = resolvedModel']) {