ultimate-pi 0.9.1 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/.agents/skills/harness-decisions/SKILL.md +17 -13
  2. package/.agents/skills/harness-plan/SKILL.md +3 -3
  3. package/.pi/agents/harness/planner.md +8 -4
  4. package/.pi/extensions/harness-live-widget.ts +48 -28
  5. package/.pi/extensions/harness-plan-approval.ts +174 -0
  6. package/.pi/extensions/harness-run-context.ts +38 -8
  7. package/.pi/extensions/lib/harness-subagents/harness-subagent-policy.ts +11 -1
  8. package/.pi/extensions/lib/harness-subagents/parent-ask-user-bridge.ts +8 -87
  9. package/.pi/extensions/lib/harness-subagents/parent-harness-ui-bridge.ts +310 -0
  10. package/.pi/extensions/lib/harness-subagents/parent-harness-ui-hooks.ts +59 -0
  11. package/.pi/extensions/lib/harness-subagents/spawn-policy.ts +9 -0
  12. package/.pi/extensions/lib/harness-subagents/vendored/agent-manager.ts +4 -0
  13. package/.pi/extensions/lib/harness-subagents/vendored/agent-runner.ts +39 -12
  14. package/.pi/extensions/lib/harness-subagents/vendored/index.ts +38 -12
  15. package/.pi/extensions/lib/harness-subagents/vendored/ui/agent-widget.ts +2 -0
  16. package/.pi/extensions/lib/plan-approval/create-plan.ts +131 -0
  17. package/.pi/extensions/lib/plan-approval/dialog.ts +291 -0
  18. package/.pi/extensions/lib/plan-approval/fallback.ts +50 -0
  19. package/.pi/extensions/lib/plan-approval/format-plan.ts +94 -0
  20. package/.pi/extensions/lib/plan-approval/render.ts +83 -0
  21. package/.pi/extensions/lib/plan-approval/schema.ts +39 -0
  22. package/.pi/extensions/lib/plan-approval/types.ts +32 -0
  23. package/.pi/extensions/lib/plan-approval/validate.ts +61 -0
  24. package/.pi/lib/harness-run-context.ts +117 -28
  25. package/.pi/prompts/harness-plan.md +20 -7
  26. package/CHANGELOG.md +12 -0
  27. package/package.json +3 -3
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Registers ask_user and approve_plan in subagent sessions, delegating UI to the parent harness session.
3
+ */
4
+
5
+ import type {
6
+ ExtensionAPI,
7
+ ExtensionContext,
8
+ } from "@earendil-works/pi-coding-agent";
9
+ import { Text } from "@earendil-works/pi-tui";
10
+ import { Type } from "@sinclair/typebox";
11
+ import type {
12
+ PlanPacketLike,
13
+ PlanUserApproval,
14
+ } from "../../../lib/harness-run-context.js";
15
+ import { parsePlanApprovalFromMessage } from "../../../lib/harness-run-context.js";
16
+ import { runAskDialog } from "../ask-user/dialog.js";
17
+ import { runAskFallback } from "../ask-user/fallback.js";
18
+ import { renderAskCall, renderAskResult } from "../ask-user/render.js";
19
+ import {
20
+ PROMPT_GUIDELINES as ASK_PROMPT_GUIDELINES,
21
+ PROMPT_SNIPPET as ASK_PROMPT_SNIPPET,
22
+ AskUserParamsSchema,
23
+ } from "../ask-user/schema.js";
24
+ import type { AskUserParams, DialogResult } from "../ask-user/types.js";
25
+ import {
26
+ formatResultText,
27
+ toToolDetails,
28
+ validateAskParams,
29
+ } from "../ask-user/validate.js";
30
+ import {
31
+ CREATE_PLAN_GUIDELINES,
32
+ CREATE_PLAN_SNIPPET,
33
+ executeCreatePlan,
34
+ formatCreatePlanResultText,
35
+ } from "../plan-approval/create-plan.js";
36
+ import { runPlanApprovalDialog } from "../plan-approval/dialog.js";
37
+ import { runPlanApprovalFallback } from "../plan-approval/fallback.js";
38
+ import {
39
+ renderApprovePlanCall,
40
+ renderApprovePlanResult,
41
+ } from "../plan-approval/render.js";
42
+ import {
43
+ ApprovePlanParamsSchema,
44
+ PROMPT_GUIDELINES as PLAN_PROMPT_GUIDELINES,
45
+ PROMPT_SNIPPET as PLAN_PROMPT_SNIPPET,
46
+ } from "../plan-approval/schema.js";
47
+ import type { ApprovePlanParams } from "../plan-approval/types.js";
48
+ import {
49
+ formatApprovePlanResultText,
50
+ toApprovePlanToolDetails,
51
+ validateApprovePlanParams,
52
+ } from "../plan-approval/validate.js";
53
+
54
+ const HARNESS_UI_AGENT_TYPES = new Set([
55
+ "harness/planner",
56
+ "harness/evaluator",
57
+ "harness/adversary",
58
+ "harness/tie-breaker",
59
+ ]);
60
+
61
+ export interface ParentHarnessUiHooks {
62
+ projectRoot?: string;
63
+ getParentEntries?: () => unknown[];
64
+ getParentRunContext?: () =>
65
+ | import("../../../lib/harness-run-context.js").HarnessRunContext
66
+ | null;
67
+ onPlanApproval?: (approval: PlanUserApproval) => void;
68
+ appendPlanDraft?: (draft: {
69
+ plan_packet: PlanPacketLike;
70
+ human_summary?: string;
71
+ }) => void;
72
+ onPlanCommitted?: (
73
+ runCtx: import("../../../lib/harness-run-context.js").HarnessRunContext,
74
+ packet: PlanPacketLike,
75
+ planPath: string,
76
+ ) => void;
77
+ }
78
+
79
+ const CreatePlanParamsSchema = Type.Object({
80
+ plan_packet: Type.Object(
81
+ {},
82
+ {
83
+ description:
84
+ "Approved PlanPacket to persist (same object as approve_plan).",
85
+ },
86
+ ),
87
+ });
88
+
89
+ const PLANNER_ONLY_AGENT = "harness/planner";
90
+
91
+ export function agentTypeAllowsParentHarnessUi(agentType: string): boolean {
92
+ return HARNESS_UI_AGENT_TYPES.has(agentType);
93
+ }
94
+
95
+ /** @deprecated Use agentTypeAllowsParentHarnessUi */
96
+ export function agentTypeAllowsParentAskUser(agentType: string): boolean {
97
+ return agentTypeAllowsParentHarnessUi(agentType);
98
+ }
99
+
100
+ function notifyPlanApproval(
101
+ hooks: ParentHarnessUiHooks | undefined,
102
+ details: unknown,
103
+ toolName: "ask_user" | "approve_plan",
104
+ ): void {
105
+ if (!hooks?.onPlanApproval) return;
106
+ const approval = parsePlanApprovalFromMessage({ toolName, details });
107
+ if (approval) hooks.onPlanApproval(approval);
108
+ }
109
+
110
+ export function createParentHarnessUiBridgeFactory(
111
+ parentCtx: ExtensionContext,
112
+ agentType: string,
113
+ hooks?: ParentHarnessUiHooks,
114
+ ): ((pi: ExtensionAPI) => void) | null {
115
+ if (!agentTypeAllowsParentHarnessUi(agentType)) {
116
+ return null;
117
+ }
118
+ return (pi: ExtensionAPI) => {
119
+ pi.registerTool({
120
+ name: "ask_user",
121
+ label: "Ask User",
122
+ description:
123
+ "Ask the user a structured question (parent session UI). Use for clarification — not final plan approval (use approve_plan).",
124
+ promptSnippet: ASK_PROMPT_SNIPPET,
125
+ promptGuidelines: ASK_PROMPT_GUIDELINES,
126
+ parameters: AskUserParamsSchema,
127
+ async execute(_toolCallId, params, _signal, _onUpdate) {
128
+ const validated = validateAskParams(params as AskUserParams);
129
+ if (typeof validated === "string") {
130
+ return {
131
+ content: [{ type: "text", text: validated }],
132
+ details: {
133
+ question: params.question ?? "",
134
+ options: [],
135
+ response: null,
136
+ cancelled: true,
137
+ },
138
+ };
139
+ }
140
+ let outcome: DialogResult;
141
+ if (parentCtx.hasUI) {
142
+ outcome = await runAskDialog(parentCtx.ui, validated);
143
+ } else {
144
+ outcome = await runAskFallback(parentCtx.ui, validated);
145
+ }
146
+ const details = toToolDetails(
147
+ validated,
148
+ outcome.response,
149
+ outcome.cancelled,
150
+ );
151
+ notifyPlanApproval(hooks, details, "ask_user");
152
+ const text = formatResultText(outcome.response, outcome.cancelled);
153
+ return {
154
+ content: [{ type: "text", text }],
155
+ details,
156
+ };
157
+ },
158
+ renderCall(args, theme) {
159
+ return renderAskCall(args, theme);
160
+ },
161
+ renderResult(result, options, theme) {
162
+ return renderAskResult(result, options, theme);
163
+ },
164
+ });
165
+
166
+ if (agentType !== PLANNER_ONLY_AGENT) {
167
+ return;
168
+ }
169
+
170
+ pi.registerTool({
171
+ name: "approve_plan",
172
+ label: "Approve Plan",
173
+ description:
174
+ "Present the full PlanPacket for user approval in the parent TUI (scrollable overlay).",
175
+ promptSnippet: PLAN_PROMPT_SNIPPET,
176
+ promptGuidelines: PLAN_PROMPT_GUIDELINES,
177
+ parameters: ApprovePlanParamsSchema,
178
+ async execute(_toolCallId, params, _signal, _onUpdate) {
179
+ const validated = validateApprovePlanParams(
180
+ params as ApprovePlanParams,
181
+ );
182
+ if (typeof validated === "string") {
183
+ return {
184
+ content: [{ type: "text", text: validated }],
185
+ details: {
186
+ plan_packet: (params as ApprovePlanParams).plan_packet ?? {},
187
+ options: [],
188
+ response: null,
189
+ cancelled: true,
190
+ },
191
+ };
192
+ }
193
+
194
+ hooks?.appendPlanDraft?.({
195
+ plan_packet: validated.plan_packet,
196
+ human_summary: validated.human_summary,
197
+ });
198
+
199
+ let outcome: DialogResult;
200
+ if (parentCtx.hasUI) {
201
+ outcome = await runPlanApprovalDialog(parentCtx.ui, validated, {
202
+ onMounted: () => {
203
+ pi.events.emit("plan-approval:mounted", {});
204
+ },
205
+ });
206
+ } else {
207
+ outcome = await runPlanApprovalFallback(parentCtx.ui, validated);
208
+ }
209
+ const details = toApprovePlanToolDetails(
210
+ validated,
211
+ outcome.response,
212
+ outcome.cancelled,
213
+ );
214
+ notifyPlanApproval(hooks, details, "approve_plan");
215
+ const text = formatApprovePlanResultText(
216
+ outcome.response,
217
+ outcome.cancelled,
218
+ );
219
+ return {
220
+ content: [{ type: "text", text }],
221
+ details,
222
+ };
223
+ },
224
+ renderCall(args, theme) {
225
+ return renderApprovePlanCall(args, theme);
226
+ },
227
+ renderResult(result, options, theme) {
228
+ return renderApprovePlanResult(result, options, theme);
229
+ },
230
+ });
231
+
232
+ pi.registerTool({
233
+ name: "create_plan",
234
+ label: "Create Plan",
235
+ description:
236
+ "Write the approved PlanPacket to the canonical plan-packet.json for this harness run. Requires approve_plan Approve first. Do not use write/edit.",
237
+ promptSnippet: CREATE_PLAN_SNIPPET,
238
+ promptGuidelines: CREATE_PLAN_GUIDELINES,
239
+ parameters: CreatePlanParamsSchema,
240
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
241
+ const validated = validateApprovePlanParams(
242
+ params as ApprovePlanParams,
243
+ );
244
+ if (typeof validated === "string") {
245
+ return {
246
+ content: [{ type: "text", text: validated }],
247
+ details: { error: validated },
248
+ };
249
+ }
250
+ const projectRoot = hooks?.projectRoot ?? parentCtx.cwd;
251
+ const parentEntries = hooks?.getParentEntries?.() ?? [];
252
+ const subEntries = ctx.sessionManager.getEntries();
253
+ const result = await executeCreatePlan(validated.plan_packet, {
254
+ projectRoot,
255
+ getParentEntries: () => parentEntries,
256
+ getSubagentEntries: () => subEntries,
257
+ getParentRunContext: () => hooks?.getParentRunContext?.() ?? null,
258
+ onCommitted: (runCtx, packet, planPath) => {
259
+ hooks?.onPlanCommitted?.(runCtx, packet, planPath);
260
+ },
261
+ });
262
+ const text = formatCreatePlanResultText(result);
263
+ return {
264
+ content: [{ type: "text", text }],
265
+ details: result.ok
266
+ ? {
267
+ plan_path: result.planPath,
268
+ plan_id: result.planId,
269
+ }
270
+ : { error: result.error },
271
+ isError: !result.ok,
272
+ };
273
+ },
274
+ renderCall(args, theme) {
275
+ const packet = (args as { plan_packet?: PlanPacketLike }).plan_packet;
276
+ const id = packet?.plan_id ?? "?";
277
+ return new Text(theme.fg("accent", `create_plan: ${id}`), 0, 0);
278
+ },
279
+ renderResult(result, _options, theme) {
280
+ const details = result.details as
281
+ | { plan_path?: string; error?: string }
282
+ | undefined;
283
+ if (details?.error) {
284
+ return new Text(
285
+ theme.fg("error", details.error ?? "create_plan failed"),
286
+ 0,
287
+ 0,
288
+ );
289
+ }
290
+ return new Text(
291
+ theme.fg(
292
+ "success",
293
+ `Wrote ${details?.plan_path ?? "plan-packet.json"}`,
294
+ ),
295
+ 0,
296
+ 0,
297
+ );
298
+ },
299
+ });
300
+ };
301
+ }
302
+
303
+ /** @deprecated Use createParentHarnessUiBridgeFactory */
304
+ export function createParentAskUserBridgeFactory(
305
+ parentCtx: ExtensionContext,
306
+ agentType: string,
307
+ hooks?: ParentHarnessUiHooks,
308
+ ): ((pi: ExtensionAPI) => void) | null {
309
+ return createParentHarnessUiBridgeFactory(parentCtx, agentType, hooks);
310
+ }
@@ -0,0 +1,59 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ appendPlanApprovalIfNew,
4
+ getLatestRunContext,
5
+ type HarnessRunContext,
6
+ nowIso,
7
+ type PlanUserApproval,
8
+ planPacketSummary,
9
+ } from "../../../lib/harness-run-context.js";
10
+ import type { ParentHarnessUiHooks } from "./parent-harness-ui-bridge.js";
11
+
12
+ function persistRunContext(pi: ExtensionAPI, runCtx: HarnessRunContext): void {
13
+ pi.appendEntry("harness-run-context", runCtx);
14
+ }
15
+
16
+ export function createParentHarnessUiHooks(
17
+ pi: ExtensionAPI,
18
+ getParentEntries: () => unknown[],
19
+ projectRoot: string,
20
+ ): ParentHarnessUiHooks {
21
+ return {
22
+ projectRoot,
23
+ getParentEntries,
24
+ getParentRunContext: () => getLatestRunContext(getParentEntries()),
25
+ appendPlanDraft: (draft) => {
26
+ const planId = String(draft.plan_packet.plan_id ?? "plan");
27
+ const summary =
28
+ draft.human_summary?.trim() || `Plan ${planId} — pending your approval`;
29
+ pi.sendMessage({
30
+ customType: "harness-plan-draft",
31
+ content: summary,
32
+ display: true,
33
+ details: {
34
+ schema_version: "1.0.0",
35
+ plan_packet: draft.plan_packet,
36
+ human_summary: draft.human_summary ?? null,
37
+ shown_at: nowIso(),
38
+ },
39
+ });
40
+ },
41
+ onPlanApproval: (approval: PlanUserApproval) => {
42
+ const entries = getParentEntries();
43
+ const runCtx = getLatestRunContext(entries);
44
+ appendPlanApprovalIfNew(
45
+ (type, data) => pi.appendEntry(type, data),
46
+ entries,
47
+ approval,
48
+ runCtx,
49
+ );
50
+ },
51
+ onPlanCommitted: (runCtx, packet, planPath) => {
52
+ persistRunContext(pi, runCtx);
53
+ pi.appendEntry(
54
+ "harness-plan-packet",
55
+ planPacketSummary(packet, planPath, "ready"),
56
+ );
57
+ },
58
+ };
59
+ }
@@ -41,5 +41,14 @@ export function evaluateSubagentToolCall(
41
41
  reason: `Tool "ask_user" is not available for ${agentType ?? "this agent"} (orchestrator-only).`,
42
42
  };
