ultimate-pi 0.7.0 → 0.9.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 (115) hide show
  1. package/.agents/skills/harness-decisions/SKILL.md +20 -1
  2. package/.agents/skills/harness-eval/SKILL.md +11 -13
  3. package/.agents/skills/harness-orchestration/SKILL.md +36 -30
  4. package/.agents/skills/harness-plan/SKILL.md +13 -18
  5. package/.pi/PACKAGING.md +1 -1
  6. package/.pi/agents/harness/adversary.md +20 -12
  7. package/.pi/agents/harness/evaluator.md +25 -14
  8. package/.pi/agents/harness/executor.md +27 -16
  9. package/.pi/agents/harness/incident-recorder.md +37 -0
  10. package/.pi/agents/harness/meta-optimizer.md +18 -15
  11. package/.pi/agents/harness/planner.md +26 -30
  12. package/.pi/agents/harness/tie-breaker.md +4 -2
  13. package/.pi/agents/harness/trace-librarian.md +18 -11
  14. package/.pi/agents/pi-pi/ext-expert.md +1 -1
  15. package/.pi/agents/pi-pi/keybinding-expert.md +1 -1
  16. package/.pi/agents/pi-pi/tui-expert.md +3 -3
  17. package/.pi/extensions/00-ultimate-pi-system-prompt.ts +2 -2
  18. package/.pi/extensions/budget-guard.ts +47 -18
  19. package/.pi/extensions/custom-footer.ts +8 -3
  20. package/.pi/extensions/custom-header.ts +2 -2
  21. package/.pi/extensions/debate-orchestrator.ts +1 -1
  22. package/.pi/extensions/dotenv-loader.ts +1 -1
  23. package/.pi/extensions/drift-monitor.ts +1 -1
  24. package/.pi/extensions/harness-ask-user.ts +1 -1
  25. package/.pi/extensions/harness-live-widget.ts +1 -1
  26. package/.pi/extensions/harness-run-context.ts +197 -33
  27. package/.pi/extensions/harness-telemetry.ts +1 -1
  28. package/.pi/extensions/harness-web-guard.ts +1 -1
  29. package/.pi/extensions/harness-web-tools.ts +1 -1
  30. package/.pi/extensions/lib/ask-user/dialog.ts +2 -2
  31. package/.pi/extensions/lib/ask-user/fallback.ts +1 -1
  32. package/.pi/extensions/lib/ask-user/render.ts +3 -3
  33. package/.pi/extensions/lib/harness-subagents/agent-loader.ts +1 -1
  34. package/.pi/extensions/lib/harness-subagents/agent-parser.ts +1 -1
  35. package/.pi/extensions/lib/harness-subagents/blackboard-tool.ts +1 -1
  36. package/.pi/extensions/lib/harness-subagents/harness-subagent-policy.ts +134 -0
  37. package/.pi/extensions/lib/harness-subagents/parent-ask-user-bridge.ts +89 -0
  38. package/.pi/extensions/lib/harness-subagents/spawn-policy.ts +20 -2
  39. package/.pi/extensions/lib/harness-subagents/vendored/agent-manager.ts +3 -2
  40. package/.pi/extensions/lib/harness-subagents/vendored/agent-runner.ts +44 -24
  41. package/.pi/extensions/lib/harness-subagents/vendored/context.ts +1 -1
  42. package/.pi/extensions/lib/harness-subagents/vendored/env.ts +1 -1
  43. package/.pi/extensions/lib/harness-subagents/vendored/index.ts +23 -2
  44. package/.pi/extensions/lib/harness-subagents/vendored/output-file.ts +1 -1
  45. package/.pi/extensions/lib/harness-subagents/vendored/schedule.ts +1 -1
  46. package/.pi/extensions/lib/harness-subagents/vendored/settings.ts +1 -1
  47. package/.pi/extensions/lib/harness-subagents/vendored/skill-loader.ts +1 -1
  48. package/.pi/extensions/lib/harness-subagents/vendored/types.ts +2 -2
  49. package/.pi/extensions/lib/harness-subagents/vendored/ui/agent-widget.ts +1 -1
  50. package/.pi/extensions/lib/harness-subagents/vendored/ui/conversation-viewer.ts +2 -2
  51. package/.pi/extensions/lib/harness-subagents/vendored/ui/schedule-menu.ts +1 -1
  52. package/.pi/extensions/observation-bus.ts +1 -1
  53. package/.pi/extensions/pi-model-router-harness.ts +1 -1
  54. package/.pi/extensions/policy-gate.ts +90 -20
  55. package/.pi/extensions/provider-payload-sanitize.ts +1 -1
  56. package/.pi/extensions/review-integrity.ts +76 -22
  57. package/.pi/extensions/sentrux-rules-sync.ts +1 -1
  58. package/.pi/extensions/soundboard.ts +1 -1
  59. package/.pi/extensions/test-diff-integrity.ts +1 -1
  60. package/.pi/extensions/trace-recorder.ts +1 -1
  61. package/.pi/extensions/ultimate-pi-vcc.ts +1 -1
  62. package/.pi/harness/agents.manifest.json +82 -78
  63. package/.pi/harness/docs/adrs/0031-harness-run-context.md +6 -3
  64. package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +37 -0
  65. package/.pi/harness/docs/adrs/README.md +1 -0
  66. package/.pi/harness/specs/budget-exhausted-event.schema.json +3 -1
  67. package/.pi/harness/specs/harness-spawn-context.schema.json +65 -0
  68. package/.pi/harness/specs/harness-turn.schema.json +18 -0
  69. package/.pi/lib/harness-agent-output.ts +41 -0
  70. package/.pi/lib/harness-run-context.ts +516 -37
  71. package/.pi/lib/harness-ui-state.ts +1 -1
  72. package/.pi/prompts/harness-auto.md +36 -61
  73. package/.pi/prompts/harness-critic.md +15 -28
  74. package/.pi/prompts/harness-eval.md +19 -27
  75. package/.pi/prompts/harness-incident.md +15 -34
  76. package/.pi/prompts/harness-plan.md +28 -49
  77. package/.pi/prompts/harness-review.md +16 -30
  78. package/.pi/prompts/harness-router-tune.md +16 -38
  79. package/.pi/prompts/harness-run.md +21 -38
  80. package/.pi/prompts/harness-setup.md +2 -0
  81. package/.pi/prompts/harness-trace.md +13 -30
  82. package/.pi/scripts/harness-generate-model-router.mjs +16 -13
  83. package/.pi/scripts/harness-verify.mjs +17 -0
  84. package/.pi/scripts/vendor-sync-pi-model-router.sh +10 -10
  85. package/CHANGELOG.md +25 -1
  86. package/README.md +4 -5
  87. package/THIRD_PARTY_NOTICES.md +1 -1
  88. package/package.json +13 -8
  89. package/vendor/pi-model-router/UPSTREAM_PIN.md +1 -1
  90. package/vendor/pi-model-router/extensions/commands.ts +2 -2
  91. package/vendor/pi-model-router/extensions/config.ts +2 -2
  92. package/vendor/pi-model-router/extensions/index.ts +1 -1
  93. package/vendor/pi-model-router/extensions/provider.ts +2 -2
  94. package/vendor/pi-model-router/extensions/routing.ts +2 -2
  95. package/vendor/pi-model-router/extensions/types.ts +1 -1
  96. package/vendor/pi-model-router/extensions/ui.ts +1 -1
  97. package/vendor/pi-model-router/package.json +4 -4
  98. package/vendor/pi-vcc/index.ts +1 -1
  99. package/vendor/pi-vcc/package.json +1 -1
  100. package/vendor/pi-vcc/src/commands/pi-vcc.ts +1 -1
  101. package/vendor/pi-vcc/src/commands/vcc-recall.ts +1 -1
  102. package/vendor/pi-vcc/src/core/content.ts +1 -1
  103. package/vendor/pi-vcc/src/core/load-messages.ts +1 -1
  104. package/vendor/pi-vcc/src/core/normalize.ts +1 -1
  105. package/vendor/pi-vcc/src/core/render-entries.ts +1 -1
  106. package/vendor/pi-vcc/src/core/report.ts +1 -1
  107. package/vendor/pi-vcc/src/core/search-entries.ts +1 -1
  108. package/vendor/pi-vcc/src/core/summarize.ts +1 -1
  109. package/vendor/pi-vcc/src/hooks/before-compact.ts +2 -2
  110. package/vendor/pi-vcc/src/tools/recall.ts +1 -1
  111. package/vendor/pi-vcc/src/types.ts +1 -1
  112. package/vendor/pi-vcc/tests/fixtures.ts +1 -1
  113. package/vendor/pi-vcc/tests/render-entries.test.ts +1 -1
  114. package/vendor/pi-vcc/tests/search-entries.test.ts +1 -1
  115. package/vendor/pi-vcc/tests/support/load-session.ts +2 -2
