zob-harness 0.11.0 → 0.12.0

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.
@@ -52,6 +52,8 @@ export type { PlanLaunchStatus, PlanTodoCanonicalItem, PlanTodoCanonicalManifest
52
52
  export { ZOB_COMPACTION_CONTINUITY_CONTRACT, ZOB_TOOL_ROUTING_CONTRACT } from "./src/core/constants.js";
53
53
  export { ZOB_COMPACTION_DETAILS_SCHEMA, ZOB_COMPACTION_ENTRY_TYPE, ZOB_COMPACTION_HARD_CAP_TOKENS, ZOB_COMPACTION_LEDGER_SCHEMA, ZOB_COMPACTION_SUMMARY_SCHEMA, ZOB_COMPACTION_TARGET_TOKENS, buildDeterministicZobCompactionResult, buildDeterministicZobCompactionSummary, buildZobCompactionDetails, buildZobCompactionInstructions, buildZobCompactionLedgerEntry, buildZobCompactionStateCapsule, withZobCompactionDetails, zobCompactionBodyFreeViolations } from "./src/runtime/compaction-policy.js";
54
54
  export type { ZobCompactionDetails, ZobCompactionFileRefsInput, ZobCompactionInstructionInput, ZobCompactionLedgerEntry, ZobCompactionStateCapsule } from "./src/runtime/compaction-policy.js";
55
+ export { assistantMessageHasVisibleOutput, createStopRestoreCandidate, findStopRestoreUserEntryId, markStopRestoreAssistantMessage, markStopRestoreRestored, markStopRestoreToolVisible, shouldRestoreStopPrompt } from "./src/runtime/stop-restore.js";
56
+ export type { StopRestoreCandidate, StopRestoreCandidateInput, StopRestoreDecision, StopRestoreDecisionInput, StopRestoreRewindResult } from "./src/runtime/stop-restore.js";
55
57
  export { isAdaptiveZmodeAlias, renderAdaptiveZmodeTemplate, resolveAdaptiveZmodeEntrypoint, validateAdaptiveZmodeEntrypoint } from "./src/runtime/adaptive-zmode.js";
56
58
  export type { AdaptiveZmodeAlias, AdaptiveZmodeEntrypoint } from "./src/runtime/adaptive-zmode.js";
57
59
  export {
@@ -15,6 +15,7 @@ import type { HarnessRuntimeState } from "../state.js";
15
15
  import { renderHarnessWidget } from "../widget.js";
16
16
  import type { HarnessCommandContext } from "./types.js";
17
17
  import { daemonInputFromState, daemonRuntimeLedgerEntry, stopDaemonLoop } from "./daemon.js";
18
+ import { findStopRestoreUserEntryId, markStopRestoreRestored, shouldRestoreStopPrompt, type StopRestoreDecision, type StopRestoreRewindResult } from "../stop-restore.js";
18
19
 
19
20
  function abortForegroundWork(ctx: HarnessCommandContext): boolean {
20
21
  const idle = typeof ctx.isIdle === "function" ? ctx.isIdle() : true;
@@ -52,6 +53,8 @@ function stopCommandLedgerEntry(input: {
52
53
  daemonWasRunning: boolean;
53
54
  runtimeGoalPaused: boolean;
54
55
  runtimeGoalId?: string;
56
+ stopRestoreDecision?: StopRestoreDecision;
57
+ stopRestoreRewind?: StopRestoreRewindResult;
55
58
  }): Record<string, unknown> {
56
59
  return {
57
60
  schema: "zob.stop-command.v1",
@@ -63,6 +66,14 @@ function stopCommandLedgerEntry(input: {
63
66
  daemonWasRunning: input.daemonWasRunning,
64
67
  runtimeGoalPaused: input.runtimeGoalPaused,
65
68
  runtimeGoalId: input.runtimeGoalId,
69
+ editorPromptRestored: input.stopRestoreDecision?.restore === true,
70
+ editorPromptRestoreReason: input.stopRestoreDecision?.reason,
71
+ editorPromptHash: input.stopRestoreDecision?.promptHash,
72
+ assistantOutputObservedBeforeStop: input.stopRestoreDecision?.assistantOutputObserved,
73
+ editorPromptRewindAttempted: input.stopRestoreRewind?.attempted,
74
+ editorPromptRewindSucceeded: input.stopRestoreRewind?.succeeded,
75
+ editorPromptRewindReason: input.stopRestoreRewind?.reason,
76
+ editorPromptRewindTargetId: input.stopRestoreRewind?.targetEntryId,
66
77
  bodyStored: false,
67
78
  promptBodiesStored: false,
68
79
  outputBodiesStored: false,
@@ -70,6 +81,36 @@ function stopCommandLedgerEntry(input: {
70
81
  };
71
82
  }
72
83
 
84
+ async function restoreStopPromptAndRewind(ctx: HarnessCommandContext, state: HarnessRuntimeState, decision: StopRestoreDecision): Promise<StopRestoreRewindResult> {
85
+ if (!decision.restore || !decision.promptText) {
86
+ return { attempted: false, succeeded: false, reason: decision.reason };
87
+ }
88
+ try {
89
+ if (typeof ctx.waitForIdle === "function") await ctx.waitForIdle();
90
+ } catch {
91
+ ctx.ui.setEditorText(decision.promptText);
92
+ markStopRestoreRestored(state.stopRestoreCandidate);
93
+ return { attempted: true, succeeded: false, reason: "wait_for_idle_failed" };
94
+ }
95
+ const targetEntryId = findStopRestoreUserEntryId(state.stopRestoreCandidate, ctx.sessionManager.getEntries());
96
+ if (!targetEntryId) {
97
+ ctx.ui.setEditorText(decision.promptText);
98
+ markStopRestoreRestored(state.stopRestoreCandidate);
99
+ return { attempted: true, succeeded: false, reason: "user_entry_not_found" };
100
+ }
101
+ try {
102
+ const navigation = await ctx.navigateTree(targetEntryId, { summarize: false });
103
+ ctx.ui.setEditorText(decision.promptText);
104
+ markStopRestoreRestored(state.stopRestoreCandidate);
105
+ if (navigation.cancelled) return { attempted: true, succeeded: false, reason: "navigate_cancelled", targetEntryId };
106
+ return { attempted: true, succeeded: true, reason: "rewound_to_prompt_checkpoint", targetEntryId };
107
+ } catch {
108
+ ctx.ui.setEditorText(decision.promptText);
109
+ markStopRestoreRestored(state.stopRestoreCandidate);
110
+ return { attempted: true, succeeded: false, reason: "navigate_failed", targetEntryId };
111
+ }
112
+ }
113
+
73
114
  export function registerNewCommand(pi: ExtensionAPI, state: HarnessRuntimeState): void {
74
115
  // Exact `/new` is handled by Pi before extension input/command hooks. Soft carryover
75
116
  // is therefore written from the `session_shutdown` reason="new" hook in events.ts.
@@ -116,11 +157,17 @@ export function registerStopCommand(pi: ExtensionAPI, state: HarnessRuntimeState
116
157
  const daemonWasRunning = state.daemon.loop.status === "running";
117
158
  const runtimeGoalId = state.runtimeGoal?.goalId;
118
159
  const foregroundAbortRequested = abortForegroundWork(ctx);
160
+ const stopRestoreDecision = shouldRestoreStopPrompt(state.stopRestoreCandidate, {
161
+ foregroundAbortRequested,
162
+ idleBeforeStop,
163
+ pendingMessagesBeforeStop,
164
+ });
119
165
  const background = abortBackgroundDelegations(state);
120
166
  stopDaemonLoop(state, "slash_stop");
121
167
  const pausedGoal = pauseRuntimeGoalForStop(pi, state, "stopped by /stop; use /goal resume to continue");
122
168
  const daemonState = buildDaemonRuntimeState(daemonInputFromState(state));
123
169
  state.daemon.lastStatus = daemonState;
170
+ const stopRestoreRewind = await restoreStopPromptAndRewind(ctx, state, stopRestoreDecision);
124
171
  pi.appendEntry("zob-daemon-runtime", daemonRuntimeLedgerEntry(daemonState));
125
172
  pi.appendEntry("zob-stop", stopCommandLedgerEntry({
126
173
  foregroundAbortRequested,
@@ -131,9 +178,11 @@ export function registerStopCommand(pi: ExtensionAPI, state: HarnessRuntimeState
131
178
  daemonWasRunning,
132
179
  runtimeGoalPaused: Boolean(pausedGoal && pausedGoal.goalId === runtimeGoalId && pausedGoal.status === "paused"),
133
180
  runtimeGoalId,
181
+ stopRestoreDecision,
182
+ stopRestoreRewind,
134
183
  }));
135
184
  renderHarnessWidget(pi, state, ctx);
136
- ctx.ui.notify(`ZOB stop: foreground=${foregroundAbortRequested ? "aborted" : "idle"} background_aborted=${background.abortedCount} daemon=${daemonWasRunning ? "stopped" : "already_stopped"} goal=${pausedGoal?.status ?? "none"}`, "warning");
185
+ ctx.ui.notify(`ZOB stop: foreground=${foregroundAbortRequested ? "aborted" : "idle"} input=${stopRestoreDecision.restore ? "restored" : "unchanged"} rewind=${stopRestoreRewind.succeeded ? "yes" : "no"} background_aborted=${background.abortedCount} daemon=${daemonWasRunning ? "stopped" : "already_stopped"} goal=${pausedGoal?.status ?? "none"}`, "warning");
137
186
  },
138
187
  });
139
188
  }
@@ -3,7 +3,7 @@ import { resolve, sep } from "node:path";
3
3
  import type { Theme } from "@earendil-works/pi-coding-agent";
4
4
  import { Markdown, truncateToWidth, visibleWidth, type MarkdownTheme } from "@earendil-works/pi-tui";
5
5
 
6
- import { delegationDurationMs, delegationSignalBadge, delegationSignalColor, formatDelegationContextLabel, formatDelegationCostLabel, formatDelegationModelLabel, formatDelegationSignalBadge, formatDuration, statusIcon, type DelegationRunView } from "./delegation-monitor.js";
6
+ import { delegationDurationMs, delegationSignalBadge, delegationSignalColor, formatDelegationContextLabel, formatDelegationCostLabel, formatDelegationCwdLabel, formatDelegationModelLabel, formatDelegationSignalBadge, formatDelegationWorkspaceLabel, formatDuration, statusIcon, type DelegationRunView } from "./delegation-monitor.js";
7
7
  import { formatActivityDuration, formatActivitySummary, readDelegationActivitySnapshot } from "./delegation-activity.js";
8
8
  import { sanitizeDelegationText } from "./delegation-markdown.js";
9
9
  import { isRecord } from "../core/utils/records.js";
@@ -109,6 +109,7 @@ export function delegationFeedFingerprint(run: DelegationRunView | undefined, re
109
109
  run.usage?.contextTokens ?? "",
110
110
  (run.gateErrors ?? []).join(";"),
111
111
  run.model ?? "",
112
+ run.cwd ?? "",
112
113
  ].join("|");
113
114
  }
114
115
 
@@ -395,7 +396,10 @@ export function renderDelegationFeedLines(run: DelegationRunView | undefined, re
395
396
  const signalBadge = delegationSignalBadge(run);
396
397
  const signalText = formatDelegationSignalBadge(signalBadge);
397
398
  const modelLabel = formatDelegationModelLabel(run);
398
- lines.push(theme.fg("dim", `${statusIcon(run.status)} ${run.agent}${signalText ? ` · ${theme.fg(delegationSignalColor(signalBadge), signalText)}` : ""}${modelLabel ? ` · ${theme.fg("muted", `(${modelLabel})`)}` : ""} · ${run.status}${run.failureKind ? ` · ${run.failureKind}` : ""} · ${formatDuration(delegationDurationMs(run))} · ${formatDelegationCostLabel(run)} · ${formatDelegationContextLabel(run)}`));
399
+ const cwdLabel = formatDelegationCwdLabel(run, repoRoot);
400
+ const workspaceLabel = formatDelegationWorkspaceLabel(run, repoRoot, Math.max(24, safeWidth - 2));
401
+ lines.push(theme.fg("dim", `${statusIcon(run.status)} ${run.agent}${signalText ? ` · ${theme.fg(delegationSignalColor(signalBadge), signalText)}` : ""}${modelLabel ? ` · ${theme.fg("muted", `(${modelLabel})`)}` : ""}${cwdLabel ? ` · ${theme.fg("muted", cwdLabel)}` : ""} · ${run.status}${run.failureKind ? ` · ${run.failureKind}` : ""} · ${formatDuration(delegationDurationMs(run))} · ${formatDelegationCostLabel(run)} · ${formatDelegationContextLabel(run)}`));
402
+ if (workspaceLabel) lines.push(theme.fg("accent", workspaceLabel));
399
403
  if (run.usage) lines.push(theme.fg("muted", `usage · in ${run.usage.input} · out ${run.usage.output} · cache ${run.usage.cacheRead}/${run.usage.cacheWrite} · context ${run.usage.contextTokens}`));
400
404
  if (run.taskPreview) lines.push(theme.fg("muted", `task · ${sanitizeDelegationText(run.taskPreview)}`));
401
405
  renderLiveActivityCard(run, repoRoot, lines, safeWidth, theme);
@@ -1,5 +1,5 @@
1
1
  import { closeSync, existsSync, openSync, readFileSync, readSync, statSync } from "node:fs";
2
- import { resolve, sep } from "node:path";
2
+ import { relative, resolve, sep } from "node:path";
3
3
 
4
4
  import type { ChildResult, DelegationFailureKind } from "../types.js";
5
5
  import { isRecord } from "../core/utils/records.js";
@@ -29,6 +29,7 @@ export interface DelegationRunView {
29
29
  endedAtMs?: number;
30
30
  outputPreview: string;
31
31
  stderrPreview: string;
32
+ cwd?: string;
32
33
  sessionPath?: string;
33
34
  exitCode?: number;
34
35
  gatePassed?: boolean;
@@ -255,6 +256,29 @@ export function formatDelegationModelLabel(run: DelegationRunView | { model?: st
255
256
  return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`;
256
257
  }
257
258
 
259
+ function delegationCwdValue(run: DelegationRunView | ChildResult | { cwd?: string } | undefined, repoRoot: string): string | undefined {
260
+ if (!run?.cwd) return undefined;
261
+ const resolvedRoot = resolve(repoRoot);
262
+ const resolvedCwd = resolve(run.cwd);
263
+ const rel = resolvedCwd === resolvedRoot ? "." : relative(resolvedRoot, resolvedCwd);
264
+ const inside = rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !rel.startsWith(sep));
265
+ return inside ? rel || "." : resolvedCwd;
266
+ }
267
+
268
+ function truncateLabel(label: string, limit: number): string {
269
+ return label.length <= limit ? label : `${label.slice(0, limit - 1)}…`;
270
+ }
271
+
272
+ export function formatDelegationCwdLabel(run: DelegationRunView | ChildResult | { cwd?: string } | undefined, repoRoot: string, limit = 48): string {
273
+ const value = delegationCwdValue(run, repoRoot);
274
+ return value ? truncateLabel(`cwd ${value}`, limit) : "";
275
+ }
276
+
277
+ export function formatDelegationWorkspaceLabel(run: DelegationRunView | ChildResult | { cwd?: string } | undefined, repoRoot: string, limit = 72): string {
278
+ const value = delegationCwdValue(run, repoRoot);
279
+ return value ? truncateLabel(`workspace · ${value}`, limit) : "";
280
+ }
281
+
258
282
  function stripSignalControlSequences(text: string): string {
259
283
  return text
260
284
  .replace(/\x1b\][\s\S]*?(?:\x07|\x1b\\)/g, "")
@@ -429,6 +453,7 @@ export function startDelegationRun(state: DelegationMonitorState, input: {
429
453
  agent: string;
430
454
  task: string;
431
455
  startedAtMs: number;
456
+ cwd?: string;
432
457
  }): DelegationRunView {
433
458
  const existingIndex = state.runs.findIndex((run) => run.id === input.id);
434
459
  const run: DelegationRunView = {
@@ -443,6 +468,7 @@ export function startDelegationRun(state: DelegationMonitorState, input: {
443
468
  startedAtMs: input.startedAtMs,
444
469
  outputPreview: "",
445
470
  stderrPreview: "",
471
+ cwd: input.cwd,
446
472
  };
447
473
  if (existingIndex >= 0) state.runs[existingIndex] = run;
448
474
  else state.runs.push(run);
@@ -463,6 +489,7 @@ export function updateDelegationRun(state: DelegationMonitorState, id: string, p
463
489
  if (patch.endedAtMs !== undefined) run.endedAtMs = patch.endedAtMs;
464
490
  if (patch.outputPreview !== undefined) run.outputPreview = capPreview(patch.outputPreview);
465
491
  if (patch.stderrPreview !== undefined) run.stderrPreview = capPreview(patch.stderrPreview);
492
+ if (patch.cwd !== undefined) run.cwd = patch.cwd;
466
493
  if (patch.sessionPath !== undefined) run.sessionPath = patch.sessionPath;
467
494
  if (patch.exitCode !== undefined) run.exitCode = patch.exitCode;
468
495
  if (patch.gatePassed !== undefined) run.gatePassed = patch.gatePassed;
@@ -539,6 +566,7 @@ export function buildDelegationLogLines(run: DelegationRunView | undefined, repo
539
566
  `[delegation ${run.id}]`,
540
567
  `agent: ${run.agent}`,
541
568
  run.model ? `model: ${run.model}` : undefined,
569
+ formatDelegationWorkspaceLabel(run, repoRoot) || undefined,
542
570
  `status: ${run.status}`,
543
571
  `duration: ${formatDuration(delegationDurationMs(run))}`,
544
572
  run.sessionPath ? `session: ${run.sessionPath}` : "session: not captured yet",
@@ -2,7 +2,7 @@ import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { matchesKey, truncateToWidth, visibleWidth, type Component, type TUI } from "@earendil-works/pi-tui";
3
3
 
4
4
  import type { DelegationMonitorState, DelegationRunView, DelegationSortMode } from "./delegation-monitor.js";
5
- import { buildDelegationGroups, delegationCost, delegationDurationMs, delegationSignalBadge, delegationSignalColor, formatDelegationContextLabel, formatDelegationCost, formatDelegationCostLabel, formatDelegationModelLabel, formatDelegationSignalBadge, formatDuration, statusIcon } from "./delegation-monitor.js";
5
+ import { buildDelegationGroups, delegationCost, delegationDurationMs, delegationSignalBadge, delegationSignalColor, formatDelegationContextLabel, formatDelegationCost, formatDelegationCostLabel, formatDelegationCwdLabel, formatDelegationModelLabel, formatDelegationSignalBadge, formatDelegationWorkspaceLabel, formatDuration, statusIcon } from "./delegation-monitor.js";
6
6
  import { delegationFeedFingerprint, renderDelegationFeedLines } from "./delegation-feed.js";
7
7
  import { delegateCloseButton, delegateSelectMarker } from "./delegation-click-markers.js";
8
8
  import { disableDelegationMouseMode, enableDelegationMouseMode, handleDelegationMouseInput } from "./delegation-mouse.js";
@@ -247,8 +247,9 @@ export class DelegationOverlayComponent implements Component {
247
247
  const headerLeft = `${th.fg(this.activePane === "list" ? "accent" : "muted", this.activePane === "list" ? "▶ Agents" : " Agents")} ${th.fg("muted", delegateCloseButton())}`;
248
248
  const selectedBadge = delegationSignalBadge(selected);
249
249
  const selectedBadgeText = formatDelegationSignalBadge(selectedBadge);
250
+ const selectedWorkspaceLabel = formatDelegationWorkspaceLabel(selected, this.repoRoot, 42);
250
251
  const selectedTitle = selected
251
- ? `${th.fg(statusColor(selected.status), `${statusIcon(selected.status)} ${selected.agent}`)}${selectedBadgeText ? ` ${th.fg(delegationSignalColor(selectedBadge), selectedBadgeText)}` : ""}${formatDelegationModelLabel(selected) ? ` ${th.fg("muted", `(${formatDelegationModelLabel(selected)})`)}` : ""} ${th.fg("dim", formatDuration(delegationDurationMs(selected)))} ${th.fg("accent", formatDelegationCostLabel(selected))} ${th.fg("muted", formatDelegationContextLabel(selected))}`
252
+ ? `${th.fg(statusColor(selected.status), `${statusIcon(selected.status)} ${selected.agent}`)}${selectedBadgeText ? ` ${th.fg(delegationSignalColor(selectedBadge), selectedBadgeText)}` : ""}${formatDelegationModelLabel(selected) ? ` ${th.fg("muted", `(${formatDelegationModelLabel(selected)})`)}` : ""}${selectedWorkspaceLabel ? ` ${th.fg("muted", selectedWorkspaceLabel)}` : ""} ${th.fg("dim", formatDuration(delegationDurationMs(selected)))} ${th.fg("accent", formatDelegationCostLabel(selected))} ${th.fg("muted", formatDelegationContextLabel(selected))}`
252
253
  : th.fg("warning", "No delegation selected");
253
254
  const headerRight = `${th.fg(this.activePane === "feed" ? "accent" : "muted", this.activePane === "feed" ? "▶ Feed" : " Feed")} ${selectedTitle}`;
254
255
  lines.push(this.row(padToWidth(headerLeft, listWidth) + th.fg("dim", "│") + padToWidth(headerRight, logWidth), inner));
@@ -432,7 +433,8 @@ export class DelegationOverlayComponent implements Component {
432
433
  const badge = formatDelegationSignalBadge(delegationSignalBadge(run));
433
434
  const modelLabel = formatDelegationModelLabel(run);
434
435
  const modelSuffix = modelLabel ? ` (${modelLabel})` : "";
435
- const base = `${row.label}${badge ? ` ${badge}` : ""}${modelSuffix} ${duration} ${cost} ${context} [view]`;
436
+ const cwdSuffix = formatDelegationCwdLabel(run, this.repoRoot, 28);
437
+ const base = `${row.label}${badge ? ` ${badge}` : ""}${modelSuffix}${cwdSuffix ? ` ${cwdSuffix}` : ""} ${duration} ${cost} ${context} [view]`;
436
438
  const labeled = `${truncateToWidth(base, width - (selected ? 2 : 0), "…")}${delegateSelectMarker(run.id)}`;
437
439
  const colored = th.fg(statusColor(run.status), labeled);
438
440
  return selected ? th.bg("selectedBg", padToWidth(colored, width)) : colored;
@@ -39,6 +39,7 @@ import { extractModeIntent, stripModeIntentMarkup, validateModeIntent, type ZobM
39
39
  import { capturePlanArtifact } from "./plan-capture.js";
40
40
  import { redactPlanTodosBlockForDisplay } from "../domains/plan/plan-todos.js";
41
41
  import { applyMode, renderHarnessWidget } from "./widget.js";
42
+ import { createStopRestoreCandidate, markStopRestoreAssistantMessage, markStopRestoreToolVisible } from "./stop-restore.js";
42
43
 
43
44
  function safelyUpdateZobLivePeer(repoRoot: string, action: "register" | "touch" | "unregister"): void {
44
45
  try {
@@ -937,6 +938,15 @@ export function registerHarnessEvents(pi: ExtensionAPI, state: HarnessRuntimeSta
937
938
  return { action: "continue" as const };
938
939
  }
939
940
  state.lastUserInputText = event.text;
941
+ const streamingBehavior = (event as { streamingBehavior?: unknown }).streamingBehavior;
942
+ const stopRestoreCandidate = createStopRestoreCandidate({
943
+ text: event.text,
944
+ source: event.source,
945
+ streamingBehavior: streamingBehavior === "steer" || streamingBehavior === "followUp" ? streamingBehavior : undefined,
946
+ leafId: ctx.sessionManager.getLeafId(),
947
+ });
948
+ if (stopRestoreCandidate) state.stopRestoreCandidate = stopRestoreCandidate;
949
+ else if (!streamingBehavior) state.stopRestoreCandidate = undefined;
940
950
  if (!event.text.trim().startsWith("/") && state.autonomy.enabled) {
941
951
  const readiness = scoreMissionReadiness(event.text, { mode: state.autonomy.mode, policy: state.autonomy.policy });
942
952
  state.autonomy.lastReadiness = readiness;
@@ -1002,6 +1012,36 @@ export function registerHarnessEvents(pi: ExtensionAPI, state: HarnessRuntimeSta
1002
1012
  return { action: "continue" as const };
1003
1013
  });
1004
1014
 
1015
+ pi.on("message_start", async (event, ctx) => {
1016
+ if (!isRecord(event.message)) return undefined;
1017
+ if (event.message.role === "user") {
1018
+ const text = textFromMessage(event.message as AssistantLikeMessage);
1019
+ const stopRestoreCandidate = createStopRestoreCandidate({
1020
+ text,
1021
+ source: "interactive",
1022
+ leafId: ctx.sessionManager.getLeafId(),
1023
+ });
1024
+ if (stopRestoreCandidate) state.stopRestoreCandidate = stopRestoreCandidate;
1025
+ return undefined;
1026
+ }
1027
+ if (event.message.role === "assistant") {
1028
+ markStopRestoreAssistantMessage(state.stopRestoreCandidate, event.message as AssistantLikeMessage);
1029
+ }
1030
+ return undefined;
1031
+ });
1032
+
1033
+ pi.on("message_update", async (event) => {
1034
+ if (isRecord(event.message) && event.message.role === "assistant") {
1035
+ markStopRestoreAssistantMessage(state.stopRestoreCandidate, event.message as AssistantLikeMessage);
1036
+ }
1037
+ return undefined;
1038
+ });
1039
+
1040
+ pi.on("tool_execution_start", async () => {
1041
+ markStopRestoreToolVisible(state.stopRestoreCandidate);
1042
+ return undefined;
1043
+ });
1044
+
1005
1045
  pi.on("tool_call", async (event, ctx) => {
1006
1046
  if (state.activeMode === "vanilla") return { action: "continue" as const };
1007
1047
 
@@ -1072,6 +1112,7 @@ export function registerHarnessEvents(pi: ExtensionAPI, state: HarnessRuntimeSta
1072
1112
 
1073
1113
  pi.on("message_end", async (event, ctx) => {
1074
1114
  if (!isRecord(event.message) || event.message.role !== "assistant") return undefined;
1115
+ markStopRestoreAssistantMessage(state.stopRestoreCandidate, event.message as AssistantLikeMessage);
1075
1116
  const text = textFromMessage(event.message as AssistantLikeMessage);
1076
1117
  const visibleText = stripModeIntentMarkup(text);
1077
1118
  let capturedPlanPath: string | undefined;
@@ -45,6 +45,7 @@ const TaskItem = Type.Object({
45
45
  const DelegateParams = Type.Object({
46
46
  agent: Type.Optional(Type.String({ description: "Agent name for single-agent mode" })),
47
47
  task: Type.Optional(Type.String({ description: "Task for single-agent mode" })),
48
+ cwd: Type.Optional(Type.String({ description: "Default cwd for delegate_agent child Pi processes. Must stay inside repo; tasks[].cwd and chain[].cwd override this value." })),
48
49
  tasks: Type.Optional(Type.Array(TaskItem, { description: "Parallel tasks. Max 8, 4 concurrent." })),
49
50
  chain: Type.Optional(Type.Array(TaskItem, { description: "Sequential chain. {previous} is replaced by prior output." })),
50
51
  scope: Type.Optional(AgentScopeSchema),
@@ -19,6 +19,7 @@ import type { ZobModeIntent } from "./mode-intent.js";
19
19
  import type { ZAgentRoomBinding } from "../domains/coms/zagents.js";
20
20
  import { createZcompactRuntimeState, restoreZcompactStateFromBranch, type ZcompactRuntimeState } from "./auto-compaction.js";
21
21
  import { createZcommitRuntimeState, recordZcommitOwnedPath, type ZcommitLastCommitRecord, type ZcommitOwnershipSource, type ZcommitRuntimeState, type ZcommitToggleState } from "../domains/git/git-ops.js";
22
+ import type { StopRestoreCandidate } from "./stop-restore.js";
22
23
 
23
24
  export interface DelegationMouseRuntimeState {
24
25
  tui?: TUI;
@@ -153,6 +154,7 @@ export interface HarnessRuntimeState {
153
154
  runtimeGoalLastAccountedAtMs?: number;
154
155
  activeRuleResolution?: RuleResolution;
155
156
  lastUserInputText?: string;
157
+ stopRestoreCandidate?: StopRestoreCandidate;
156
158
  lastModeIntent?: ZobModeIntent & { at: number; accepted: boolean; validationReason: string };
157
159
  delegations: DelegationMonitorState;
158
160
  delegationMouse: DelegationMouseRuntimeState;
@@ -181,6 +183,7 @@ export function createHarnessRuntimeState(): HarnessRuntimeState {
181
183
  runtimeGoalLastAccountedAtMs: undefined,
182
184
  activeRuleResolution: undefined,
183
185
  lastUserInputText: undefined,
186
+ stopRestoreCandidate: undefined,
184
187
  lastModeIntent: undefined,
185
188
  delegations: createDelegationMonitorState(),
186
189
  delegationMouse: { enabled: false, opening: false, mouseReleaseEpoch: 0 },
@@ -0,0 +1,144 @@
1
+ import type { AssistantLikeMessage } from "../types.js";
2
+ import { sha256 } from "../core/utils/hashing.js";
3
+ import { isRecord } from "../core/utils/records.js";
4
+
5
+ export type StopRestoreInputSource = "interactive" | "rpc";
6
+ export type StopRestoreStreamingBehavior = "steer" | "followUp";
7
+
8
+ export interface StopRestoreCandidate {
9
+ schema: "zob.stop-restore-candidate.v1";
10
+ promptText: string;
11
+ promptHash: string;
12
+ source: StopRestoreInputSource;
13
+ inputAtMs: number;
14
+ leafId: string | null;
15
+ assistantStarted: boolean;
16
+ assistantVisibleOutput: boolean;
17
+ toolVisibleOutput: boolean;
18
+ restoredAtMs?: number;
19
+ }
20
+
21
+ export interface StopRestoreCandidateInput {
22
+ text: string;
23
+ source: string;
24
+ streamingBehavior?: StopRestoreStreamingBehavior;
25
+ leafId?: string | null;
26
+ nowMs?: number;
27
+ }
28
+
29
+ export interface StopRestoreDecisionInput {
30
+ foregroundAbortRequested: boolean;
31
+ idleBeforeStop: boolean;
32
+ pendingMessagesBeforeStop: boolean;
33
+ nowMs?: number;
34
+ maxCandidateAgeMs?: number;
35
+ }
36
+
37
+ export interface StopRestoreDecision {
38
+ restore: boolean;
39
+ reason: string;
40
+ promptText?: string;
41
+ promptHash?: string;
42
+ assistantOutputObserved: boolean;
43
+ }
44
+
45
+ export interface StopRestoreRewindResult {
46
+ attempted: boolean;
47
+ succeeded: boolean;
48
+ reason: string;
49
+ targetEntryId?: string;
50
+ }
51
+
52
+ const DEFAULT_MAX_CANDIDATE_AGE_MS = 30 * 60 * 1000;
53
+
54
+ function restorablePromptText(text: string): boolean {
55
+ const trimmed = text.trim();
56
+ return trimmed.length > 0 && !trimmed.startsWith("/");
57
+ }
58
+
59
+ export function createStopRestoreCandidate(input: StopRestoreCandidateInput): StopRestoreCandidate | undefined {
60
+ if (input.source !== "interactive" && input.source !== "rpc") return undefined;
61
+ if (input.streamingBehavior) return undefined;
62
+ if (!restorablePromptText(input.text)) return undefined;
63
+ return {
64
+ schema: "zob.stop-restore-candidate.v1",
65
+ promptText: input.text,
66
+ promptHash: sha256(input.text),
67
+ source: input.source,
68
+ inputAtMs: input.nowMs ?? Date.now(),
69
+ leafId: input.leafId ?? null,
70
+ assistantStarted: false,
71
+ assistantVisibleOutput: false,
72
+ toolVisibleOutput: false,
73
+ };
74
+ }
75
+
76
+ export function assistantMessageHasVisibleOutput(message: AssistantLikeMessage | undefined): boolean {
77
+ if (!message || !Array.isArray(message.content)) return false;
78
+ return message.content.some((part) => {
79
+ if (!isRecord(part)) return false;
80
+ if (part.type === "text") return typeof part.text === "string" && part.text.trim().length > 0;
81
+ if (part.type === "thinking") return typeof part.thinking === "string" && part.thinking.trim().length > 0;
82
+ if (part.type === "toolCall") return true;
83
+ return false;
84
+ });
85
+ }
86
+
87
+ export function markStopRestoreAssistantMessage(candidate: StopRestoreCandidate | undefined, message: AssistantLikeMessage | undefined): void {
88
+ if (!candidate || candidate.restoredAtMs) return;
89
+ candidate.assistantStarted = true;
90
+ if (assistantMessageHasVisibleOutput(message)) candidate.assistantVisibleOutput = true;
91
+ }
92
+
93
+ export function markStopRestoreToolVisible(candidate: StopRestoreCandidate | undefined): void {
94
+ if (!candidate || candidate.restoredAtMs) return;
95
+ candidate.toolVisibleOutput = true;
96
+ }
97
+
98
+ function textFromEntryContent(content: unknown): string {
99
+ if (typeof content === "string") return content;
100
+ if (!Array.isArray(content)) return "";
101
+ return content
102
+ .filter((part): part is { type: string; text: string } => isRecord(part) && part.type === "text" && typeof part.text === "string")
103
+ .map((part) => part.text)
104
+ .join("");
105
+ }
106
+
107
+ export function findStopRestoreUserEntryId(candidate: StopRestoreCandidate | undefined, entries: unknown[]): string | undefined {
108
+ if (!candidate) return undefined;
109
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
110
+ const entry = entries[index];
111
+ if (!isRecord(entry) || entry.type !== "message" || typeof entry.id !== "string") continue;
112
+ const parentId = typeof entry.parentId === "string" ? entry.parentId : entry.parentId === null ? null : undefined;
113
+ if (parentId !== candidate.leafId) continue;
114
+ if (!isRecord(entry.message) || entry.message.role !== "user") continue;
115
+ if (textFromEntryContent(entry.message.content) === candidate.promptText) return entry.id;
116
+ }
117
+ return undefined;
118
+ }
119
+
120
+ export function markStopRestoreRestored(candidate: StopRestoreCandidate | undefined, nowMs = Date.now()): void {
121
+ if (!candidate) return;
122
+ candidate.restoredAtMs = nowMs;
123
+ }
124
+
125
+ export function shouldRestoreStopPrompt(candidate: StopRestoreCandidate | undefined, input: StopRestoreDecisionInput): StopRestoreDecision {
126
+ const assistantOutputObserved = Boolean(candidate?.assistantVisibleOutput || candidate?.toolVisibleOutput);
127
+ if (!candidate) return { restore: false, reason: "no_candidate", assistantOutputObserved };
128
+ if (!input.foregroundAbortRequested) return { restore: false, reason: "foreground_idle", assistantOutputObserved };
129
+ if (input.idleBeforeStop) return { restore: false, reason: "idle_before_stop", assistantOutputObserved };
130
+ if (input.pendingMessagesBeforeStop) return { restore: false, reason: "pending_messages_restored_by_pi", assistantOutputObserved };
131
+ if (candidate.restoredAtMs) return { restore: false, reason: "already_restored", promptHash: candidate.promptHash, assistantOutputObserved };
132
+ if (!restorablePromptText(candidate.promptText)) return { restore: false, reason: "non_restorable_prompt", promptHash: candidate.promptHash, assistantOutputObserved };
133
+ if (assistantOutputObserved) return { restore: false, reason: "assistant_output_observed", promptHash: candidate.promptHash, assistantOutputObserved };
134
+ const nowMs = input.nowMs ?? Date.now();
135
+ const maxAgeMs = input.maxCandidateAgeMs ?? DEFAULT_MAX_CANDIDATE_AGE_MS;
136
+ if (nowMs - candidate.inputAtMs > maxAgeMs) return { restore: false, reason: "candidate_stale", promptHash: candidate.promptHash, assistantOutputObserved };
137
+ return {
138
+ restore: true,
139
+ reason: "foreground_aborted_before_assistant_output",
140
+ promptText: candidate.promptText,
141
+ promptHash: candidate.promptHash,
142
+ assistantOutputObserved,
143
+ };
144
+ }
@@ -32,7 +32,9 @@ import {
32
32
  delegationSignalColor,
33
33
  extractDelegationSignalBadge,
34
34
  finishDelegationRun,
35
+ formatDelegationCwdLabel,
35
36
  formatDelegationModelLabel,
37
+ formatDelegationWorkspaceLabel,
36
38
  formatDelegationSignalBadge,
37
39
  formatDuration,
38
40
  hasActiveDelegations,
@@ -625,6 +627,7 @@ export function hydrateDelegationRunsFromDetails(source: "delegate_agent" | "del
625
627
  agent: result.agent,
626
628
  task: result.task || "restored from delegate tool result",
627
629
  startedAtMs: nowMs - Math.max(1, index + 1),
630
+ cwd: result.cwd,
628
631
  });
629
632
  }
630
633
  finishDelegationRun(state.delegations, result.ledgerRunId, {
@@ -634,6 +637,7 @@ export function hydrateDelegationRunsFromDetails(source: "delegate_agent" | "del
634
637
  index,
635
638
  agent: result.agent,
636
639
  model: result.model,
640
+ cwd: result.cwd,
637
641
  status: result.stopReason === "aborted" ? "aborted" : isFailed(result) ? "failed" : "complete",
638
642
  endedAtMs: existing?.endedAtMs ?? nowMs,
639
643
  outputPreview: result.output,
@@ -730,6 +734,16 @@ export function formatDelegationCatalogSummary(catalog: Record<string, unknown>)
730
734
  return lines.join("\n");
731
735
  }
732
736
 
737
+ function formatWorkspaceSummaryLine(items: Array<DelegationRunView | ChildResult>, repoRoot: string): string {
738
+ const workspaces = [...new Set(items.map((item) => formatDelegationWorkspaceLabel(item, repoRoot, 72)).filter(Boolean))];
739
+ if (workspaces.length === 0) return "";
740
+ if (workspaces.length === 1) return workspaces[0] ?? "";
741
+ const values = workspaces.map((label) => label.replace(/^workspace · /, ""));
742
+ const visible = values.slice(0, 3).join(", ");
743
+ const suffix = values.length > 3 ? `, +${values.length - 3} more` : "";
744
+ return `workspaces · ${visible}${suffix}`;
745
+ }
746
+
733
747
  function renderMiniActivityLines(run: DelegationRunView, nowMs: number, expanded: boolean, theme: { fg: (color: any, text: string) => string }): string[] {
734
748
  const lines: string[] = [];
735
749
  const snapshot = readDelegationActivitySnapshot(run.sessionPath, process.cwd(), nowMs);
@@ -770,8 +784,10 @@ export function renderDelegationToolResultText(source: "delegate_agent" | "deleg
770
784
  const mode = details?.mode ?? (source === "delegate_task" ? "single" : "single");
771
785
  const lines = [
772
786
  `${theme.fg("toolTitle", theme.bold(source))} ${theme.fg("accent", mode)} ${isPartial ? theme.fg("warning", "running") : theme.fg(failCount > 0 ? "error" : "success", `${okCount}/${results.length || monitoredRuns.length || 1} ok`)}`,
773
- `${theme.fg("dim", "running")} ${theme.fg(runningCount > 0 ? "warning" : "dim", String(runningCount))} ${theme.fg("dim", "blocked")} ${theme.fg(blockedCount > 0 ? "warning" : "dim", String(blockedCount))} ${theme.fg("dim", "gate")} ${theme.fg(gateCount > 0 ? "warning" : "dim", String(gateCount))} ${theme.fg("dim", "runtime")} ${theme.fg(runtimeCount > 0 ? "error" : "dim", String(runtimeCount))} ${theme.fg("dim", "details")} ${theme.fg("muted", "/zstatus delegations")}`,
774
787
  ];
788
+ const workspaceSummary = formatWorkspaceSummaryLine(monitoredRuns.length > 0 ? monitoredRuns : results, process.cwd());
789
+ if (workspaceSummary) lines.push(theme.fg("accent", workspaceSummary));
790
+ lines.push(`${theme.fg("dim", "running")} ${theme.fg(runningCount > 0 ? "warning" : "dim", String(runningCount))} ${theme.fg("dim", "blocked")} ${theme.fg(blockedCount > 0 ? "warning" : "dim", String(blockedCount))} ${theme.fg("dim", "gate")} ${theme.fg(gateCount > 0 ? "warning" : "dim", String(gateCount))} ${theme.fg("dim", "runtime")} ${theme.fg(runtimeCount > 0 ? "error" : "dim", String(runtimeCount))} ${theme.fg("dim", "details")} ${theme.fg("muted", "/zstatus delegations")}`);
775
791
 
776
792
  if (monitoredRuns.length > 0) {
777
793
  const maxRows = expanded ? 12 : 5;
@@ -787,7 +803,9 @@ export function renderDelegationToolResultText(source: "delegate_agent" | "deleg
787
803
  const badgeText = formatDelegationSignalBadge(badge);
788
804
  const modelLabel = formatDelegationModelLabel(run);
789
805
  const modelSuffix = modelLabel ? ` ${theme.fg("muted", `(${modelLabel})`)}` : "";
790
- lines.push(`${theme.fg("dim", prefix)} ${theme.fg(color, `${statusIcon(run.status)} ${run.agent}${kind}${background}`)}${badgeText ? ` ${theme.fg(delegationSignalColor(badge), badgeText)}` : ""}${modelSuffix} ${theme.fg("dim", formatDuration(delegationDurationMs(run, nowMs)))} ${theme.fg("muted", viewHint)}`);
806
+ const cwdLabel = formatDelegationCwdLabel(run, process.cwd(), 36);
807
+ const cwdSuffix = cwdLabel ? ` ${theme.fg("muted", cwdLabel)}` : "";
808
+ lines.push(`${theme.fg("dim", prefix)} ${theme.fg(color, `${statusIcon(run.status)} ${run.agent}${kind}${background}`)}${badgeText ? ` ${theme.fg(delegationSignalColor(badge), badgeText)}` : ""}${modelSuffix}${cwdSuffix} ${theme.fg("dim", formatDuration(delegationDurationMs(run, nowMs)))} ${theme.fg("muted", viewHint)}`);
791
809
  lines.push(...renderMiniActivityLines(run, nowMs, expanded, theme));
792
810
  if (expanded && (run.errorMessage || run.stopReason || run.gateErrors?.length)) lines.push(` ${theme.fg("dim", run.errorMessage ?? run.gateErrors?.join("; ") ?? run.stopReason ?? "")}`);
793
811
  }
@@ -808,7 +826,9 @@ export function renderDelegationToolResultText(source: "delegate_agent" | "deleg
808
826
  const badgeText = formatDelegationSignalBadge(badge);
809
827
  const modelLabel = formatDelegationModelLabel(result);
810
828
  const modelSuffix = modelLabel ? ` ${theme.fg("muted", `(${modelLabel})`)}` : "";
811
- lines.push(`${theme.fg("dim", prefix)} ${theme.fg(color, `${failed ? "✗" : "✓"} ${result.agent}${kind}`)}${badgeText ? ` ${theme.fg(delegationSignalColor(badge), badgeText)}` : ""}${modelSuffix} ${theme.fg("dim", result.ledgerRunId ?? "")} ${theme.fg("muted", viewHint)}`);
829
+ const cwdLabel = formatDelegationCwdLabel(result, process.cwd(), 36);
830
+ const cwdSuffix = cwdLabel ? ` ${theme.fg("muted", cwdLabel)}` : "";
831
+ lines.push(`${theme.fg("dim", prefix)} ${theme.fg(color, `${failed ? "✗" : "✓"} ${result.agent}${kind}`)}${badgeText ? ` ${theme.fg(delegationSignalColor(badge), badgeText)}` : ""}${modelSuffix}${cwdSuffix} ${theme.fg("dim", result.ledgerRunId ?? "")} ${theme.fg("muted", viewHint)}`);
812
832
  if (expanded && result.output.trim()) lines.push(` ${theme.fg("muted", result.output.split("\n")[0] ?? "")}`);
813
833
  }
814
834
  if (hasMore) lines.push(theme.fg("dim", `└─ … ${results.length - maxRows} more result(s)`));
@@ -32,6 +32,7 @@ import {
32
32
  delegationSignalColor,
33
33
  extractDelegationSignalBadge,
34
34
  finishDelegationRun,
35
+ formatDelegationCwdLabel,
35
36
  formatDelegationModelLabel,
36
37
  formatDelegationSignalBadge,
37
38
  formatDuration,
@@ -109,6 +110,8 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
109
110
  "If agent routing is uncertain, call zob_delegation_catalog before the first delegation.",
110
111
  "Use delegate_agent for broad discovery, external research, skeptical review, or independent QA before making risky edits.",
111
112
  "When using delegate_agent, give each child a bounded six-part contract and a concrete final output shape.",
113
+ "Optional cwd spawns the child inside that repo-local working directory; tasks[].cwd and chain[].cwd override delegate_agent top-level cwd defaults.",
114
+ "cwd only selects the child working directory; it does not replace allowed_paths or write-scope grants.",
112
115
  "If effective tools include edit/write, provide non-empty repo-relative-only allowed_paths; use repo-local reports/... snapshot/context_ref refs for external context.",
113
116
  ],
114
117
  parameters: DelegateParams,
@@ -169,6 +172,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
169
172
  const startedAt = new Date(startedAtMs).toISOString();
170
173
  const requestedTools = parseToolList(params.tools);
171
174
  const effectiveThinking = item.thinking ?? params.thinking;
175
+ const cwdResult = resolveChildCwd(ctx.cwd, item.cwd ?? params.cwd);
172
176
  startDelegationRun(state.delegations, {
173
177
  id: runId,
174
178
  parentToolCallId: toolCallId,
@@ -178,6 +182,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
178
182
  agent: item.agent,
179
183
  task: taskText,
180
184
  startedAtMs,
185
+ cwd: cwdResult.cwd,
181
186
  });
182
187
  renderDelegationMonitor();
183
188
  const agent = byName.get(item.agent.toLowerCase());
@@ -193,6 +198,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
193
198
  gateErrors: ["unknown agent"],
194
199
  failureKind: "config",
195
200
  usage: usageEmpty(),
201
+ cwd: cwdResult.cwd,
196
202
  };
197
203
  const endedAtMs = Date.now();
198
204
  const endedAt = new Date(endedAtMs).toISOString();
@@ -203,7 +209,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
203
209
  ...delegationLedgerMeta("delegate_agent", toolCallId, monitor.mode, monitor.index),
204
210
  agent: item.agent,
205
211
  taskHash: sha256(taskText),
206
- cwd: resolveChildCwd(ctx.cwd, item.cwd).cwd,
212
+ cwd: cwdResult.cwd,
207
213
  tools: requestedTools ?? [],
208
214
  errors: ["unknown agent"],
209
215
  failureKind: result.failureKind,
@@ -216,7 +222,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
216
222
  mode: state.activeMode,
217
223
  agent: item.agent,
218
224
  model: params.model,
219
- cwd: resolveChildCwd(ctx.cwd, item.cwd).cwd,
225
+ cwd: cwdResult.cwd,
220
226
  tools: requestedTools ?? [],
221
227
  taskHash: sha256(taskText),
222
228
  outputContract: inferOutputContract(item.agent),
@@ -240,14 +246,14 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
240
246
  failureKind: result.failureKind,
241
247
  errorMessage: "Configuration blocked; no child launched: unknown agent",
242
248
  model: params.model,
249
+ cwd: cwdResult.cwd,
243
250
  });
244
251
  recordTodoClaimFromChildResult(pi, state, effectiveChildGoal, result);
245
252
  renderDelegationMonitor();
246
253
  return result;
247
254
  }
248
255
 
249
- updateDelegationRun(state.delegations, runId, { agent: agent.name });
250
- const cwdResult = resolveChildCwd(ctx.cwd, item.cwd);
256
+ updateDelegationRun(state.delegations, runId, { agent: agent.name, cwd: cwdResult.cwd });
251
257
  const effectiveTools = requestedTools ?? agent.tools ?? [];
252
258
  const preflightErrors = [
253
259
  ...strictGoalErrors(state),
@@ -275,6 +281,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
275
281
  gateErrors: preflightErrors,
276
282
  failureKind: classifyConfigOrPreflight(preflightErrors),
277
283
  usage: usageEmpty(),
284
+ cwd: cwdResult.cwd,
278
285
  };
279
286
  const endedAtMs = Date.now();
280
287
  const endedAt = new Date(endedAtMs).toISOString();
@@ -321,6 +328,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
321
328
  failureKind: result.failureKind,
322
329
  errorMessage: `${result.failureKind === "config" ? "Configuration blocked" : "Preflight blocked"}; no child launched: ${preflightErrors.join("; ")}`,
323
330
  model: params.model ?? agent.model,
331
+ cwd: cwdResult.cwd,
324
332
  });
325
333
  recordTodoClaimFromChildResult(pi, state, effectiveChildGoal, result);
326
334
  renderDelegationMonitor();
@@ -349,6 +357,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
349
357
  const childPathPolicy = { allowedPaths: params.allowed_paths, forbiddenPaths: params.forbidden_paths };
350
358
  const beforeChildDirty = toolsEnableWrites(effectiveChildTools) ? captureZcommitChildDirtySnapshot(ctx.cwd, childPathPolicy) : undefined;
351
359
  const result = await runChildAgent(ctx, agent, taskText, cwdResult.cwd, signal, params.model, requestedTools?.join(","), (partial) => {
360
+ partial.cwd = cwdResult.cwd;
352
361
  updateDelegationRun(state.delegations, runId, {
353
362
  status: partial.stopReason === "aborted" ? "aborted" : "running",
354
363
  agent: partial.agent,
@@ -356,6 +365,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
356
365
  outputPreview: partial.output,
357
366
  stderrPreview: partial.stderr,
358
367
  sessionPath: partial.sessionPath,
368
+ cwd: cwdResult.cwd,
359
369
  stopReason: partial.stopReason,
360
370
  errorMessage: partial.errorMessage,
361
371
  usage: partial.usage,
@@ -363,6 +373,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
363
373
  renderDelegationMonitor();
364
374
  update?.(partial);
365
375
  }, childPathPolicy, effectiveThinking);
376
+ result.cwd = cwdResult.cwd;
366
377
  result.childChangedPaths = captureChildDirtyDelta(ctx.cwd, childPathPolicy, beforeChildDirty);
367
378
  result.ledgerRunId = runId;
368
379
  result.outputContract = outputContract;
@@ -480,6 +491,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
480
491
  childChangedPaths: result.childChangedPaths,
481
492
  usage: result.usage,
482
493
  model: result.model,
494
+ cwd: cwdResult.cwd,
483
495
  });
484
496
  if (shouldRunAgenticClaimValidation(effectiveChildGoal, claimRecord)) {
485
497
  await runAgenticTodoClaimValidation({ ctx, pi, state, childGoal: effectiveChildGoal, claimRecord, parentRunId: runId, appendDelegationLedger, signal, modelOverride: params.model, allowedPaths: params.allowed_paths, forbiddenPaths: params.forbidden_paths });
@@ -491,7 +503,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
491
503
  if (params.agent && params.task) {
492
504
  startMonitorTicker();
493
505
  try {
494
- const result = await runOne({ agent: params.agent, task: params.task, thinking: params.thinking, child_goal: params.child_goal }, { mode: "single", index: 0 }, (partial) => {
506
+ const result = await runOne({ agent: params.agent, task: params.task, cwd: params.cwd, thinking: params.thinking, child_goal: params.child_goal }, { mode: "single", index: 0 }, (partial) => {
495
507
  onUpdate?.({ content: [{ type: "text", text: partial.output || partial.stderr || "running..." }], details: makeDetails("single", [partial]) });
496
508
  });
497
509
  return {
@@ -519,7 +531,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
519
531
  return result;
520
532
  });
521
533
  const successCount = results.filter((result) => !isFailed(result)).length;
522
- const summaries = results.map((result) => `### ${result.agent} — ${isFailed(result) ? "FAILED/INCOMPLETE" : "OK"}\n\n${capOutput(formatChildResultText(result))}`);
534
+ const summaries = results.map((result) => `### ${result.agent} — ${isFailed(result) ? "FAILED/INCOMPLETE" : "OK"}${formatDelegationCwdLabel(result, ctx.cwd) ? ` · ${formatDelegationCwdLabel(result, ctx.cwd)}` : ""}\n\n${capOutput(formatChildResultText(result))}`);
523
535
  return {
524
536
  content: [{ type: "text", text: `Parallel delegation: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n---\n\n")}` }],
525
537
  details: makeDetails("parallel", results),
@@ -542,7 +554,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
542
554
  results.push(result);
543
555
  if (isFailed(result)) {
544
556
  return {
545
- content: [{ type: "text", text: `Chain stopped at step ${index + 1} (${result.agent}):\n\n${formatChildResultText(result)}` }],
557
+ content: [{ type: "text", text: `Chain stopped at step ${index + 1} (${result.agent}${formatDelegationCwdLabel(result, ctx.cwd) ? ` · ${formatDelegationCwdLabel(result, ctx.cwd)}` : ""}):\n\n${formatChildResultText(result)}` }],
546
558
  details: makeDetails("chain", results),
547
559
  };
548
560
  }
@@ -572,6 +584,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
572
584
  "Use delegate_task when you need strict preflight rather than a freeform delegated prompt.",
573
585
  "Normally omit output_contract; the harness infers it from the selected agent. Do not invent output contract ids.",
574
586
  "Normally omit required_tools; the harness infers the selected agent's declared tools. Only set required_tools to intentionally narrow tools.",
587
+ "Optional cwd spawns the child inside that repo-local working directory; it only selects cwd and does not replace allowed_paths or write-scope grants.",
575
588
  "If effective tools include edit/write, set top-level original_user_ask to the original human request; context or task text does not satisfy the strict write preflight gate.",
576
589
  "Use canonical JSON keys expected_outcome, must_do, must_not_do, context, original_user_ask, allowed_paths, forbidden_paths; safe aliases are accepted only when non-conflicting.",
577
590
  "Accepted aliases: expectedOutcome, mustDo, mustNotDo/must_not/mustNot, originalUserAsk, allowedPaths, forbiddenPaths, requiredTools, outputContract, runInBackground, childGoal, loadSkills.",
@@ -598,6 +611,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
598
611
  const effectiveChildGoal = childGoalResolution.childGoal;
599
612
  const startedAtMs = Date.now();
600
613
  const startedAt = new Date(startedAtMs).toISOString();
614
+ const cwdResult = resolveChildCwd(ctx.cwd, params.cwd);
601
615
  const appendDelegationLedger = (entry: Record<string, unknown>): void => {
602
616
  appendLedgerFile(ctx.cwd, entry);
603
617
  pi.appendEntry("zob-delegation", entry);
@@ -635,6 +649,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
635
649
  agent: params.agent,
636
650
  task: params.task,
637
651
  startedAtMs,
652
+ cwd: cwdResult.cwd,
638
653
  });
639
654
  renderDelegationMonitor();
640
655
  startMonitorTicker();
@@ -652,6 +667,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
652
667
  gateErrors: ["unknown agent"],
653
668
  failureKind: "config",
654
669
  usage: usageEmpty(),
670
+ cwd: cwdResult.cwd,
655
671
  };
656
672
  const endedAtMs = Date.now();
657
673
  const endedAt = new Date(endedAtMs).toISOString();
@@ -662,7 +678,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
662
678
  ...delegationLedgerMeta("delegate_task", toolCallId, "single", 0),
663
679
  agent: params.agent,
664
680
  taskHash: sha256(params.task),
665
- cwd: resolveChildCwd(ctx.cwd, params.cwd).cwd,
681
+ cwd: cwdResult.cwd,
666
682
  tools: params.required_tools ?? [],
667
683
  errors: ["unknown agent"],
668
684
  failureKind: result.failureKind,
@@ -675,7 +691,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
675
691
  mode: state.activeMode,
676
692
  agent: params.agent,
677
693
  model: params.model,
678
- cwd: resolveChildCwd(ctx.cwd, params.cwd).cwd,
694
+ cwd: cwdResult.cwd,
679
695
  tools: params.required_tools ?? [],
680
696
  taskHash: sha256(params.task),
681
697
  outputContract: params.output_contract ?? inferOutputContract(params.agent),
@@ -699,13 +715,14 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
699
715
  failureKind: result.failureKind,
700
716
  errorMessage: "Configuration blocked; no child launched: unknown agent",
701
717
  model: params.model,
718
+ cwd: cwdResult.cwd,
702
719
  });
703
720
  recordTodoClaimFromChildResult(pi, state, effectiveChildGoal, result);
704
721
  stopMonitorTicker();
705
722
  return { content: [{ type: "text", text: formatChildResultText(result) }], details: { mode: "single", results: [result], agents: agents.map((candidate) => candidate.name) } };
706
723
  }
707
724
 
708
- updateDelegationRun(state.delegations, runId, { agent: agent.name });
725
+ updateDelegationRun(state.delegations, runId, { agent: agent.name, cwd: cwdResult.cwd });
709
726
  const requestedOutputContract = params.output_contract ?? inferOutputContract(agent.name);
710
727
  const effectiveTools = params.required_tools?.length ? params.required_tools : agent.tools ?? [];
711
728
 
@@ -728,7 +745,6 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
728
745
  .filter((part): part is string => typeof part === "string")
729
746
  .join("\n");
730
747
 
731
- const cwdResult = resolveChildCwd(ctx.cwd, params.cwd);
732
748
  const preflightErrors = [
733
749
  ...normalized.errors,
734
750
  ...strictGoalErrors(state),
@@ -759,6 +775,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
759
775
  gateErrors: preflightErrors,
760
776
  failureKind: classifyConfigOrPreflight(preflightErrors),
761
777
  usage: usageEmpty(),
778
+ cwd: cwdResult.cwd,
762
779
  };
763
780
  const endedAtMs = Date.now();
764
781
  const endedAt = new Date(endedAtMs).toISOString();
@@ -805,6 +822,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
805
822
  failureKind: result.failureKind,
806
823
  errorMessage: delegateTaskPreflightHelp(preflightErrors).replace(/\n/g, " "),
807
824
  model: params.model ?? agent.model,
825
+ cwd: cwdResult.cwd,
808
826
  });
809
827
  recordTodoClaimFromChildResult(pi, state, effectiveChildGoal, result);
810
828
  stopMonitorTicker();
@@ -834,6 +852,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
834
852
  const childPathPolicy = { allowedPaths: params.allowed_paths, forbiddenPaths: params.forbidden_paths };
835
853
  const beforeChildDirty = toolsEnableWrites(effectiveTools) ? captureZcommitChildDirtySnapshot(ctx.cwd, childPathPolicy) : undefined;
836
854
  const result = await runChildAgent(ctx, agent, structuredTask, cwdResult.cwd, childSignal, params.model, effectiveTools.join(","), (partial) => {
855
+ partial.cwd = cwdResult.cwd;
837
856
  updateDelegationRun(state.delegations, runId, {
838
857
  status: partial.stopReason === "aborted" ? "aborted" : "running",
839
858
  agent: partial.agent,
@@ -841,6 +860,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
841
860
  outputPreview: partial.output,
842
861
  stderrPreview: partial.stderr,
843
862
  sessionPath: partial.sessionPath,
863
+ cwd: cwdResult.cwd,
844
864
  stopReason: partial.stopReason,
845
865
  errorMessage: partial.errorMessage,
846
866
  usage: partial.usage,
@@ -848,6 +868,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
848
868
  renderDelegationMonitor();
849
869
  if (emitToolUpdates) onUpdate?.({ content: [{ type: "text", text: partial.output || partial.stderr || "running..." }], details: { mode: "single", results: [partial], agents: agents.map((candidate) => candidate.name) } });
850
870
  }, childPathPolicy, params.thinking);
871
+ result.cwd = cwdResult.cwd;
851
872
  result.childChangedPaths = captureChildDirtyDelta(ctx.cwd, childPathPolicy, beforeChildDirty);
852
873
  result.ledgerRunId = runId;
853
874
  result.outputContract = outputContract;
@@ -966,6 +987,7 @@ export function registerDelegationTools(pi: ExtensionAPI, state: HarnessRuntimeS
966
987
  childChangedPaths: result.childChangedPaths,
967
988
  usage: result.usage,
968
989
  model: result.model,
990
+ cwd: cwdResult.cwd,
969
991
  });
970
992
  if (shouldRunAgenticClaimValidation(effectiveChildGoal, claimRecord)) {
971
993
  await runAgenticTodoClaimValidation({ ctx, pi, state, childGoal: effectiveChildGoal, claimRecord, parentRunId: runId, appendDelegationLedger, signal: childSignal, modelOverride: params.model, allowedPaths: params.allowed_paths, forbiddenPaths: params.forbidden_paths });
@@ -1092,6 +1114,7 @@ ${formatChildResultText(result)}` : result.output || "(no output)" }],
1092
1114
  failureKind: result.failureKind,
1093
1115
  stopReason: result.stopReason,
1094
1116
  stopCondition: result.stopCondition,
1117
+ cwd: result.cwd,
1095
1118
  sessionPath: result.sessionPath,
1096
1119
  });
1097
1120
  const resultDetails = (result: ChildResult) => includeResult ? { result } : { resultSummary: compactResult(result), resultIncluded: false };
@@ -81,6 +81,7 @@ export interface ChildResult {
81
81
  exitCode: number;
82
82
  output: string;
83
83
  stderr: string;
84
+ cwd?: string;
84
85
  model?: string;
85
86
  sessionPath?: string;
86
87
  ledgerRunId?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zob-harness",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
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",