43
43
  }
44
+ if (toolName === "approve_plan" || toolName === "create_plan") {
45
+ if (agentType === "harness/planner") {
46
+ return { action: "allow" };
47
+ }
48
+ return {
49
+ action: "block",
50
+ reason: `Tool "${toolName}" is only available for harness/planner.`,
51
+ };
52
+ }
44
53
  return { action: "allow" };
45
54
  }
@@ -13,6 +13,7 @@ import type {
13
13
  ExtensionAPI,
14
14
  ExtensionContext,
15
15
  } from "@earendil-works/pi-coding-agent";
16
+ import type { ParentHarnessUiHooks } from "../parent-harness-ui-bridge.js";
16
17
  import { resumeAgent, runAgent, type ToolActivity } from "./agent-runner.js";
17
18
  import type {
18
19
  AgentInvocation,
@@ -84,6 +85,8 @@ interface SpawnOptions {
84
85
  onCompaction?: (info: CompactionInfo) => void;
85
86
  /** Spawn context (blackboard injection) for subagent system prompt. */
86
87
  systemPromptAppendix?: string;
88
+ /** Parent UI hooks for plan approval sync and draft transcript. */
89
+ parentHarnessUiHooks?: ParentHarnessUiHooks;
87
90
  }
88
91
 
89
92
  export class AgentManager {
@@ -253,6 +256,7 @@ export class AgentManager {
253
256
  },
254
257
  systemPromptAppendix: options.systemPromptAppendix,
255
258
  parentExtensionContext: ctx,
259
+ parentHarnessUiHooks: options.parentHarnessUiHooks,
256
260
  })