@@ -19,7 +19,7 @@ You are an extensions expert for the Pi coding agent. You know EVERYTHING about
19
19
  - Flags via `pi.registerFlag()`
20
20
  - State management via tool result details and `pi.appendEntry()`
21
21
  - Custom rendering via renderCall/renderResult
22
- - Available imports: `@mariozechner/pi-coding-agent`, `@sinclair/typebox`, `@mariozechner/pi-ai` (StringEnum), `@mariozechner/pi-tui`
22
+ - Available imports: `@earendil-works/pi-coding-agent`, `@sinclair/typebox`, `@earendil-works/pi-ai` (StringEnum), `@earendil-works/pi-tui`
23
23
  - System prompt override via before_agent_start
24
24
  - Context manipulation via context event
25
25
  - Tool blocking and result modification
@@ -87,7 +87,7 @@ This is CRITICAL for building extensions that work on macOS:
87
87
  - When a reserved action is remapped away from a key, that key becomes available for extensions
88
88
  - The conflict check uses EFFECTIVE keybindings (after user remaps), not defaults
89
89
 
90
- ### Key Helper (from @mariozechner/pi-tui)
90
+ ### Key Helper (from @earendil-works/pi-tui)
91
91
 
92
92
  - `Key.ctrl("x")` → `"ctrl+x"`
