ultimate-pi 0.8.0 → 0.9.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.
@@ -129,6 +129,36 @@ export interface PlanUserApproval {
129
129
  source: "ask_user" | "harness-plan-approval" | "noninteractive";
130
130
  }
131
131
 
132
+ /** Persisted on `input` when user invokes a raw `/harness-*` prompt template. */
133
+ export interface HarnessTurnEntry {
134
+ schema_version: "1.0.0";
135
+ command: string;
136
+ args: string;
137
+ source: "slash";
138
+ invoked_at: string;
139
+ }
140
+
141
+ export const HARNESS_COMMAND_PHASE: Record<string, HarnessPhase> = {
142
+ "harness-plan": "plan",
143
+ "harness-auto": "plan",
144
+ "harness-run": "execute",
145
+ "harness-eval": "evaluate",
146
+ "harness-review": "evaluate",
147
+ "harness-critic": "adversary",
148
+ "harness-trace": "evaluate",
149
+ "harness-incident": "evaluate",
150
+ "harness-drift-replan": "plan",
151
+ "harness-drift-proceed": "execute",
152
+ "harness-abort": "plan",
153
+ "harness-new-run": "plan",
154
+ "harness-run-status": "plan",
155
+ "harness-use-run": "plan",
156
+ "harness-policy-status": "merge",
157
+ "harness-router-tune": "plan",
158
+ "harness-budget-status": "plan",
159
+ "harness-setup": "execute",
160
+ };
161
+
132
162
  export interface PlanPhaseMutationDecision {
133
163
  allowed: boolean;
134
164
  reason?: string;
@@ -193,11 +223,40 @@ export async function isPlanPhaseScopedWrite(
193
223
  return isCanonicalPlanPacketPath(resolved, projectRoot, runCtx.run_id);
194
224
  }
195
225
 
226
+ export function getLatestHarnessTurn(
227
+ entries: unknown[],
228
+ ): HarnessTurnEntry | null {
229
+ for (let i = entries.length - 1; i >= 0; i--) {
230
+ const entry = entries[i] as SessionEntryLike;
231
+ if (entry.type !== "custom" || entry.customType !== "harness-turn") {
232
+ continue;
233
+ }
234
+ const data = entry.data as Partial<HarnessTurnEntry> | undefined;
235
+ if (data?.command && typeof data.command === "string") {
236
+ return {
237
+ schema_version: "1.0.0",
238
+ command: data.command,
239
+ args: typeof data.args === "string" ? data.args : "",
240
+ source: "slash",
241
+ invoked_at:
242
+ typeof data.invoked_at === "string" ? data.invoked_at : nowIso(),
243
+ };
244
+ }
245
+ }
246
+ return null;
247
+ }
248
+
196
249
  export function indexOfLastPlanCommand(entries: unknown[]): number {
197
250
  for (let i = entries.length - 1; i >= 0; i--) {
198
251
  const entry = entries[i] as SessionEntryLike & {
199
252
  message?: { role?: string; content?: string | unknown[] };
200
253
  };
254
+ if (entry.type === "custom" && entry.customType === "harness-turn") {
255
+ const cmd = (entry.data as { command?: string })?.command;
256
+ if (cmd === "harness-plan" || cmd === "harness-auto") {
257
+ return i;
258
+ }
259
+ }
201
260
  if (
202
261
  entry.type === "custom" &&
203
262
  entry.customType === "harness-plan-attempt"
@@ -221,7 +280,7 @@ export function indexOfLastPlanCommand(entries: unknown[]): number {
221
280
  .join("\n")
222
281
  : "";
223
282
  const visible = userVisiblePromptSlice(text);
224
- const parsed = parseHarnessSlashCommand(visible);
283
+ const parsed = parseHarnessSlashInput(visible);
225
284
  if (
226
285
  parsed?.command === "harness-plan" ||
227
286
  parsed?.command === "harness-auto"
@@ -342,7 +401,7 @@ export function isHarnessAutoSession(entries: unknown[]): boolean {
342
401
  typeof entry.message.content === "string"
343
402
  ? userVisiblePromptSlice(entry.message.content)
344
403
  : "";
345
- const parsed = parseHarnessSlashCommand(text);
404
+ const parsed = parseHarnessSlashInput(text);
346
405
  if (parsed?.command === "harness-auto") return true;
347
406
  }
348
407
  return false;
@@ -469,18 +528,16 @@ export function nowIso(): string {
469
528
  return new Date().toISOString();
470
529
  }
471
530
 
531
+ /** @deprecated Use parseHarnessSlashInput on raw `input` event text only. */
472
532
  export function isHarnessSlashCommand(prompt: string): boolean {
473
- const trimmed = prompt.trim();
474
- if (!trimmed.startsWith("/harness-")) return false;
475
- const match = trimmed.match(/^\/(harness-[a-z0-9-]+)/);
476
- if (!match) return false;
477
- return HARNESS_COMMANDS.has(match[1]);
533
+ return parseHarnessSlashInput(prompt) !== null;
478
534
  }
479
535
 
480
- export function parseHarnessSlashCommand(
481
- prompt: string,
536
+ /** Parse raw user input before prompt-template expansion (`input` hook only). */
537
+ export function parseHarnessSlashInput(
538
+ text: string,
482
539
  ): { command: string; args: string } | null {
483
- const trimmed = prompt.trim();
540
+ const trimmed = text.trim();
484
541
  const match = trimmed.match(/^\/(harness-[a-z0-9-]+)(?:\s+([\s\S]*))?$/);
485
542
  if (!match) return null;
486
543
  const command = match[1];
@@ -488,6 +545,13 @@ export function parseHarnessSlashCommand(
488
545
  return { command, args: (match[2] ?? "").trim() };
489
546
  }
490
547
 
548
+ /** @deprecated Prefer parseHarnessSlashInput on raw input; kept for expanded-prompt fallbacks. */
549
+ export function parseHarnessSlashCommand(
550
+ prompt: string,
551
+ ): { command: string; args: string } | null {
552
+ return parseHarnessSlashInput(userVisiblePromptSlice(prompt));
553
+ }
554
+
491
555
  /** User-visible prompt slice for policy signals (exclude injected blocks). */
492
556
  export function userVisiblePromptSlice(prompt: string): string {
493
557
  const markers = [
@@ -720,7 +784,32 @@ export function planPacketSummary(
720
784
  };
721
785
  }
722
786
 
723
- export function formatPlanContextBlock(ctx: HarnessRunContext): string {
787
+ export function buildHarnessSpawnContextSnippet(
788
+ ctx: HarnessRunContext,
789
+ opts?: { mode?: "create" | "revise"; risk_level?: string; quick?: boolean },
790
+ ): string {
791
+ const mode =
792
+ opts?.mode ??
793
+ (ctx.plan_ready || ctx.status === "aborted" ? "revise" : "create");
794
+ return JSON.stringify(
795
+ {
796
+ schema_version: "1.0.0",
797
+ run_id: ctx.run_id,
798
+ plan_packet_path: ctx.plan_packet_path,
799
+ task_summary: ctx.task_summary,
800
+ mode,
801
+ risk_level: opts?.risk_level ?? "med",
802
+ quick: opts?.quick ?? false,
803
+ },
804
+ null,
805
+ 2,
806
+ );
807
+ }
808
+
809
+ export function formatPlanContextBlock(
810
+ ctx: HarnessRunContext,
811
+ opts?: { mode?: "create" | "revise"; risk_level?: string; quick?: boolean },
812
+ ): string {
724
813
  const lines = [
725
814
  "[HarnessRunContext]",
726
815
  `run_id=${ctx.run_id}`,
@@ -735,6 +824,12 @@ export function formatPlanContextBlock(ctx: HarnessRunContext): string {
735
824
  if (ctx.plan_packet_path) {
736
825
  lines.push(`plan_packet_path=${ctx.plan_packet_path}`);
737
826
  }
827
+ if (ctx.task_summary) {
828
+ lines.push(`task_summary=${ctx.task_summary}`);
829
+ }
830
+ lines.push(
831
+ `HarnessSpawnContext=${buildHarnessSpawnContextSnippet(ctx, opts)}`,
832
+ );
738
833
  return lines.join("\n");
739
834
  }
740
835
 
@@ -850,7 +945,7 @@ export function shouldReuseHarnessRunId(
850
945
  ctx: HarnessRunContext | null,
851
946
  command: string | null,
852
947
  ): boolean {
853
- if (!command || !isHarnessSlashCommand(prompt)) return false;
948
+ if (!command) return false;
854
949
  if (command === "harness-new-run") return false;
855
950
  if (!ctx) return false;
856
951
  if (command === "harness-plan" || command === "harness-auto") {
@@ -875,27 +970,43 @@ export interface HarnessPolicyState {
875
970
  aborted: boolean;
876
971
  }
877
972
 
973
+ export function inferHarnessPhaseFromTurn(entries: unknown[]): HarnessPhase | null {
974
+ const turn = getLatestHarnessTurn(entries);
975
+ if (!turn) return null;
976
+ return HARNESS_COMMAND_PHASE[turn.command] ?? null;
977
+ }
978
+
979
+ /** Prefer session `harness-turn`; fall back to raw slash in visible prompt only. */
980
+ export function inferHarnessPhase(
981
+ entries: unknown[],
982
+ userPrompt?: string,
983
+ ): HarnessPhase {
984
+ const fromTurn = inferHarnessPhaseFromTurn(entries);
985
+ if (fromTurn) return fromTurn;
986
+ if (userPrompt) {
987
+ const parsed = parseHarnessSlashInput(userVisiblePromptSlice(userPrompt));
988
+ if (parsed && HARNESS_COMMAND_PHASE[parsed.command]) {
989
+ return HARNESS_COMMAND_PHASE[parsed.command];
990
+ }
991
+ }
992
+ return "execute";
993
+ }
994
+
995
+ /** @deprecated Use inferHarnessPhase(entries, prompt) — substring matching causes false plan phase. */
878
996
  export function inferHarnessPhaseFromPrompt(prompt: string): HarnessPhase {
879
- const p = prompt.toLowerCase();
880
- if (
881
- p.includes("/harness-plan") ||
882
- p.includes("harness-plan") ||
883
- p.includes("/harness-auto") ||
884
- p.includes("harness-auto")
885
- ) {
886
- return "plan";
997
+ const p = userVisiblePromptSlice(prompt).toLowerCase();
998
+ const parsed = parseHarnessSlashInput(userVisiblePromptSlice(prompt));
999
+ if (parsed && HARNESS_COMMAND_PHASE[parsed.command]) {
1000
+ return HARNESS_COMMAND_PHASE[parsed.command];
887
1001
  }
888
- if (p.includes("/harness-run") || p.includes("harness-run")) return "execute";
889
- if (p.includes("/harness-eval") || p.includes("harness-eval")) {
890
- return "evaluate";
1002
+ if (p.startsWith("/harness-plan") || p.startsWith("/harness-auto")) {
1003
+ return "plan";
891
1004
  }
892
- if (p.includes("/harness-review") || p.includes("harness-review")) {
1005
+ if (p.startsWith("/harness-run")) return "execute";
1006
+ if (p.startsWith("/harness-eval") || p.startsWith("/harness-review")) {
893
1007
  return "evaluate";
894
1008
  }
895
- if (p.includes("/harness-critic") || p.includes("harness-critic")) {
896
- return "adversary";
897
- }
898
- if (p.includes("adversary")) return "adversary";
1009
+ if (p.startsWith("/harness-critic")) return "adversary";
899
1010
  if (p.includes("merge gate") || p.includes("policy decision")) return "merge";
900
1011
  return "execute";
901
1012
  }
@@ -914,8 +1025,8 @@ export function isValidHarnessPhaseTransition(
914
1025
 
915
1026
  export function getLatestPolicyState(entries: unknown[]): HarnessPolicyState {
916
1027
  const fallback: HarnessPolicyState = {
917
- phase: "execute",
918
- approvedPlan: true,
1028
+ phase: "plan",
1029
+ approvedPlan: false,
919
1030
  planId: null,
920
1031
  aborted: false,
921
1032
  };
@@ -970,7 +1081,7 @@ export function getPolicyTransitionBlock(
970
1081
  return { blocked: false };
971
1082
  }
972
1083
  const state = getLatestPolicyState(entries);
973
- const nextPhase = inferHarnessPhaseFromPrompt(userPrompt);
1084
+ const nextPhase = inferHarnessPhase(entries, userPrompt);
974
1085
  if (!isValidHarnessPhaseTransition(state.phase, nextPhase)) {
975
1086
  return {
976
1087
  blocked: true,
@@ -1014,7 +1125,7 @@ export function isNewTaskPlanBlocked(
1014
1125
  ): boolean {
1015
1126
  if (ctx.status !== "active") return false;
1016
1127
  if (isAmendPlanAllowed(ctx, prompt, false)) return false;
1017
- const cmd = parseHarnessSlashCommand(prompt);
1128
+ const cmd = parseHarnessSlashInput(userVisiblePromptSlice(prompt));
1018
1129
  if (cmd?.command !== "harness-plan") return false;
1019
1130
  const taskMatch = prompt.match(/"([^"]+)"/);
1020
1131
  if (!taskMatch || !ctx.task_summary) return true;
@@ -1137,3 +1248,26 @@ export function driftGateActive(entries: unknown[]): boolean {
1137
1248
  export function phaseTraceFileName(phase: HarnessPhase): string {
1138
1249
  return `trace-${phase}.json`;
1139
1250
  }
1251
+
1252
+ /** Collect plan approvals from a session entry list (e.g. subagent in-memory session). */
1253
+ export function extractPlanApprovalsFromEntries(
1254
+ entries: unknown[],
1255
+ ): PlanUserApproval[] {
1256
+ const out: PlanUserApproval[] = [];
1257
+ for (let i = 0; i < entries.length; i++) {
1258
+ const entry = entries[i] as SessionEntryLike & {
1259
+ message?: {
1260
+ role?: string;
1261
+ toolName?: string;
1262
+ details?: unknown;
1263
+ content?: { type?: string; text?: string }[];
1264
+ };
1265
+ };
1266
+ if (entry.type !== "message" || entry.message?.role !== "toolResult") {
1267
+ continue;
1268
+ }
1269
+ const fromAsk = parseAskUserApprovalFromMessage(entry.message);
1270
+ if (fromAsk) out.push(fromAsk);
1271
+ }
1272
+ return out;
1273
+ }
@@ -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`, present draft, run `ask_user`, write plan after Approve. Do **not** plan inline in this session.
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.
9
9
 
10
10
  ## Step 0 — Parse arguments
11
11
 
@@ -22,39 +22,37 @@ If task is missing:
22
22
 
23
23
  ## Active plan context
24
24
 
25
+ Use injected context only — **do not** read `.pi/harness/specs/*.schema.json` or explore specs with bash.
26
+
25
27
  If `[HarnessActivePlan]` is present:
26
28
 
27
- - Read current packet from `plan_packet_path` first.
28
29
  - Treat task as **revise/amend** unless `/harness-new-run` was used.
29
- - Pass `mode: revise` in spawn context.
30
+ - Pass `mode: revise` using the `HarnessSpawnContext` JSON in `[HarnessRunContext]`.
30
31
 
31
- Otherwise use canonical path from `[HarnessRunContext]` for greenfield `mode: create`.
32
+ Otherwise use `HarnessSpawnContext` from `[HarnessRunContext]` for greenfield `mode: create`.
32
33
 
33
34
  ## Orchestration (required)
34
35
 
35
- 1. Build `HarnessSpawnContext` JSON (`.pi/harness/specs/harness-spawn-context.schema.json`) from injected run/plan context: `run_id`, `plan_packet_path`, `task_summary`, `risk_level`, `quick`, `mode`.
36
- 2. Spawn with **`inherit_context: false`**:
36
+ 1. Copy the `HarnessSpawnContext=…` JSON from `[HarnessRunContext]` into the spawn prompt (adjust `risk_level`, `quick`, `mode` from `$ARGUMENTS` if needed).
37
+ 2. Spawn **once** with **`inherit_context: false`**:
37
38
 
38
39
  ```
39
40
  Agent({ subagent_type: "harness/planner", prompt: "<task + HarnessSpawnContext JSON + output schema>" })
40
41
  ```
41
42
 
42
43
  3. `get_subagent_result` — parse final JSON (`status`, `plan_packet`, `human_summary`, `clarification`) via fenced `json` block.
43
- 4. If `needs_clarification`, call `ask_user` (harness-decisions) with planner `clarification.options`, then re-spawn with answers.
44
- 5. Present **full** human-readable plan in chat (scope, assumptions, acceptance_checks, rollback_plan, risk_level).
45
- 6. Call `ask_user`: **Approve** / **Request changes** / **Cancel** (harness-decisions). **Do not write** until Approve.
46
- 7. On **Request changes**, re-spawn planner with `mode: revise` and user feedback — do not write file.
47
- 8. **Only after Approve** — write `PlanPacket` JSON to canonical `plan_packet_path`.
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]`.
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.
48
47
 
49
48
  ## Parent rules
50
49
 
51
- - Do not mutate project source files — only `plan-packet.json` after approval.
52
- - Validate draft against `.pi/harness/specs/plan-packet.schema.json` before `ask_user` Approve.
50
+ - Do not mutate project source files — only `plan-packet.json` after subagent approval is recorded.
53
51
  - Do not embed `plan_id=` in prompts for policy sync.
52
+ - Optional: `/harness-plan-commit` if write was blocked but approval exists.
54
53
 
55
54
  ## Completion
56
55
 
57
56
  - `plan_status`: `ready` or `needs_clarification`
58
57
  - `risk_level` used
59
58
  - `next_command`: `/harness-run` when `ready` (never `/harness-run --plan …`)
60
- - If `needs_clarification`, user may reply in chat or re-run `/harness-plan`
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { readFile, access } from "node:fs/promises";
7
7
  import { constants } from "node:fs";
8
- import { join, dirname } from "node:path";
8
+ import { join, dirname, resolve } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { spawn } from "node:child_process";
11
11
 
@@ -22,6 +22,7 @@ const REQUIRED_SCHEMAS = [
22
22
  "run-trace.schema.json",
23
23
  "eval-verdict.schema.json",
24
24
  "harness-spawn-context.schema.json",
25
+ "harness-turn.schema.json",
25
26
  ];
26
27
 
27
28
  const REQUIRED_ADRS = [
@@ -201,6 +202,33 @@ async function main() {
201
202
  if (!(await fileExists(runCtxLib))) fail("missing lib/harness-run-context.ts");
202
203
  ok("lib/harness-run-context.ts");
203
204
 
205
+ const vendoredIndex = join(
206
+ ROOT,
207
+ ".pi",
208
+ "extensions",
209
+ "lib",
210
+ "harness-subagents",
211
+ "vendored",
212
+ "index.ts",
213
+ );
214
+ const vendoredSrc = await readFile(vendoredIndex, "utf-8");
215
+ const runCtxImport = vendoredSrc.match(
216
+ /from ["']([^"']*harness-run-context\.js)["']/,
217
+ );
218
+ if (!runCtxImport) {
219
+ fail("vendored/index.ts must import harness-run-context.js");
220
+ }
221
+ const runCtxImportPath = resolve(
222
+ dirname(vendoredIndex),
223
+ runCtxImport[1].replace(/\.js$/, ".ts"),
224
+ );
225
+ if (runCtxImportPath !== runCtxLib) {
226
+ fail(
227
+ `vendored/index.ts harness-run-context import resolves to ${runCtxImportPath}, expected ${runCtxLib}`,
228
+ );
229
+ }
230
+ ok("vendored/index.ts harness-run-context import path");
231
+
204
232
  const policyGateSrc = await readFile(
205
233
  join(ROOT, ".pi", "extensions", "policy-gate.ts"),
206
234
  "utf-8",
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [v0.9.1] — 2026-05-17
8
+
9
+ ### 🐛 Fixes
10
+
11
+ - **npm package:** fix `harness-subagents` vendored import of `harness-run-context` (`../../../../lib`); broken installs failed with `Cannot find module '../../../lib/harness-run-context.js'`.
12
+
13
+ ## [v0.9.0] — 2026-05-17
14
+
15
+ ### ✨ Features
16
+
17
+ - **Harness plan UX:** Pi-native `harness-turn` routing on `input` (no expanded-prompt matching); subagent `ask_user` bridged to parent UI; plan-phase budget cap 80k with debounced exhaustion events; thin `harness-plan` orchestrator with `harness-plan-commit`; `harness-turn.schema.json` and tests.
18
+
7
19
  ## [v0.8.0] — 2026-05-17
8
20
 
9
21
  ### ✨ Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-pi",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
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",
@@ -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/sentrux-rules-sync.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",
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",
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",