ultimate-pi 0.9.1 → 0.10.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.
Files changed (25) 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-plan-approval.ts +140 -0
  5. package/.pi/extensions/harness-run-context.ts +29 -8
  6. package/.pi/extensions/lib/harness-subagents/harness-subagent-policy.ts +11 -1
  7. package/.pi/extensions/lib/harness-subagents/parent-ask-user-bridge.ts +8 -87
  8. package/.pi/extensions/lib/harness-subagents/parent-harness-ui-bridge.ts +306 -0
  9. package/.pi/extensions/lib/harness-subagents/parent-harness-ui-hooks.ts +59 -0
  10. package/.pi/extensions/lib/harness-subagents/spawn-policy.ts +9 -0
  11. package/.pi/extensions/lib/harness-subagents/vendored/agent-manager.ts +4 -0
  12. package/.pi/extensions/lib/harness-subagents/vendored/agent-runner.ts +39 -12
  13. package/.pi/extensions/lib/harness-subagents/vendored/index.ts +35 -11
  14. package/.pi/extensions/lib/plan-approval/create-plan.ts +131 -0
  15. package/.pi/extensions/lib/plan-approval/dialog.ts +207 -0
  16. package/.pi/extensions/lib/plan-approval/fallback.ts +50 -0
  17. package/.pi/extensions/lib/plan-approval/format-plan.ts +94 -0
  18. package/.pi/extensions/lib/plan-approval/render.ts +83 -0
  19. package/.pi/extensions/lib/plan-approval/schema.ts +39 -0
  20. package/.pi/extensions/lib/plan-approval/types.ts +32 -0
  21. package/.pi/extensions/lib/plan-approval/validate.ts +61 -0
  22. package/.pi/lib/harness-run-context.ts +117 -28
  23. package/.pi/prompts/harness-plan.md +6 -6
  24. package/CHANGELOG.md +6 -0
  25. package/package.json +3 -3
@@ -126,7 +126,11 @@ const PLAN_CANCEL_OPTION =
126
126
  export interface PlanUserApproval {
127
127
  plan_id: string | null;
128
128
  approved_at: string;
129
- source: "ask_user" | "harness-plan-approval" | "noninteractive";
129
+ source:
130
+ | "ask_user"
131
+ | "approve_plan"
132
+ | "harness-plan-approval"
133
+ | "noninteractive";
130
134
  }
131
135
 
132
136
  /** Persisted on `input` when user invokes a raw `/harness-*` prompt template. */
@@ -291,32 +295,45 @@ export function indexOfLastPlanCommand(entries: unknown[]): number {
291
295
  return -1;
292
296
  }
293
297
 