93
93
  - `Key.shift("tab")` → `"shift+tab"`
@@ -19,7 +19,7 @@ You are a TUI (Terminal User Interface) expert for the Pi coding agent. You know
19
19
  - wantsKeyRelease? — for Kitty protocol key release events
20
20
  - invalidate() — clear cached render state
21
21
 
22
- ### Built-in Components (from @mariozechner/pi-tui)
22
+ ### Built-in Components (from @earendil-works/pi-tui)
23
23
 
24
24
  - Text: multi-line text with word wrapping, paddingX, paddingY, background function
25
25
  - Box: container with padding and background color
@@ -30,7 +30,7 @@ You are a TUI (Terminal User Interface) expert for the Pi coding agent. You know
30
30
  - SelectList: selection dialog with theme, onSelect/onCancel
31
31
  - SettingsList: toggle settings with theme
32
32
 
33
- ### From @mariozechner/pi-coding-agent
33
+ ### From @earendil-works/pi-coding-agent
34
34
 
35
35
  - DynamicBorder: border with color function — ALWAYS type the param: `(s: string) => theme.fg("accent", s)`
36
36
  - BorderedLoader: spinner with abort support
@@ -93,7 +93,7 @@ Then read the fetched file. Also search the local codebase for existing TUI comp
93
93
  ## How to Respond
94
94
 
95
95
  - Provide COMPLETE, WORKING component code
96
- - Include all imports from @mariozechner/pi-tui and @mariozechner/pi-coding-agent
96
+ - Include all imports from @earendil-works/pi-tui and @earendil-works/pi-coding-agent
97
97
  - Show the ctx.ui.custom() wrapper for interactive components
98
98
  - Handle invalidation properly for theme changes
99
99
  - Include keyboard input handling where relevant
@@ -13,8 +13,8 @@ import { join } from "node:path";
13
13
  import type {
14
14
  BuildSystemPromptOptions,
15
15
  ExtensionAPI,
16
- } from "@mariozechner/pi-coding-agent";
17
- import { formatSkillsForPrompt } from "@mariozechner/pi-coding-agent";
16
+ } from "@earendil-works/pi-coding-agent";
17
+ import { formatSkillsForPrompt } from "@earendil-works/pi-coding-agent";
18
18
  import { resolveHarnessAsset } from "./lib/harness-paths.js";
19
19
 
20
20
  // @ts-expect-error pi extensions run as ESM
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { appendFile, mkdir, readFile } from "node:fs/promises";
9
9
  import { join } from "node:path";
10
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
10
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
11
  import { getRunIdFromSession } from "../lib/harness-run-context.js";
12
12
 
13
13
  type HarnessPhase = "plan" | "execute" | "evaluate" | "adversary" | "merge";
