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
@@ -36,18 +36,22 @@ description: Structured user decisions via ask_user for harness setup, planning,
36
36
 
37
37
  ## Example (plan — approval gate)
38
38
 
39
- After presenting the full PlanPacket in chat:
39
+ `harness/planner` calls **`approve_plan`** with the full `plan_packet` (parent TUI: scrollable plan + Approve / Request changes / Cancel), then **`create_plan`** with the same packet after Approve. Do not use `ask_user` for final approval or `write`/`edit` for the plan file.
40
40
 
41
41
  ```json
42
42
  {
43
- "question": "Approve this plan for execution?",
44
- "context": "Scope, acceptance checks, and rollback are listed above. The plan file is written only after you approve.",
45
- "options": [
46
- { "title": "Approve", "description": "Write plan-packet.json and mark plan ready" },
47
- { "title": "Request changes", "description": "Revise scope or acceptance before writing" },
48
- { "title": "Cancel", "description": "Stop with needs_clarification" }
49
- ],
50
- "allowFreeform": false
43
+ "plan_packet": {
44
+ "schema_version": "1.0.0",
45
+ "contract_version": "1.0.0",
46
+ "plan_id": "",
47
+ "task_id": "",
48
+ "scope": "",
49
+ "assumptions": [],
50
+ "risk_level": "med",
51
+ "acceptance_checks": ["…"],
52
+ "rollback_plan": { "revert_commit_ready": true, "rollback_artifacts": { "revert_command": "…", "revert_branch": "…", "patch_bundle": "…" } }
53
+ },
54
+ "human_summary": "One-line summary for the overlay header"
51
55
  }
52
56
  ```
53
57
 
@@ -64,8 +68,8 @@ After presenting the full PlanPacket in chat:
64
68
  }
65
69
  ```
66
70
 
67
- ## Who must NOT call ask_user
71
+ ## Who calls what
68
72
 
69
- - `harness/planner` — returns `clarification.options` in JSON; parent runs `ask_user`.
70
- - `harness/evaluator`, `harness/adversary`, and `harness/tie-breaker` — emit `human_required` in structured verdicts; the **parent orchestrator** calls `ask_user`.
71
- - `harness/executor` — parent handles plan-level and governance forks.
73
+ - `harness/planner` — `ask_user` for clarification; **`approve_plan`** then **`create_plan`** for the plan file (`write`/`edit` blocked).
74
+ - `harness/evaluator`, `harness/adversary`, and `harness/tie-breaker` — emit `human_required`; the **parent orchestrator** calls `ask_user`.
75
+ - Parent orchestrator during `/harness-plan` — must **not** call `ask_user`, `approve_plan`, or `create_plan` (planner owns the full plan lifecycle).
@@ -17,12 +17,12 @@ description: Produce PlanPacket-aligned harness plans before execute phase. Use
17
17
  1. Use `HarnessSpawnContext` from injected `[HarnessRunContext]` — do not read spec files from disk.
18
18
  2. Spawn `harness/planner` **once** with that JSON in the prompt (`inherit_context: false`).
19
19
  3. Parse planner JSON from `get_subagent_result` (`status`, `plan_packet`, `clarification`).
20
- 4. Do **not** parent `ask_user` or re-spawn for clarification — planner uses `ask_user` in the subagent.
21
- 5. **Only after** subagent approval is synced — write canonical `plan_packet_path`.
20
+ 4. Do **not** parent `ask_user` / `approve_plan` / `create_plan` or re-spawn — planner uses those tools in the subagent (bridged UI + `create_plan` write).
21
+ 5. Parent checks `plan_ready` on `harness-run-context` after planner returns **does not** write `plan-packet.json`.
22
22
 
23
23
  ## Rules
24
24
 
25
- - `harness/planner` owns clarification and approval `ask_user` (bridged to parent UI).
25
+ - `harness/planner` owns clarification (`ask_user`), approval (`approve_plan`), and persistence (`create_plan` — only path to `plan-packet.json`; `write`/`edit` blocked).
26
26
  - Never plan or mutate source inline in the slash-command session.
27
27
  - context-mode only on harness paths; never lean-ctx.
28
28
 
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Harness planner that compiles strict PlanPacket contracts before execution.
3
- tools: read, grep, find, ls, ask_user
3
+ tools: read, grep, find, ls, ask_user, approve_plan, create_plan
4
+ disallowed_tools: write, edit, bash
4
5
  extensions: false
5
6
  thinking: medium
6
7
  max_turns: 20
@@ -11,7 +12,7 @@ You are the Harness Planner.
11
12
 
12
13
  ## Mission
13
14
 
14
- Compile a strict, machine-readable `PlanPacket` draft. Run clarification and final approval via `ask_user` in this session (parent UI). You do **not** write `plan-packet.json` — the orchestrator writes the canonical file after you return `status: ready` and the user has approved.
15
+ Compile a strict, machine-readable `PlanPacket`, get user approval, and persist it with **`create_plan`**. You do **not** use `write` or `edit` — those are blocked. The parent orchestrator does not write `plan-packet.json`.
15
16
 
16
17
  ## Spawn context
17
18
 
@@ -26,11 +27,12 @@ Read the `HarnessSpawnContext` JSON in the spawn prompt (`schema_version`, `mode
26
27
  5. Build a complete `PlanPacket`: `plan_id`, `task_id`, `scope`, `assumptions`, `risk_level`, `acceptance_checks`, `rollback_plan` with `revert_command`, `revert_branch`, `patch_bundle`, `revert_commit_ready: true`.