294
- export function parseAskUserApprovalFromMessage(msg: {
298
+ type PlanApprovalToolDetails = {
299
+ cancelled?: boolean;
300
+ plan_packet?: PlanPacketLike;
301
+ response?: {
302
+ kind?: string;
303
+ text?: string;
304
+ selections?: string[];
305
+ };
306
+ };
307
+
308
+ function planIdFromApprovalDetails(
309
+ details: PlanApprovalToolDetails | undefined,
310
+ ): string | null {
311
+ const fromPacket = details?.plan_packet?.plan_id;
312
+ return typeof fromPacket === "string" && fromPacket.length > 0
313
+ ? fromPacket
314
+ : null;
315
+ }
316
+
317
+ export function parsePlanApprovalFromMessage(msg: {
295
318
  toolName?: string;
296
319
  details?: unknown;
297
320
  content?: { type?: string; text?: string }[];
298
321
  }): PlanUserApproval | null {
299
- if (msg.toolName !== "ask_user") return null;
300
- const details = msg.details as
301
- | {
302
- cancelled?: boolean;
303
- response?: {
304
- kind?: string;
305
- text?: string;
306
- selections?: string[];
307
- };
308
- }
309
- | undefined;
322
+ const toolName = msg.toolName;
323
+ if (toolName !== "ask_user" && toolName !== "approve_plan") return null;
324
+ const source = toolName === "approve_plan" ? "approve_plan" : "ask_user";
325
+ const details = msg.details as PlanApprovalToolDetails | undefined;
310
326
  if (details?.cancelled) return null;
311
327
  const response = details?.response;
312
328
  if (!response) return null;
329
+ const plan_id = planIdFromApprovalDetails(details);
313
330
  if (response.kind === "freeform") {
314
331
  const text = (response.text ?? "").trim();
315
332
  if (/^approve(d)?\b/i.test(text)) {
316
333
  return {
317
- plan_id: null,
334
+ plan_id,
318
335
  approved_at: nowIso(),
319
- source: "ask_user",
336
+ source,
320
337
  };
321
338
  }
322
339
  return null;
@@ -325,14 +342,23 @@ export function parseAskUserApprovalFromMessage(msg: {
325
342
  if (!selection || PLAN_CANCEL_OPTION.test(selection)) return null;
326
343
  if (PLAN_APPROVE_OPTION.test(selection)) {
327
344
  return {
328
- plan_id: null,
345
+ plan_id,
329
346
  approved_at: nowIso(),
330
- source: "ask_user",
347
+ source,
331
348
  };
332
349
  }
333
350
  return null;
334
351
  }
335
352
 
353
+ /** @deprecated Use parsePlanApprovalFromMessage */
354
+ export function parseAskUserApprovalFromMessage(msg: {
355
+ toolName?: string;
356
+ details?: unknown;
357
+ content?: { type?: string; text?: string }[];
358
+ }): PlanUserApproval | null {
359
+ return parsePlanApprovalFromMessage(msg);
360
+ }
361
+
336
362
  export function getLatestPlanUserApproval(
337
363
  entries: unknown[],
338
364
  sinceIndex = 0,
@@ -365,8 +391,8 @@ export function getLatestPlanUserApproval(
365
391
  if (entry.type !== "message" || entry.message?.role !== "toolResult") {
366
392
  continue;
367
393
  }
368
- const fromAsk = parseAskUserApprovalFromMessage(entry.message);
369
- if (fromAsk) return fromAsk;
394
+ const fromTool = parsePlanApprovalFromMessage(entry.message);
395
+ if (fromTool) return fromTool;
370
396
  }
371
397
  return null;
372
398
  }
@@ -472,7 +498,7 @@ export async function isPlanPhaseAllowedMutation(
472
498
  allowed: false,
473
499
  isScopedPlanWrite: true,
474
500
  reason:
475
- "policy-gate: plan-packet.json write blocked until the user approves via ask_user (present the full plan, then Approve).",
501
+ "policy-gate: plan-packet.json write blocked until the user approves via approve_plan or ask_user (present the full plan, then Approve).",
476
502
  };
477
503
  }
478
504
  if (opts.aborted) {
@@ -569,11 +595,72 @@ export function userVisiblePromptSlice(prompt: string): string {
569
595
 
570
596
  export function hasApprovedPlanSignalFromUserPrompt(prompt: string): boolean {
571
597
  const p = userVisiblePromptSlice(prompt).toLowerCase();
572
- return (
573
- p.includes("planpacket") ||
574
- p.includes("approved plan") ||
575
- /\bplan_id\s*[=:]/i.test(p)
576
- );
598
+ if (p.includes("user approved") || p.includes("already approved")) {
599
+ return true;
600
+ }
601
+ if (/\bapprove(d)?\s+(this\s+)?plan\b/.test(p)) return true;
602
+ if (p.includes("harness-plan-approval")) return true;
603
+ return false;
604
+ }
605
+
606
+ /** Detect parent-session ask_user calls that duplicate planner plan approval. */
607
+ export function isPlanApprovalAskUser(input: {
608
+ question?: string;
609
+ options?: unknown[];
610
+ }): boolean {
611
+ const q = String(input.question ?? "").trim();
612
+ const opts = Array.isArray(input.options) ? input.options : [];
613
+ const titles = opts.map((o) => {
614
+ if (typeof o === "string") return o.trim();
615
+ if (o && typeof o === "object" && "title" in o) {
616
+ return String((o as { title?: string }).title ?? "").trim();
617
+ }
618
+ return "";
619
+ });
620
+ const hasPlanOptions =
621
+ titles.some(
622
+ (t) => PLAN_APPROVE_OPTION.test(t) || PLAN_CANCEL_OPTION.test(t),
623
+ ) || PLAN_APPROVE_OPTION.test(q);
624
+ if (!hasPlanOptions) return false;
625
+ return /plan|approve/i.test(q);
626
+ }
627
+
628
+ export function appendPlanApprovalIfNew(
629
+ appendEntry: (customType: string, data: unknown) => void,
630
+ parentEntries: unknown[],
631
+ approval: PlanUserApproval,
632
+ runCtx: HarnessRunContext | null,
633
+ opts?: { sincePlanCommand?: boolean },
634
+ ): boolean {
635
+ const since =
636
+ opts?.sincePlanCommand !== false
637
+ ? Math.max(0, indexOfLastPlanCommand(parentEntries))
638
+ : 0;
639
+ if (getLatestPlanUserApproval(parentEntries, since)) {
640
+ return false;
641
+ }
642
+ appendEntry("harness-plan-approval", {
643
+ plan_id: approval.plan_id ?? runCtx?.plan_id ?? null,
644
+ approved_at: approval.approved_at,
645
+ source: approval.source,
646
+ });
647
+ return true;
648
+ }
649
+
650
+ /** Sync planner subagent approvals into the parent session (deduped). */
651
+ export function syncPlannerApprovalsToParent(
652
+ appendEntry: (customType: string, data: unknown) => void,
653
+ parentEntries: unknown[],
654
+ subEntries: unknown[],
655
+ runCtx: HarnessRunContext | null,
656
+ ): number {
657
+ let synced = 0;
658
+ for (const approval of extractPlanApprovalsFromEntries(subEntries)) {
659
+ if (appendPlanApprovalIfNew(appendEntry, parentEntries, approval, runCtx)) {
660
+ synced++;
661
+ }
662
+ }
663
+ return synced;
577
664
  }
578
665
 
579
666
  export function isDriftReplanPrompt(prompt: string): boolean {
@@ -970,7 +1057,9 @@ export interface HarnessPolicyState {
970
1057
  aborted: boolean;
971
1058
  }
972
1059
 
973
- export function inferHarnessPhaseFromTurn(entries: unknown[]): HarnessPhase | null {
1060
+ export function inferHarnessPhaseFromTurn(
1061
+ entries: unknown[],
1062
+ ): HarnessPhase | null {
974
1063
  const turn = getLatestHarnessTurn(entries);
975
1064
  if (!turn) return null;
976
1065
  return HARNESS_COMMAND_PHASE[turn.command] ?? null;
@@ -1266,8 +1355,8 @@ export function extractPlanApprovalsFromEntries(
1266
1355
  if (entry.type !== "message" || entry.message?.role !== "toolResult") {
1267
1356
  continue;
1268
1357
  }
1269
- const fromAsk = parseAskUserApprovalFromMessage(entry.message);
1270
- if (fromAsk) out.push(fromAsk);
1358
+ const fromTool = parsePlanApprovalFromMessage(entry.message);
1359
+ if (fromTool) out.push(fromTool);
1271
1360
  }
1272
1361
  return out;
1273
1362
  }
@@ -5,7 +5,7 @@ argument-hint: "\"<task>\" [--risk low|med|high] [--budget <amount>] [--quick]"
5
5
 
6
6
  # harness-plan
7
7
 
8
- Orchestrator only — spawn `harness/planner` once; planner runs clarification and approval via `ask_user` (parent UI). Write `plan-packet.json` only after approval. Do **not** plan inline in this session.
8
+ Orchestrator only — spawn `harness/planner` once. The planner runs clarification (`ask_user`), approval (`approve_plan`), and persists the plan (`create_plan`). Do **not** write `plan-packet.json` in this parent session.
9
9
 
10
10
  ## Step 0 — Parse arguments
11
11
 
@@ -33,7 +33,7 @@ Otherwise use `HarnessSpawnContext` from `[HarnessRunContext]` for greenfield `m
33
33
 
34
34
  ## Orchestration (required)
35
35
 
36
- 1. Copy the `HarnessSpawnContext=…` JSON from `[HarnessRunContext]` into the spawn prompt (adjust `risk_level`, `quick`, `mode` from `$ARGUMENTS` if needed).
36
+ 1. Copy the `HarnessSpawnContext=…` JSON from `[HarnessRunContext]` into the spawn prompt (adjust `risk_level`, `quick`, `mode` from `$ARGUMENTS` if needed). Do **not** add “call ask_user for approval” in the `Agent` prompt — the planner agent instructions already define `approve_plan` / `create_plan`.
37
37
  2. Spawn **once** with **`inherit_context: false`**:
38
38
 
39
39
  ```
@@ -41,15 +41,15 @@ Agent({ subagent_type: "harness/planner", prompt: "<task + HarnessSpawnContext J
41
41
  ```
42
42
 
43
43
  3. `get_subagent_result` — parse final JSON (`status`, `plan_packet`, `human_summary`, `clarification`) via fenced `json` block.
44
- 4. If `status === "ready"` and user approved in the subagent (`ask_user` Approve), validate `plan_packet` fields, then **write** `PlanPacket` JSON to canonical `plan_packet_path` from `[HarnessRunContext]`.
44
+ 4. If `status === "ready"` and parent `harness-run-context` shows `plan_ready: true` (planner called `create_plan`), confirm `plan_packet_path` exists do **not** write the file yourself.
45
45
  5. If `needs_clarification`, tell the user the planner is waiting — do **not** re-spawn; user should answer in the subagent or re-run `/harness-plan`.
46
- 6. Do **not** call `ask_user` in this parent session for planner clarification or approval.
46
+ 6. Do **not** call `ask_user`, `approve_plan`, or `create_plan` in this parent session.
47
47
 
48
48
  ## Parent rules
49
49
 
50
- - Do not mutate project source files only `plan-packet.json` after subagent approval is recorded.
50
+ - Do not mutate project source files in the plan phase.
51
51
  - Do not embed `plan_id=` in prompts for policy sync.
52
- - Optional: `/harness-plan-commit` if write was blocked but approval exists.
52
+ - Optional recovery: `/harness-plan-commit` only if the planner approved but `create_plan` failed.
53
53
 
54
54
  ## Completion
55
55
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [v0.10.0] — 2026-05-17
8
+
9
+ ### ✨ Features
10
+
11
+ - **Harness plan UX:** scrollable `approve_plan` overlay with `harness-plan-draft` parent transcript; planner-only `create_plan` persists canonical `plan-packet.json` after approval; blocks planner `write`/`edit` for plan files; syncs subagent approvals and dedupes duplicate plan-approval gates.
12
+
7
13
  ## [v0.9.1] — 2026-05-17
8
14
 
9
15
  ### 🐛 Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-pi",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Ultimate AI coding harness for pi.dev — extensible skills, Obsidian wiki knowledge layer, compressed context, deterministic output",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -73,7 +73,7 @@
73
73
  "@earendil-works/pi-coding-agent": "*"
74
74
  },
75
75
  "scripts": {
76
- "check:ts": "tsc --noEmit --target ES2023 --lib ES2023 --moduleResolution nodenext --module nodenext --skipLibCheck .pi/extensions/00-ultimate-pi-system-prompt.ts .pi/lib/harness-run-context.ts .pi/lib/harness-ui-state.ts .pi/extensions/harness-run-context.ts .pi/extensions/lib/harness-vcc-settings.ts .pi/extensions/dotenv-loader.ts .pi/extensions/lib/posthog-node.d.ts .pi/extensions/lib/harness-posthog.ts .pi/extensions/lib/harness-paths.ts .pi/extensions/pi-model-router-harness.ts .pi/extensions/provider-payload-sanitize.ts .pi/extensions/harness-telemetry.ts .pi/extensions/harness-ask-user.ts .pi/extensions/lib/ask-user/schema.ts .pi/extensions/lib/ask-user/types.ts .pi/extensions/lib/ask-user/validate.ts .pi/extensions/lib/ask-user/dialog.ts .pi/extensions/lib/ask-user/fallback.ts .pi/extensions/lib/ask-user/render.ts .pi/extensions/trace-recorder.ts .pi/extensions/observation-bus.ts .pi/extensions/drift-monitor.ts .pi/extensions/policy-gate.ts .pi/extensions/budget-guard.ts .pi/extensions/debate-orchestrator.ts .pi/extensions/harness-live-widget.ts .pi/extensions/sentrux-rules-sync.ts .pi/extensions/custom-header.ts .pi/extensions/lib/harness-subagents/agent-loader.ts .pi/extensions/lib/harness-subagents/agent-parser.ts .pi/extensions/lib/harness-subagents/agent-manifest.ts .pi/extensions/lib/harness-subagents/blackboard.ts .pi/extensions/lib/harness-subagents/blackboard-tool.ts .pi/extensions/lib/harness-subagents/spawn-policy.ts .pi/extensions/lib/harness-subagents/types-blackboard.ts .pi/extensions/harness-web-tools.ts .pi/extensions/harness-web-guard.ts .pi/extensions/lib/harness-web/run-cli.ts",
76
+ "check:ts": "tsc --noEmit --target ES2023 --lib ES2023 --moduleResolution nodenext --module nodenext --skipLibCheck .pi/extensions/00-ultimate-pi-system-prompt.ts .pi/lib/harness-run-context.ts .pi/lib/harness-ui-state.ts .pi/extensions/harness-run-context.ts .pi/extensions/lib/harness-vcc-settings.ts .pi/extensions/dotenv-loader.ts .pi/extensions/lib/posthog-node.d.ts .pi/extensions/lib/harness-posthog.ts .pi/extensions/lib/harness-paths.ts .pi/extensions/pi-model-router-harness.ts .pi/extensions/provider-payload-sanitize.ts .pi/extensions/harness-telemetry.ts .pi/extensions/harness-ask-user.ts .pi/extensions/harness-plan-approval.ts .pi/extensions/lib/ask-user/schema.ts .pi/extensions/lib/ask-user/types.ts .pi/extensions/lib/ask-user/validate.ts .pi/extensions/lib/ask-user/dialog.ts .pi/extensions/lib/ask-user/fallback.ts .pi/extensions/lib/ask-user/render.ts .pi/extensions/lib/plan-approval/types.ts .pi/extensions/lib/plan-approval/schema.ts .pi/extensions/lib/plan-approval/validate.ts .pi/extensions/lib/plan-approval/format-plan.ts .pi/extensions/lib/plan-approval/dialog.ts .pi/extensions/lib/plan-approval/fallback.ts .pi/extensions/lib/plan-approval/render.ts .pi/extensions/lib/plan-approval/create-plan.ts .pi/extensions/lib/harness-subagents/parent-harness-ui-bridge.ts .pi/extensions/lib/harness-subagents/parent-harness-ui-hooks.ts .pi/extensions/lib/harness-subagents/parent-ask-user-bridge.ts .pi/extensions/trace-recorder.ts .pi/extensions/observation-bus.ts .pi/extensions/drift-monitor.ts .pi/extensions/policy-gate.ts .pi/extensions/budget-guard.ts .pi/extensions/debate-orchestrator.ts .pi/extensions/harness-live-widget.ts .pi/extensions/sentrux-rules-sync.ts .pi/extensions/custom-header.ts .pi/extensions/lib/harness-subagents/agent-loader.ts .pi/extensions/lib/harness-subagents/agent-parser.ts .pi/extensions/lib/harness-subagents/agent-manifest.ts .pi/extensions/lib/harness-subagents/blackboard.ts .pi/extensions/lib/harness-subagents/blackboard-tool.ts .pi/extensions/lib/harness-subagents/spawn-policy.ts .pi/extensions/lib/harness-subagents/types-blackboard.ts .pi/extensions/harness-web-tools.ts .pi/extensions/harness-web-guard.ts .pi/extensions/lib/harness-web/run-cli.ts",
77
77
  "vendor:sync-router": "bash .pi/scripts/vendor-sync-pi-model-router.sh",
78
78
  "vendor:sync-vcc": "bash .pi/scripts/vendor-sync-pi-vcc.sh",
79
79
  "release": "bash .pi/scripts/release.sh",
@@ -82,7 +82,7 @@
82
82
  "format": "biome format --write",
83
83
  "format:check": "biome format",
84
84
  "prepare": "lefthook install",
85
- "test": "node --test test/harness-verify.test.mjs test/harness-ask-user.test.mjs test/harness-subagents-loader.test.mjs test/harness-subagents-import-path.test.mjs test/sentrux-rules-sync.test.mjs test/harness-budget-guard.test.mjs && npx -y tsx --test test/harness-vcc-settings.test.ts test/harness-plan-phase-policy.test.mjs test/harness-subagent-policy.test.mjs test/harness-turn-routing.test.mjs",
85
+ "test": "node --test test/harness-verify.test.mjs test/harness-ask-user.test.mjs test/harness-subagents-loader.test.mjs test/harness-subagents-import-path.test.mjs test/sentrux-rules-sync.test.mjs test/harness-budget-guard.test.mjs && npx -y tsx --test test/harness-vcc-settings.test.ts test/harness-plan-phase-policy.test.mjs test/harness-subagent-policy.test.mjs test/harness-turn-routing.test.mjs test/plan-approval-format.test.mjs test/plan-approval-sync.test.mjs test/plan-create-plan.test.mjs",
86
86
  "test:vcc": "npx -y tsx --test vendor/pi-vcc/tests/*.test.ts",
87
87
  "harness:sentrux-bootstrap": "node .pi/scripts/harness-sentrux-bootstrap.mjs",
88
88
  "harness:sentrux-sync": "node .pi/scripts/sentrux-rules-sync.mjs --force",