257
261
  .then(({ responseText, session, aborted, steered }) => {
258
262
  // Don't overwrite status if externally stopped via abort()
@@ -18,7 +18,10 @@ import {
18
18
  SettingsManager,
19
19
  } from "@earendil-works/pi-coding-agent";
20
20
  import { evaluateHarnessSubagentToolCall } from "../harness-subagent-policy.js";
21
- import { createParentAskUserBridgeFactory } from "../parent-ask-user-bridge.js";
21
+ import {
22
+ createParentHarnessUiBridgeFactory,
23
+ type ParentHarnessUiHooks,
24
+ } from "../parent-harness-ui-bridge.js";
22
25
  import {
23
26
  getAgentConfig,
24
27
  getConfig,
@@ -152,8 +155,10 @@ export interface RunOptions {
152
155
  }) => void;
153
156
  /** Blackboard or other spawn context appended to the subagent system prompt. */
154
157
  systemPromptAppendix?: string;
155
- /** Parent session context — used to bridge ask_user UI into subagents. */
158
+ /** Parent session context — used to bridge ask_user / approve_plan UI into subagents. */
156
159
  parentExtensionContext?: ExtensionContext;
160
+ /** Parent-session hooks (plan draft transcript, approval sync). */
161
+ parentHarnessUiHooks?: ParentHarnessUiHooks;
157
162
  }
158
163
 
159
164
  export interface RunResult {
@@ -331,11 +336,15 @@ export async function runAgent(
331
336
  : systemPrompt;
332
337
 
333
338
  const extensionFactories: Array<(pi: ExtensionAPI) => void> = [];
334
- const askUserBridge = options.parentExtensionContext
335
- ? createParentAskUserBridgeFactory(options.parentExtensionContext, type)
339
+ const harnessUiBridge = options.parentExtensionContext
340
+ ? createParentHarnessUiBridgeFactory(
341
+ options.parentExtensionContext,
342
+ type,
343
+ options.parentHarnessUiHooks,
344
+ )
336
345
  : null;
337
- if (askUserBridge) {
338
- extensionFactories.push(askUserBridge);
346
+ if (harnessUiBridge) {
347
+ extensionFactories.push(harnessUiBridge);
339
348
  }
340
349
  extensionFactories.push((pi) => {
341
350
  pi.on("tool_call", (event) => {
@@ -410,7 +419,14 @@ export async function runAgent(
410
419
  const filterTools = (names: string[]) =>
411
420
  names.filter((t) => {
412
421
  if (EXCLUDED_TOOL_NAMES.includes(t)) return false;
413
- if (t === "ask_user" && askUserBridge) return true;
422
+ if (t === "ask_user" && harnessUiBridge) return true;
423
+ if (
424
+ (t === "approve_plan" || t === "create_plan") &&
425
+ harnessUiBridge &&
426
+ type === "harness/planner"
427
+ ) {
428
+ return true;
429
+ }
414
430
  if (disallowedSet?.has(t)) return false;
415
431
  if (builtinToolNameSet.has(t)) return true;
416
432
  if (extensions === false) return false;
@@ -425,7 +441,14 @@ export async function runAgent(
425
441
  session.setActiveToolsByName(activeTools);
426
442
  } else {
427
443
  const fallback = toolNames.filter((t) => {
428
- if (t === "ask_user" && askUserBridge) return true;
444
+ if (t === "ask_user" && harnessUiBridge) return true;
445
+ if (
446
+ (t === "approve_plan" || t === "create_plan") &&
447
+ harnessUiBridge &&
448
+ type === "harness/planner"
449
+ ) {
450
+ return true;
451
+ }
429
452
  return !disallowedSet?.has(t);
430
453
  });
431
454
  session.setActiveToolsByName(fallback);
@@ -444,10 +467,14 @@ export async function runAgent(
444
467
  },
445
468
  });
446
469
 
447
- if (askUserBridge) {
448
- const withAsk = new Set(session.getActiveToolNames());
449
- withAsk.add("ask_user");
450
- session.setActiveToolsByName([...withAsk]);
470
+ if (harnessUiBridge) {
471
+ const withHarnessUi = new Set(session.getActiveToolNames());
472
+ withHarnessUi.add("ask_user");
473
+ if (type === "harness/planner") {
474
+ withHarnessUi.add("approve_plan");
475
+ withHarnessUi.add("create_plan");
476
+ }
477
+ session.setActiveToolsByName([...withHarnessUi]);
451
478
  }
452
479
 
453
480
  options.onSessionCreated?.(session);
@@ -18,8 +18,8 @@ import {
18
18
  import { Text } from "@earendil-works/pi-tui";
19
19
  import { Type } from "@sinclair/typebox";
20
20
  import {
21
- extractPlanApprovalsFromEntries,
22
21
  getLatestRunContext,
22
+ syncPlannerApprovalsToParent,
23
23
  } from "../../../../lib/harness-run-context.js";
24
24
  import { getDriftReport } from "../agent-manifest.js";
25
25
  import { Blackboard } from "../blackboard.js";
@@ -27,6 +27,8 @@ import {
27
27
  buildBlackboardContextInjection,
28
28
  registerBlackboardTool,
29
29
  } from "../blackboard-tool.js";
30
+ import type { ParentHarnessUiHooks } from "../parent-harness-ui-bridge.js";
31
+ import { createParentHarnessUiHooks } from "../parent-harness-ui-hooks.js";
30
32
  import { AgentManager } from "./agent-manager.js";
31
33
  import {
32
34
  getAgentConversation,
@@ -650,6 +652,7 @@ export function createHarnessSubagentsExtension(packageRoot: string) {
650
652
 
651
653
  // --- Cross-extension RPC via pi.events ---
652
654
  let currentCtx: ExtensionContext | undefined;
655
+ let parentHarnessUiHooks: ParentHarnessUiHooks | undefined;
653
656
 
654
657
  // ---- Subagent scheduler ----
655
658
  // Session-scoped: store is constructed inside session_start once sessionId
@@ -678,6 +681,11 @@ export function createHarnessSubagentsExtension(packageRoot: string) {
678
681
  // Capture ctx from session_start for RPC spawn handler + start the scheduler.
679
682
  pi.on("session_start", async (_event, ctx) => {
680
683
  currentCtx = ctx;
684
+ parentHarnessUiHooks = createParentHarnessUiHooks(
685
+ pi,
686
+ () => ctx.sessionManager.getEntries(),
687
+ ctx.cwd,
688
+ );
681
689
  manager.clearCompleted();
682
690
  if (isSchedulingEnabled() && !scheduler.isActive()) startScheduler(ctx);
683
691
 
@@ -751,7 +759,9 @@ export function createHarnessSubagentsExtension(packageRoot: string) {
751
759
  });
752
760
 
753
761
  // Live widget: show running agents above editor
754
- const widget = new AgentWidget(manager, agentActivity);
762
+ const widget = new AgentWidget(manager, agentActivity, () => {
763
+ pi.events.emit("subagents:agents-widget-mounted", {});
764
+ });
755
765
 
756
766
  // ---- Join mode configuration ----
757
767
  let defaultJoinMode: JoinMode = "smart";
@@ -1338,6 +1348,7 @@ Guidelines:
1338
1348
  isolation,
1339
1349
  invocation: agentInvocation,
1340
1350
  systemPromptAppendix,
1351
+ parentHarnessUiHooks,
1341
1352
  ...bgCallbacks,
1342
1353
  });
1343
1354
  } catch (err) {
@@ -1481,6 +1492,7 @@ Guidelines:
1481
1492
  invocation: agentInvocation,
1482
1493
  systemPromptAppendix,
1483
1494
  signal,
1495
+ parentHarnessUiHooks,
1484
1496
  ...fgCallbacks,
1485
1497
  },
1486
1498
  );
@@ -1491,6 +1503,24 @@ Guidelines:
1491
1503
 
1492
1504
  clearInterval(spinnerInterval);
1493
1505
 
1506
+ if (
1507
+ subagentType === "harness/planner" &&
1508
+ record.session &&
1509
+ record.status !== "running" &&
1510
+ record.status !== "queued"
1511
+ ) {
1512
+ const parentEntries = ctx.sessionManager.getEntries();
1513
+ const runCtx = getLatestRunContext(parentEntries);
1514
+ if (runCtx) {
1515
+ syncPlannerApprovalsToParent(
1516
+ (type, data) => pi.appendEntry(type, data),
1517
+ parentEntries,
1518
+ record.session.sessionManager.getEntries(),
1519
+ runCtx,
1520
+ );
1521
+ }
1522
+ }
1523
+
1494
1524
  // Clean up foreground agent from widget
1495
1525
  if (fgId) {
1496
1526
  agentActivity.delete(fgId);
@@ -1607,16 +1637,12 @@ Guidelines:
1607
1637
  const parentEntries = _ctx.sessionManager.getEntries();
1608
1638
  const runCtx = getLatestRunContext(parentEntries);
1609
1639
  if (runCtx) {
1610
- const subEntries = record.session.sessionManager.getEntries();
1611
- for (const approval of extractPlanApprovalsFromEntries(
1612
- subEntries,
1613
- )) {
1614
- pi.appendEntry("harness-plan-approval", {
1615
- plan_id: approval.plan_id ?? runCtx.plan_id,
1616
- approved_at: approval.approved_at,
1617
- source: "ask_user",
1618
- });
1619
- }
1640
+ syncPlannerApprovalsToParent(
1641
+ (type, data) => pi.appendEntry(type, data),
1642
+ parentEntries,
1643
+ record.session.sessionManager.getEntries(),
1644
+ runCtx,
1645
+ );
1620
1646
  }
1621
1647
  }
1622
1648
 
@@ -264,6 +264,7 @@ export class AgentWidget {
264
264
  constructor(
265
265
  private manager: AgentManager,
266
266
  private agentActivity: Map<string, AgentActivity>,
267
+ private onWidgetRegistered?: () => void,
267
268
  ) {}
268
269
 
269
270
  /** Set the UI context (grabbed from first tool execution). */
@@ -615,6 +616,7 @@ export class AgentWidget {
615
616
  { placement: "aboveEditor" },
616
617
  );
617
618
  this.widgetRegistered = true;
619
+ this.onWidgetRegistered?.();
618
620
  } else {
619
621
  // Widget already registered — just request a re-render of existing components.
620
622
  this.tui?.requestRender();