27
28
  6. Escalate `risk_level` to `high` for blast radius, uncertainty, or policy-sensitive surfaces.
28
29
  7. If scope is ambiguous, call `ask_user` with structured options — do not return `needs_clarification` without trying `ask_user` first when options are clear.
29
- 8. Before returning `ready`, present the full plan in chat and call `ask_user` with **Approve** / **Request changes** / **Cancel**. On Request changes, revise and ask again in this session.
30
+ 8. Call **`approve_plan`** with the full `plan_packet` (and optional `human_summary`). The parent TUI shows a scrollable plan plus **Approve** / **Request changes** / **Cancel**. On Request changes, revise and call `approve_plan` again.
31
+ 9. After the user selects **Approve**, call **`create_plan`** with the same `plan_packet` to write canonical `plan-packet.json` for this run.
30
32
 
31
33
  ## Guardrails
32
34
 
33
- - Do not mutate project files (read-only tools except `ask_user`).
35
+ - Never call `write`, `edit`, or mutating `bash` — use **`create_plan`** only for the plan file.
34
36
  - Never speculate about code you have not read.
35
37
  - Do not execute or widen implementation scope.
36
38
 
@@ -48,3 +50,5 @@ End with a single fenced `json` block the parent can parse:
48
50
  ```
49
51
 
50
52
  Use `"status": "needs_clarification"` only when blocked after `ask_user` or user cancelled; include `clarification` when the parent must intervene without a live subagent.
53
+
54
+ When `create_plan` succeeds, set `status` to `"ready"` and confirm `plan_packet_path` was written.
@@ -329,6 +329,52 @@ export default function harnessLiveWidget(pi: ExtensionAPI) {
329
329
  let component: HarnessWidgetComponent | null = null;
330
330
  let refreshQueued = false;
331
331
  let lastRenderHash = "";
332
+ let mountCtx: ExtensionContext | null = null;
333
+
334
+ function mountHarnessWidget(ctx: ExtensionContext): void {
335
+ if (!ctx.hasUI) return;
336
+ const state = stateStore.refresh(ctx);
337
+ const inFlight: InFlightState = { toolCount: 0, lastToolName: null };
338
+ lastRenderHash = computeRenderHash(state, inFlight);
339
+
340
+ ctx.ui.setWidget(
341
+ "harness-live",
342
+ (tui, theme) => {
343
+ widgetMounted = true;
344
+ tuiHandle = tui;
345
+ component = new HarnessWidgetComponent(
346
+ stateStore.snapshot(),
347
+ inFlight,
348
+ theme,
349
+ );
350
+ return {
351
+ render(width: number): string[] {
352
+ component?.setTheme(theme);
353
+ return component?.render(width) ?? [];
354
+ },
355
+ invalidate(): void {
356
+ component?.invalidate();
357
+ },
358
+ };
359
+ },
360
+ { placement: "aboveEditor" },
361
+ );
362
+ updateStatusFallback(ctx, state);
363
+ }
364
+
365
+ function remountHarnessLiveWidget(ctx: ExtensionContext): void {
366
+ if (!ctx.hasUI || !widgetMounted) return;
367
+ ctx.ui.setWidget("harness-live", undefined);
368
+ mountHarnessWidget(ctx);
369
+ }
370
+
371
+ pi.events.on("subagents:agents-widget-mounted", () => {
372
+ if (mountCtx) remountHarnessLiveWidget(mountCtx);
373
+ });
374
+
375
+ pi.events.on("plan-approval:mounted", () => {
376
+ if (mountCtx) remountHarnessLiveWidget(mountCtx);
377
+ });
332
378
 
333
379
  function updateStatusFallback(
334
380
  ctx: ExtensionContext,
@@ -385,34 +431,8 @@ export default function harnessLiveWidget(pi: ExtensionAPI) {
385
431
  }
386
432
 
387
433
  pi.on("session_start", (_event, ctx) => {
388
- if (!ctx.hasUI) return;
389
- const state = stateStore.refresh(ctx);
390
- const inFlight: InFlightState = { toolCount: 0, lastToolName: null };
391
- lastRenderHash = computeRenderHash(state, inFlight);
392
-
393
- ctx.ui.setWidget(
394
- "harness-live",
395
- (tui, theme) => {
396
- widgetMounted = true;
397
- tuiHandle = tui;
398
- component = new HarnessWidgetComponent(
399
- stateStore.snapshot(),
400
- inFlight,
401
- theme,
402
- );
403
- return {
404
- render(width: number): string[] {
405
- component?.setTheme(theme);
406
- return component?.render(width) ?? [];
407
- },
408
- invalidate(): void {
409
- component?.invalidate();
410
- },
411
- };
412
- },
413
- { placement: "aboveEditor" },
414
- );
415
- updateStatusFallback(ctx, state);
434
+ mountCtx = ctx;
435
+ mountHarnessWidget(ctx);
416
436
  });
417
437
 
418
438
  pi.on("context", (_event, ctx) => {
@@ -0,0 +1,174 @@
1
+ /**
2
+ * harness-plan-approval — PlanPacket approval UI and transcript renderer for parent sessions.
3
+ */
4
+
5
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+ import { Text } from "@earendil-works/pi-tui";
7
+ import {
8
+ appendPlanApprovalIfNew,
9
+ getLatestRunContext,
10
+ hasPlanUserApproval,
11
+ parsePlanApprovalFromMessage,
12
+ } from "../lib/harness-run-context.js";
13
+ import { runPlanApprovalDialog } from "./lib/plan-approval/dialog.js";
14
+ import { runPlanApprovalFallback } from "./lib/plan-approval/fallback.js";
15
+ import {
16
+ renderApprovePlanCall,
17
+ renderApprovePlanResult,
18
+ renderHarnessPlanDraft,
19
+ } from "./lib/plan-approval/render.js";
20
+ import {
21
+ ApprovePlanParamsSchema,
22
+ PROMPT_GUIDELINES,
23
+ PROMPT_SNIPPET,
24
+ } from "./lib/plan-approval/schema.js";
25
+ import type {
26
+ ApprovePlanParams,
27
+ PlanApprovalDialogResult,
28
+ } from "./lib/plan-approval/types.js";
29
+ import {
30
+ formatApprovePlanResultText,
31
+ toApprovePlanToolDetails,
32
+ validateApprovePlanParams,
33
+ } from "./lib/plan-approval/validate.js";
34
+
35
+ export default function harnessPlanApproval(pi: ExtensionAPI) {
36
+ pi.registerMessageRenderer(
37
+ "harness-plan-draft",
38
+ (message, _options, theme) => {
39
+ const data = message.details as
40
+ | {
41
+ plan_packet?: unknown;
42
+ human_summary?: string | null;
43
+ }
44
+ | undefined;
45
+ if (!data?.plan_packet) return undefined;
46
+ const lines = renderHarnessPlanDraft(
47
+ {
48
+ plan_packet: data.plan_packet as Parameters<
49
+ typeof renderHarnessPlanDraft
50
+ >[0]["plan_packet"],
51
+ human_summary: data.human_summary,
52
+ },
53
+ 80,
54
+ theme,
55
+ );
56
+ return new Text(lines.join("\n"), 0, 0);
57
+ },
58
+ );
59
+
60
+ pi.registerTool({
61
+ name: "approve_plan",
62
+ label: "Approve Plan",
63
+ description:
64
+ "Present a PlanPacket for user approval with a scrollable plan view. Planners should prefer the subagent bridge; this registers the tool on parent sessions for non-interactive fallback.",
65
+ promptSnippet: PROMPT_SNIPPET,
66
+ promptGuidelines: PROMPT_GUIDELINES,
67
+ parameters: ApprovePlanParamsSchema,
68
+
69
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
70
+ const validated = validateApprovePlanParams(params as ApprovePlanParams);
71
+ if (typeof validated === "string") {
72
+ return {
73
+ content: [{ type: "text", text: validated }],
74
+ details: {
75
+ plan_packet: (params as ApprovePlanParams).plan_packet ?? {},
76
+ options: [],
77
+ response: null,
78
+ cancelled: true,
79
+ },
80
+ };
81
+ }
82
+
83
+ const entries = ctx.sessionManager.getEntries();
84
+ if (
85
+ hasPlanUserApproval(entries, {
86
+ sincePlanCommand: true,
87
+ planId: validated.plan_packet.plan_id ?? null,
88
+ })
89
+ ) {
90
+ const planId = String(validated.plan_packet.plan_id ?? "plan");
91
+ return {
92
+ content: [
93
+ {
94
+ type: "text",
95
+ text: `Plan ${planId} already approved in this harness run (planner subagent). Proceed with /harness-run.`,
96
+ },
97
+ ],
98
+ details: {
99
+ plan_packet: validated.plan_packet,
100
+ options: validated.options,
101
+ response: {
102
+ kind: "selection",
103
+ selections: ["Approve"],
104
+ },
105
+ cancelled: false,
106
+ },
107
+ };
108
+ }
109
+
110
+ const planId = String(validated.plan_packet.plan_id ?? "plan");
111
+ const summary =
112
+ validated.human_summary?.trim() ||
113
+ `Plan ${planId} — pending your approval`;
114
+ pi.sendMessage({
115
+ customType: "harness-plan-draft",
116
+ content: summary,
117
+ display: true,
118
+ details: {
119
+ schema_version: "1.0.0",
120
+ plan_packet: validated.plan_packet,
121
+ human_summary: validated.human_summary ?? null,
122
+ shown_at: new Date().toISOString(),
123
+ },
124
+ });
125
+
126
+ let outcome: PlanApprovalDialogResult;
127
+ if (ctx.hasUI) {
128
+ outcome = await runPlanApprovalDialog(ctx.ui, validated, {
129
+ onMounted: () => {
130
+ pi.events.emit("plan-approval:mounted", {});
131
+ },
132
+ });
133
+ } else {
134
+ outcome = await runPlanApprovalFallback(ctx.ui, validated);
135
+ }
136
+
137
+ const details = toApprovePlanToolDetails(
138
+ validated,
139
+ outcome.response,
140
+ outcome.cancelled,
141
+ );
142
+ const approval = parsePlanApprovalFromMessage({
143
+ toolName: "approve_plan",
144
+ details,
145
+ });
146
+ if (approval) {
147
+ const runCtx = getLatestRunContext(entries);
148
+ appendPlanApprovalIfNew(
149
+ (type, data) => pi.appendEntry(type, data),
150
+ entries,
151
+ approval,
152
+ runCtx,
153
+ );
154
+ }
155
+
156
+ const text = formatApprovePlanResultText(
157
+ outcome.response,
158
+ outcome.cancelled,
159
+ );
160
+ return {
161
+ content: [{ type: "text", text }],
162
+ details,
163
+ };
164
+ },
165
+
166
+ renderCall(args, theme) {
167
+ return renderApprovePlanCall(args, theme);
168
+ },
169
+
170
+ renderResult(result, options, theme) {
171
+ return renderApprovePlanResult(result, options, theme);
172
+ },
173
+ });
174
+ }
@@ -26,14 +26,15 @@ import {
26
26
  isAmendPlanAllowed,
27
27
  isHarnessBootstrapPrompt,
28
28
  isNewTaskPlanBlocked,
29
+ isPlanApprovalAskUser,
29
30
  isStaleActiveRunPointer,
30
31
  loadProjectActiveRun,
31
32
  loadRunContextFromDisk,
32
33
  nextStepAfterOutcome,
33
34
  nowIso,
34
35
  type PlanPacketSummary,
35
- parseAskUserApprovalFromMessage,
36
36
  parseHarnessSlashInput,
37
+ parsePlanApprovalFromMessage,
37
38
  planPacketSummary,
38
39
  readPlanPacketFromPath,
39
40
  resolveArgsForCommand,
@@ -582,7 +583,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
582
583
  activeCtx.last_outcome = "needs_clarification";
583
584
  activeCtx.last_completed_step = "plan";
584
585
  const msg =
585
- "Plan file exists but user approval was not recorded. Present the full plan and call ask_user (Approve) before writing plan-packet.json.";
586
+ "Plan file exists but user approval was not recorded. Planner must call approve_plan (or bridged ask_user Approve) before writing plan-packet.json.";
586
587
  if (ctx.hasUI) ctx.ui.notify(msg, "warning");
587
588
  else
588
589
  pi.sendMessage({
@@ -649,9 +650,12 @@ export default function harnessRunContext(pi: ExtensionAPI) {
649
650
  });
650
651
 
651
652
  pi.on("tool_result", async (event, ctx) => {
652
- if (event.toolName !== "ask_user" || event.isError) return;
653
- const approval = parseAskUserApprovalFromMessage({
654
- toolName: "ask_user",
653
+ if (event.isError) return;
654
+ if (event.toolName !== "ask_user" && event.toolName !== "approve_plan") {
655
+ return;
656
+ }
657
+ const approval = parsePlanApprovalFromMessage({
658
+ toolName: event.toolName,
655
659
  details: event.details,
656
660
  content: event.content,
657
661
  });
@@ -662,11 +666,36 @@ export default function harnessRunContext(pi: ExtensionAPI) {
662
666
  pi.appendEntry("harness-plan-approval", {
663
667
  plan_id: approval.plan_id ?? runCtx.plan_id,
664
668
  approved_at: approval.approved_at,
665
- source: "ask_user",
669
+ source: approval.source,
666
670
  });
667
671
  });
668
672
 
669
- pi.on("tool_call", async (event) => {
673
+ pi.on("tool_call", async (event, ctx) => {
674
+ if (activeCtx?.plan_packet_path) {
675
+ const entries = getEntries(ctx);
676
+ if (hasPlanUserApproval(entries, { sincePlanCommand: true })) {
677
+ if (event.toolName === "approve_plan") {
678
+ return {
679
+ block: true,
680
+ reason:
681
+ "harness-run-context: plan already approved via planner subagent; do not call approve_plan again in the parent session.",
682
+ };
683
+ }
684
+ if (event.toolName === "ask_user") {
685
+ const input = event.input as {
686
+ question?: string;
687
+ options?: unknown[];
688
+ };
689
+ if (isPlanApprovalAskUser(input)) {
690
+ return {
691
+ block: true,
692
+ reason:
693
+ "harness-run-context: plan already approved via planner subagent; do not call ask_user for plan approval in the parent session.",
694
+ };
695
+ }
696
+ }
697
+ }
698
+ }
670
699
  if (!activeCtx?.plan_packet_path) return undefined;
671
700
  const phase = activeCtx.phase;
672
701
  if (phase !== "evaluate" && phase !== "adversary") return undefined;
@@ -807,7 +836,8 @@ export default function harnessRunContext(pi: ExtensionAPI) {
807
836
  }
808
837
  const target = runCtx.plan_packet_path;
809
838
  if (!target) {
810
- if (ctx.hasUI) ctx.ui.notify("No plan_packet_path on active run.", "error");
839
+ if (ctx.hasUI)
840
+ ctx.ui.notify("No plan_packet_path on active run.", "error");
811
841
  return;
812
842
  }
813
843
  if (pathArg && pathArg !== target) {
@@ -96,10 +96,20 @@ export function evaluateHarnessSubagentToolCall(
96
96
  return { action: "allow" };
97
97
  }
98
98
 
99
+ if (toolName === "create_plan") {
100
+ if (kind === "planner") {
101
+ return { action: "allow" };
102
+ }
103
+ return {
104
+ action: "block",
105
+ reason: `harness-subagent-policy: create_plan is only for harness/planner.`,
106
+ };
107
+ }
108
+
99
109
  if (MUTATING_TOOLS.has(toolName)) {
100
110
  return {
101
111
  action: "block",
102
- reason: `harness-subagent-policy: ${toolName} blocked for harness/${kind} (read-only phase agent).`,
112
+ reason: `harness-subagent-policy: ${toolName} blocked for harness/${kind} (read-only phase agent). Use create_plan after approve_plan instead of write/edit.`,
103
113
  };
104
114
  }
105
115
 
@@ -1,89 +1,10 @@
1
1
  /**
2
- * Registers ask_user in subagent sessions, delegating UI to the parent harness session.
2
+ * @deprecated Import from parent-harness-ui-bridge.js kept for stable import paths.
3
3
  */
4
-
5
- import type {
6
- ExtensionAPI,
7
- ExtensionContext,
8
- } from "@earendil-works/pi-coding-agent";
9
- import { runAskDialog } from "../ask-user/dialog.js";
10
- import { runAskFallback } from "../ask-user/fallback.js";
11
- import { renderAskCall, renderAskResult } from "../ask-user/render.js";
12
- import {
13
- AskUserParamsSchema,
14
- PROMPT_GUIDELINES,
15
- PROMPT_SNIPPET,
16
- } from "../ask-user/schema.js";
17
- import type { AskUserParams, DialogResult } from "../ask-user/types.js";
18
- import {
19
- formatResultText,
20
- toToolDetails,
21
- validateAskParams,
22
- } from "../ask-user/validate.js";
23
-
24
- const ASK_USER_AGENT_TYPES = new Set([
25
- "harness/planner",
26
- "harness/evaluator",
27
- "harness/adversary",
28
- "harness/tie-breaker",
29
- ]);
30
-
31
- export function agentTypeAllowsParentAskUser(agentType: string): boolean {
32
- return ASK_USER_AGENT_TYPES.has(agentType);
33
- }
34
-
35
- export function createParentAskUserBridgeFactory(
36
- parentCtx: ExtensionContext,
37
- agentType: string,
38
- ): ((pi: ExtensionAPI) => void) | null {
39
- if (!agentTypeAllowsParentAskUser(agentType)) {
40
- return null;
41
- }
42
- return (pi: ExtensionAPI) => {
43
- pi.registerTool({
44
- name: "ask_user",
45
- label: "Ask User",
46
- description:
47
- "Ask the user a structured question (parent session UI). Use for clarification and plan approval.",
48
- promptSnippet: PROMPT_SNIPPET,
49
- promptGuidelines: PROMPT_GUIDELINES,
50
- parameters: AskUserParamsSchema,
51
- async execute(_toolCallId, params, _signal, _onUpdate) {
52
- const validated = validateAskParams(params as AskUserParams);
53
- if (typeof validated === "string") {
54
- return {
55
- content: [{ type: "text", text: validated }],
56
- details: {
57
- question: params.question ?? "",
58
- options: [],
59
- response: null,
60
- cancelled: true,
61
- },
62
- };
63
- }
64
- let outcome: DialogResult;
65
- if (parentCtx.hasUI) {
66
- outcome = await runAskDialog(parentCtx.ui, validated);
67
- } else {
68
- outcome = await runAskFallback(parentCtx.ui, validated);
69
- }
70
- const details = toToolDetails(
71
- validated,
72
- outcome.response,
73
- outcome.cancelled,
74
- );
75
- const text = formatResultText(outcome.response, outcome.cancelled);
76
- return {
77
- content: [{ type: "text", text }],
78
- details,
79
- };
80
- },
81
- renderCall(args, theme) {
82
- return renderAskCall(args, theme);
83
- },
84
- renderResult(result, options, theme) {
85
- return renderAskResult(result, options, theme);
86
- },
87
- });
88
- };
89
- }
4
+ export {
5
+ agentTypeAllowsParentAskUser,
6
+ agentTypeAllowsParentHarnessUi,
7
+ createParentAskUserBridgeFactory,
8
+ createParentHarnessUiBridgeFactory,
9
+ type ParentHarnessUiHooks,
10
+ } from "./parent-harness-ui-bridge.js";