@@ -23,7 +23,9 @@ interface BudgetExhaustedEvent {
23
23
  exhaustion_reason:
24
24
  | "max_rounds_reached"
25
25
  | "round_token_cap_exceeded"
26
- | "debate_global_cap_exceeded";
26
+ | "debate_global_cap_exceeded"
27
+ | "phase_cap_exceeded"
28
+ | "global_cap_exceeded";
27
29
  caps: {
28
30
  max_rounds: number;
29
31
  round_token_cap: number;
@@ -52,7 +54,7 @@ const DEFAULT_GLOBAL_CAP = Number(
52
54
  );
53
55
  const HARD_STOP_BUDGETS = process.env.HARNESS_BUDGET_HARD_STOP === "true";
54
56
  const DEFAULT_PHASE_CAPS: Record<HarnessPhase, number> = {
55
- plan: Number(process.env.HARNESS_BUDGET_PLAN_TOKENS ?? "12000"),
57
+ plan: Number(process.env.HARNESS_BUDGET_PLAN_TOKENS ?? "80000"),
56
58
  execute: Number(process.env.HARNESS_BUDGET_EXECUTE_TOKENS ?? "80000"),
57
59
  evaluate: Number(process.env.HARNESS_BUDGET_EVALUATE_TOKENS ?? "25000"),
58
60
  adversary: Number(process.env.HARNESS_BUDGET_ADVERSARY_TOKENS ?? "35000"),
@@ -191,6 +193,8 @@ async function emitBudgetEvent(
191
193
  pi.appendEntry("harness-budget-exhausted", event);
192
194
  }
193
195
 
196
+ const debouncedSoftLimit = new Map<string, boolean>();
197
+
194
198
  export default function budgetGuard(pi: ExtensionAPI) {
195
199
  pi.on("tool_call", async (_event, ctx) => {
196
200
  const policy = getPolicyContext(ctx);
@@ -202,35 +206,60 @@ export default function budgetGuard(pi: ExtensionAPI) {
202
206
  const globalCap = DEFAULT_GLOBAL_CAP;
203
207
  const phaseCap = DEFAULT_PHASE_CAPS[phase];
204
208
  const caps = await readDebateCapsFromSchema();
209
+ const runId = getRunId(ctx);
210
+
211
+ const phaseExceeded = phaseUsed >= phaseCap;
212
+ const globalExceeded = usage.totalTokens >= globalCap;
213
+ if (!phaseExceeded && !globalExceeded) return undefined;
205
214
 
206
- if (usage.totalTokens < globalCap && phaseUsed < phaseCap) return undefined;
215
+ const exhaustionReason = phaseExceeded
216
+ ? "phase_cap_exceeded"
217
+ : "global_cap_exceeded";
218
+ const debateCaps =
219
+ phase === "adversary" || phase === "evaluate"
220
+ ? caps
221
+ : {
222
+ max_rounds: 0,
223
+ round_token_cap: phaseCap,
224
+ debate_global_cap: globalCap,
225
+ };
207
226
 
208
227
  const exhausted: BudgetExhaustedEvent = {
209
228
  schema_version: "1.0.0",
210
229
  contract_version: "1.0.0",
211
230
  event_type: "budget_exhausted",
212
- run_id: getRunId(ctx),
231
+ run_id: runId,
213
232
  debate_id: `${phase}-budget-guard`,
214
233
  round_count: 1,
215
- budget_used: Math.max(usage.totalTokens, phaseUsed),
216
- exhaustion_reason: "debate_global_cap_exceeded",
217
- caps,
234
+ budget_used: phaseExceeded ? phaseUsed : usage.totalTokens,
235
+ exhaustion_reason: exhaustionReason,
236
+ caps: debateCaps,
218
237
  minimum_evidence_confidence: 0.6,
219
238
  default_policy_outcome: "block",
220
239
  human_override_allowed: true,
221
240
  };
222
241
 
223
- await emitBudgetEvent(pi, exhausted);
242
+ const debounceKey = `${runId}:${phase}:${exhaustionReason}`;
243
+ if (!debouncedSoftLimit.has(debounceKey)) {
244
+ debouncedSoftLimit.set(debounceKey, true);
245
+ await emitBudgetEvent(pi, exhausted);
246
+ }
247
+
224
248
  if (!HARD_STOP_BUDGETS) {
225
- pi.appendEntry("harness-budget-soft-limit", {
226
- run_id: exhausted.run_id,
227
- phase,
228
- phaseUsed,
229
- phaseCap,
230
- totalUsed: usage.totalTokens,
231
- totalCap: globalCap,
232
- timestamp: nowIso(),
233
- });
249
+ const softKey = `${debounceKey}:soft`;
250
+ if (!debouncedSoftLimit.has(softKey)) {
251
+ debouncedSoftLimit.set(softKey, true);
252
+ pi.appendEntry("harness-budget-soft-limit", {
253
+ run_id: exhausted.run_id,
254
+ phase,
255
+ phaseUsed,
256
+ phaseCap,
257
+ totalUsed: usage.totalTokens,
258
+ totalCap: globalCap,
259
+ exhaustion_reason: exhaustionReason,
260
+ timestamp: nowIso(),
261
+ });
262
+ }
234
263
  return undefined;
235
264
  }
236
265
  return {
@@ -9,9 +9,14 @@ import type {
9
9
  ExtensionAPI,
10
10
  ExtensionContext,
11
11
  ThemeColor,
12
- } from "@mariozechner/pi-coding-agent";
13
- import type { TUI } from "@mariozechner/pi-tui";
14
- import { Box, Text, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
12
+ } from "@earendil-works/pi-coding-agent";
13
+ import type { TUI } from "@earendil-works/pi-tui";
14
+ import {
15
+ Box,
16
+ Text,
17
+ truncateToWidth,
18
+ visibleWidth,
19
+ } from "@earendil-works/pi-tui";
15
20
 
16
21
  // ── router decision reader ──────────────────────────────────────────
17
22
 
@@ -6,8 +6,8 @@
6
6
  * doubling vertical resolution in the same terminal footprint.
7
7
  */
8
8
 
9
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
10
- import { truncateToWidth } from "@mariozechner/pi-tui";
9
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
+ import { truncateToWidth } from "@earendil-works/pi-tui";
11
11
  import * as JimpModule from "jimp";
12
12
  import { resolveHarnessAsset } from "./lib/harness-paths.js";
13
13
 
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
18
18
  import { join } from "node:path";
19
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
19
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
20
20
  import { getRunIdFromSession } from "../lib/harness-run-context.js";
21
21
 
22
22
  type DebateParticipant =
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { existsSync, readFileSync } from "node:fs";
15
15
  import { resolve } from "node:path";
16
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
16
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
17
 
18
18
  // ── .env parser ──────────────────────────────────────────────────
19
19
 
@@ -4,7 +4,7 @@
4
4
  * Emits harness-drift-report custom entries for harness-telemetry + observation bus.
5
5
  */
6
6
 
7
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
7
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
8
 
9
9
  type HarnessPhase = "plan" | "execute" | "evaluate" | "adversary" | "merge";
10
10
 
@@ -3,7 +3,7 @@
3
3
  * Design references: pi-ask-user, @pi-unipi/ask-user, rpiv-ask-user-question (not vendored).
4
4
  */
5
5
 
6
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
7
  import { runAskDialog } from "./lib/ask-user/dialog.js";
8
8
  import { runAskFallback } from "./lib/ask-user/fallback.js";
9
9
  import { renderAskCall, renderAskResult } from "./lib/ask-user/render.js";
@@ -1,7 +1,7 @@
1
1
  import type {
2
2
  ExtensionAPI,
3
3
  ExtensionContext,
4
- } from "@mariozechner/pi-coding-agent";
4
+ } from "@earendil-works/pi-coding-agent";
5
5
  import {
6
6
  type HarnessUiState,
7
7
  HarnessUiStateStore,
@@ -5,7 +5,8 @@
5
5
  * in before_agent_start so trace-recorder reuses it on agent_start.
6
6
  */
7
7
 
8
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
8
+ import { readFile, writeFile } from "node:fs/promises";
9
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
10
  import {
10
11
  canonicalPlanPath,
11
12
  createFreshRunContext,
@@ -13,21 +14,26 @@ import {
13
14
  extractCompletionStatuses,
14
15
  formatActivePlanBlock,
15
16
  formatPlanContextBlock,
17
+ getLatestHarnessTurn,
16
18
  getLatestPolicyPhase,
17
19
  getLatestRunContext,
18
20
  getPolicyTransitionBlock,
19
21
  type HarnessRunContext,
22
+ type HarnessTurnEntry,
20
23
  hasHarnessAbortSignal,
24
+ hasPlanUserApproval,
25
+ inferHarnessPhase,
21
26
  isAmendPlanAllowed,
22
27
  isHarnessBootstrapPrompt,
23
- isHarnessSlashCommand,
24
28
  isNewTaskPlanBlocked,
25
29
  isStaleActiveRunPointer,
26
30
  loadProjectActiveRun,
27
31
  loadRunContextFromDisk,
28
32
  nextStepAfterOutcome,
33
+ nowIso,
29
34
  type PlanPacketSummary,
30
- parseHarnessSlashCommand,
35
+ parseAskUserApprovalFromMessage,
36
+ parseHarnessSlashInput,
31
37
  planPacketSummary,
32
38
  readPlanPacketFromPath,
33
39
  resolveArgsForCommand,
@@ -57,29 +63,42 @@ function persistContext(pi: ExtensionAPI, ctx: HarnessRunContext): void {
57
63
  void saveProjectActiveRun(ctx);
58
64
  }
59
65
 
60
- function extractTaskSummary(prompt: string): string | null {
61
- const quoted = prompt.match(/"([^"]+)"/);
62
- if (quoted?.[1]) return quoted[1];
63
- const cmd = parseHarnessSlashCommand(prompt);
64
- if (cmd?.args) return cmd.args.slice(0, 200);
66
+ function extractTaskSummary(args: string, prompt?: string): string | null {
67
+ const fromArgs = args.match(/"([^"]+)"/);
68
+ if (fromArgs?.[1]) return fromArgs[1];
69
+ if (args.trim()) return args.trim().slice(0, 200);
70
+ if (prompt) {
71
+ const quoted = prompt.match(/"([^"]+)"/);
72
+ if (quoted?.[1]) return quoted[1];
73
+ }
65
74
  return null;
66
75
  }
67
76
 
77
+ function appendHarnessTurn(pi: ExtensionAPI, turn: HarnessTurnEntry): void {
78
+ pi.appendEntry("harness-turn", turn);
79
+ pi.appendEntry("harness-plan-attempt", {
80
+ run_id: null,
81
+ command: turn.command,
82
+ started_at: turn.invoked_at,
83
+ });
84
+ }
85
+
68
86
  function syncPolicyFromPlan(
69
87
  pi: ExtensionAPI,
70
88
  entries: unknown[],
71
89
  planId: string,
72
90
  phase: HarnessRunContext["phase"],
91
+ approvedPlan: boolean,
73
92
  ): void {
74
93
  let prior: Record<string, unknown> = {
75
94
  phase,
76
- approvedPlan: true,
95
+ approvedPlan,
77
96
  planId,
78
97
  budgetBypass: false,
79
98
  aborted: false,
80
99
  abortReason: null,
81
100
  abortedAt: null,
82
- updatedAt: new Date().toISOString(),
101
+ updatedAt: nowIso(),
83
102
  };
84
103
  for (let i = entries.length - 1; i >= 0; i--) {
85
104
  const entry = entries[i] as SessionEntryLike;
@@ -144,15 +163,35 @@ export default function harnessRunContext(pi: ExtensionAPI) {
144
163
  activeCtx = await hydrateFromDisk(sessionId, projectRoot, entries);
145
164
  });
146
165
 
166
+ pi.on("input", async (event) => {
167
+ if (event.source === "extension") {
168
+ return { action: "continue" as const };
169
+ }
170
+ const parsed = parseHarnessSlashInput(event.text);
171
+ if (!parsed) {
172
+ return { action: "continue" as const };
173
+ }
174
+ appendHarnessTurn(pi, {
175
+ schema_version: "1.0.0",
176
+ command: parsed.command,
177
+ args: parsed.args,
178
+ source: "slash",
179
+ invoked_at: nowIso(),
180
+ });
181
+ return { action: "continue" as const };
182
+ });
183
+
147
184
  pi.on("before_agent_start", async (event, ctx) => {
148
185
  const sessionId = ctx.sessionManager.getSessionId();
149
186
  const projectRoot = process.cwd();
150
187
  const entries = getEntries(ctx);
151
188
  const userPrompt = userVisiblePromptSlice(event.prompt);
152
- const parsed = parseHarnessSlashCommand(userPrompt);
189
+ const turn = getLatestHarnessTurn(entries);
190
+ const parsed = turn
191
+ ? { command: turn.command, args: turn.args }
192
+ : parseHarnessSlashInput(userPrompt);
153
193
  const harnessTurn =
154
- isHarnessSlashCommand(userPrompt) ||
155
- needsClarificationFollowUp(activeCtx);
194
+ Boolean(turn) || Boolean(parsed) || needsClarificationFollowUp(activeCtx);
156
195
 
157
196
  if (
158
197
  userPrompt.toLowerCase().includes("/harness-abort") ||
@@ -182,7 +221,10 @@ export default function harnessRunContext(pi: ExtensionAPI) {
182
221
  }
183
222
 
184
223
  const policyPhase =
185
- getLatestPolicyPhase(entries) ?? activeCtx?.phase ?? "plan";
224
+ inferHarnessPhase(entries, userPrompt) ??
225
+ getLatestPolicyPhase(entries) ??
226
+ activeCtx?.phase ??
227
+ "plan";
186
228
  const driftActive = driftGateActive(entries);
187
229
 
188
230
  // Plain-language follow-up after needs_clarification
@@ -192,18 +234,17 @@ export default function harnessRunContext(pi: ExtensionAPI) {
192
234
  const packet = activeCtx.plan_packet_path
193
235
  ? await readPlanPacketFromPath(activeCtx.plan_packet_path)
194
236
  : null;
195
- const summary = packet
196
- ? planPacketSummary(
197
- packet,
198
- activeCtx.plan_packet_path!,
199
- "needs_clarification",
200
- )
201
- : null;
237
+ const planPath = activeCtx.plan_packet_path;
238
+ const summary =
239
+ packet && planPath
240
+ ? planPacketSummary(packet, planPath, "needs_clarification")
241
+ : null;
202
242
  syncPolicyFromPlan(
203
243
  pi,
204
244
  entries,
205
245
  activeCtx.plan_id ?? "plan-pending",
206
246
  "plan",
247
+ false,
207
248
  );
208
249
  persistContext(pi, activeCtx);
209
250
  return {
@@ -239,7 +280,7 @@ export default function harnessRunContext(pi: ExtensionAPI) {
239
280
  activeCtx.last_outcome = "abandoned";
240
281
  persistContext(pi, activeCtx);
241
282
  }
242
- const task = extractTaskSummary(userPrompt);
283
+ const task = extractTaskSummary(args, userPrompt);
243
284
  activeCtx = createFreshRunContext(sessionId, projectRoot, task);
244
285
  persistContext(pi, activeCtx);
245
286
  return {
@@ -318,18 +359,29 @@ export default function harnessRunContext(pi: ExtensionAPI) {
318
359
  !activeCtx ||
319
360
  !shouldReuseHarnessRunId(userPrompt, activeCtx, command)
320
361
  ) {
321
- const task = extractTaskSummary(userPrompt);
362
+ const task = extractTaskSummary(args, userPrompt);
322
363
  activeCtx = createFreshRunContext(sessionId, projectRoot, task);
323
364
  }
324
- if (activeCtx.status === "aborted") {
325
- activeCtx.plan_ready = false;
326
- }
365
+ activeCtx.plan_ready = false;
327
366
  activeCtx.phase = "plan";
328
367
  activeCtx.status = "active";
329
368
  if (command === "harness-plan") {
330
- const task = extractTaskSummary(userPrompt);
369
+ const task = extractTaskSummary(args, userPrompt);
331
370
  if (task) activeCtx.task_summary = task;
332
371
  }
372
+ if (turn) {
373
+ pi.appendEntry("harness-plan-attempt", {
374
+ run_id: activeCtx.run_id,
375
+ command,
376
+ started_at: turn.invoked_at,
377
+ });
378
+ } else {
379
+ pi.appendEntry("harness-plan-attempt", {
380
+ run_id: activeCtx.run_id,
381
+ command,
382
+ started_at: nowIso(),
383
+ });
384
+ }
333
385
  } else if (
334
386
  activeCtx &&
335
387
  shouldReuseHarnessRunId(userPrompt, activeCtx, command)
@@ -465,7 +517,6 @@ export default function harnessRunContext(pi: ExtensionAPI) {
465
517
 
466
518
  pi.on("agent_end", async (_event, ctx) => {
467
519
  const entries = getEntries(ctx);
468
- const sessionId = ctx.sessionManager.getSessionId();
469
520
  if (!activeCtx) {
470
521
  activeCtx = getLatestRunContext(entries);
471
522
  }
@@ -485,7 +536,10 @@ export default function harnessRunContext(pi: ExtensionAPI) {
485
536
  ? lastUser.message.content
486
537
  : "";
487
538
  }
488
- const parsed = parseHarnessSlashCommand(userVisiblePromptSlice(lastPrompt));
539
+ const lastTurn = getLatestHarnessTurn(entries);
540
+ const parsed = lastTurn
541
+ ? { command: lastTurn.command, args: lastTurn.args }
542
+ : parseHarnessSlashInput(userVisiblePromptSlice(lastPrompt));
489
543
  if (!parsed && !needsClarificationFollowUp(activeCtx)) return;
490
544
 
491
545
  const policyPhase = getLatestPolicyPhase(entries) ?? activeCtx.phase;
@@ -519,15 +573,31 @@ export default function harnessRunContext(pi: ExtensionAPI) {
519
573
  ) {
520
574
  const packet = await readPlanPacketFromPath(activeCtx.plan_packet_path);
521
575
  const validation = validatePlanPacket(packet);
522
- planReady = validation.valid;
523
- if (planReady && packet?.plan_id) {
576
+ const approved = hasPlanUserApproval(entries, {
577
+ sincePlanCommand: true,
578
+ planId: packet?.plan_id ?? null,
579
+ });
580
+ planReady = validation.valid && approved;
581
+ if (validation.valid && !approved) {
582
+ activeCtx.last_outcome = "needs_clarification";
583
+ activeCtx.last_completed_step = "plan";
584
+ 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
+ if (ctx.hasUI) ctx.ui.notify(msg, "warning");
587
+ else
588
+ pi.sendMessage({
589
+ customType: "harness-plan-packet",
590
+ content: msg,
591
+ display: true,
592
+ });
593
+ } else if (planReady && packet?.plan_id) {
524
594
  activeCtx.plan_id = packet.plan_id;
525
- syncPolicyFromPlan(pi, entries, packet.plan_id, "plan");
595
+ syncPolicyFromPlan(pi, entries, packet.plan_id, "plan", true);
526
596
  const summary = planPacketSummary(packet, activeCtx.plan_packet_path);
527
597
  pi.appendEntry("harness-plan-packet", summary);
528
598
  activeCtx.last_completed_step = "plan";
529
599
  activeCtx.last_outcome = summary.plan_status;
530
- } else {
600
+ } else if (!validation.valid) {
531
601
  activeCtx.last_outcome = "needs_clarification";
532
602
  activeCtx.last_completed_step = "plan";
533
603
  }
@@ -578,6 +648,24 @@ export default function harnessRunContext(pi: ExtensionAPI) {
578
648
  }
579
649
  });
580
650
 
651
+ pi.on("tool_result", async (event, ctx) => {
652
+ if (event.toolName !== "ask_user" || event.isError) return;
653
+ const approval = parseAskUserApprovalFromMessage({
654
+ toolName: "ask_user",
655
+ details: event.details,
656
+ content: event.content,
657
+ });
658
+ if (!approval) return;
659
+ const entries = getEntries(ctx);
660
+ const runCtx = getLatestRunContext(entries) ?? activeCtx;
661
+ if (!runCtx) return;
662
+ pi.appendEntry("harness-plan-approval", {
663
+ plan_id: approval.plan_id ?? runCtx.plan_id,
664
+ approved_at: approval.approved_at,
665
+ source: "ask_user",
666
+ });
667
+ });
668
+
581
669
  pi.on("tool_call", async (event) => {
582
670
  if (!activeCtx?.plan_packet_path) return undefined;
583
671
  const phase = activeCtx.phase;
@@ -673,6 +761,82 @@ export default function harnessRunContext(pi: ExtensionAPI) {
673
761
  },
674
762
  });
675
763
 
764
+ pi.registerCommand("harness-plan-commit", {
765
+ description:
766
+ "Write approved plan-packet.json to the active run (requires harness-plan-approval)",
767
+ handler: async (args, ctx) => {
768
+ const projectRoot = process.cwd();
769
+ const entries = getEntries(ctx);
770
+ let runCtx = getLatestRunContext(entries) ?? activeCtx;
771
+ if (!runCtx) {
772
+ runCtx = await hydrateFromDisk(
773
+ ctx.sessionManager.getSessionId(),
774
+ projectRoot,
775
+ entries,
776
+ );
777
+ }
778
+ if (!runCtx?.plan_packet_path) {
779
+ const msg = "No active harness run. Run /harness-plan first.";
780
+ if (ctx.hasUI) ctx.ui.notify(msg, "warning");
781
+ return;
782
+ }
783
+ if (
784
+ !hasPlanUserApproval(entries, {
785
+ sincePlanCommand: true,
786
+ planId: runCtx.plan_id,
787
+ })
788
+ ) {
789
+ const msg =
790
+ "Plan commit blocked: no user approval recorded. Approve via ask_user in the planner subagent first.";
791
+ if (ctx.hasUI) ctx.ui.notify(msg, "warning");
792
+ return;
793
+ }
794
+ const pathArg = args.trim();
795
+ let packetPath = runCtx.plan_packet_path;
796
+ if (pathArg) {
797
+ packetPath = pathArg;
798
+ }
799
+ const packet = await readPlanPacketFromPath(packetPath);
800
+ const validation = validatePlanPacket(packet);
801
+ if (!validation.valid || !packet) {
802
+ const msg = !packet
803
+ ? "Plan packet file missing or unreadable."
804
+ : `Invalid plan packet: ${validation.errors.join("; ")}`;
805
+ if (ctx.hasUI) ctx.ui.notify(msg, "error");
806
+ return;
807
+ }
808
+ const target = runCtx.plan_packet_path;
809
+ if (!target) {
810
+ if (ctx.hasUI) ctx.ui.notify("No plan_packet_path on active run.", "error");
811
+ return;
812
+ }
813
+ if (pathArg && pathArg !== target) {
814
+ const raw = await readFile(pathArg, "utf-8");
815
+ await writeFile(target, raw, "utf-8");
816
+ }
817
+ runCtx.plan_id = packet.plan_id ?? runCtx.plan_id;
818
+ runCtx.plan_ready = true;
819
+ runCtx.phase = "plan";
820
+ runCtx.last_completed_step = "plan";
821
+ runCtx.last_outcome = "ready";
822
+ runCtx.next_recommended_command = "/harness-run";
823
+ runCtx.updated_at = nowIso();
824
+ activeCtx = runCtx;
825
+ persistContext(pi, runCtx);
826
+ syncPolicyFromPlan(
827
+ pi,
828
+ entries,
829
+ runCtx.plan_id ?? packet.plan_id ?? "plan-pending",
830
+ "plan",
831
+ true,
832
+ );
833
+ const summary = planPacketSummary(packet, target, "ready");
834
+ pi.appendEntry("harness-plan-packet", summary);
835
+ const msg = `Plan committed: ${target}`;
836
+ if (ctx.hasUI) ctx.ui.notify(msg, "info");
837
+ },
838
+ });
839
+
676
840
  pi.registerCommand("harness-use-run", {
677
841
  description: "Point this session at an existing run directory (recovery)",
678
842
  handler: async (args, ctx) => {
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { createHash } from "node:crypto";
11
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
11
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
12
  import {
13
13
  captureHarnessEvent,
14
14
  type HarnessPostHogEventName,
@@ -2,7 +2,7 @@
2
2
  * harness-web-guard — block bash that bypasses web_search / web_fetch tools.
3
3
  */
4
4
 
5
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
6
 
7
7
  const BLOCK_REASON =
8
8
  "harness-web-guard: use web_search (SERP) or web_fetch (page content) instead of raw curl/wget/firecrawl/scrapling fetch. " +
@@ -2,7 +2,7 @@
2
2
  * harness-web-tools — web_search + web_fetch pi tools wrapping harness-web.py.
3
3
  */
4
4
 
5
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
6
  import { Type } from "@sinclair/typebox";
7
7
  import {
8
8
  harnessWebContextLine,
@@ -1,11 +1,11 @@
1
- import type { ExtensionUIContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  Editor,
4
4
  type EditorTheme,
5
5
  Key,
6
6
  matchesKey,
7
7
  truncateToWidth,
8
- } from "@mariozechner/pi-tui";
8
+ } from "@earendil-works/pi-tui";
9
9
  import type { AskResponse, DialogResult, ValidatedAskParams } from "./types.js";
10
10
 
11
11
  type DisplayOption = {
@@ -1,4 +1,4 @@
1
- import type { ExtensionUIContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { DialogResult, ValidatedAskParams } from "./types.js";
3
3
 
4
4
  export async function runAskFallback(