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.
- package/.agents/skills/harness-decisions/SKILL.md +20 -1
- package/.agents/skills/harness-eval/SKILL.md +11 -13
- package/.agents/skills/harness-orchestration/SKILL.md +36 -30
- package/.agents/skills/harness-plan/SKILL.md +13 -18
- package/.pi/PACKAGING.md +1 -1
- package/.pi/agents/harness/adversary.md +20 -12
- package/.pi/agents/harness/evaluator.md +25 -14
- package/.pi/agents/harness/executor.md +27 -16
- package/.pi/agents/harness/incident-recorder.md +37 -0
- package/.pi/agents/harness/meta-optimizer.md +18 -15
- package/.pi/agents/harness/planner.md +26 -30
- package/.pi/agents/harness/tie-breaker.md +4 -2
- package/.pi/agents/harness/trace-librarian.md +18 -11
- package/.pi/agents/pi-pi/ext-expert.md +1 -1
- package/.pi/agents/pi-pi/keybinding-expert.md +1 -1
- package/.pi/agents/pi-pi/tui-expert.md +3 -3
- package/.pi/extensions/00-ultimate-pi-system-prompt.ts +2 -2
- package/.pi/extensions/budget-guard.ts +47 -18
- package/.pi/extensions/custom-footer.ts +8 -3
- package/.pi/extensions/custom-header.ts +2 -2
- package/.pi/extensions/debate-orchestrator.ts +1 -1
- package/.pi/extensions/dotenv-loader.ts +1 -1
- package/.pi/extensions/drift-monitor.ts +1 -1
- package/.pi/extensions/harness-ask-user.ts +1 -1
- package/.pi/extensions/harness-live-widget.ts +1 -1
- package/.pi/extensions/harness-run-context.ts +197 -33
- package/.pi/extensions/harness-telemetry.ts +1 -1
- package/.pi/extensions/harness-web-guard.ts +1 -1
- package/.pi/extensions/harness-web-tools.ts +1 -1
- package/.pi/extensions/lib/ask-user/dialog.ts +2 -2
- package/.pi/extensions/lib/ask-user/fallback.ts +1 -1
- package/.pi/extensions/lib/ask-user/render.ts +3 -3
- package/.pi/extensions/lib/harness-subagents/agent-loader.ts +1 -1
- package/.pi/extensions/lib/harness-subagents/agent-parser.ts +1 -1
- package/.pi/extensions/lib/harness-subagents/blackboard-tool.ts +1 -1
- package/.pi/extensions/lib/harness-subagents/harness-subagent-policy.ts +134 -0
- package/.pi/extensions/lib/harness-subagents/parent-ask-user-bridge.ts +89 -0
- package/.pi/extensions/lib/harness-subagents/spawn-policy.ts +20 -2
- package/.pi/extensions/lib/harness-subagents/vendored/agent-manager.ts +3 -2
- package/.pi/extensions/lib/harness-subagents/vendored/agent-runner.ts +44 -24
- package/.pi/extensions/lib/harness-subagents/vendored/context.ts +1 -1
- package/.pi/extensions/lib/harness-subagents/vendored/env.ts +1 -1
- package/.pi/extensions/lib/harness-subagents/vendored/index.ts +23 -2
- package/.pi/extensions/lib/harness-subagents/vendored/output-file.ts +1 -1
- package/.pi/extensions/lib/harness-subagents/vendored/schedule.ts +1 -1
- package/.pi/extensions/lib/harness-subagents/vendored/settings.ts +1 -1
- package/.pi/extensions/lib/harness-subagents/vendored/skill-loader.ts +1 -1
- package/.pi/extensions/lib/harness-subagents/vendored/types.ts +2 -2
- package/.pi/extensions/lib/harness-subagents/vendored/ui/agent-widget.ts +1 -1
- package/.pi/extensions/lib/harness-subagents/vendored/ui/conversation-viewer.ts +2 -2
- package/.pi/extensions/lib/harness-subagents/vendored/ui/schedule-menu.ts +1 -1
- package/.pi/extensions/observation-bus.ts +1 -1
- package/.pi/extensions/pi-model-router-harness.ts +1 -1
- package/.pi/extensions/policy-gate.ts +90 -20
- package/.pi/extensions/provider-payload-sanitize.ts +1 -1
- package/.pi/extensions/review-integrity.ts +76 -22
- package/.pi/extensions/sentrux-rules-sync.ts +1 -1
- package/.pi/extensions/soundboard.ts +1 -1
- package/.pi/extensions/test-diff-integrity.ts +1 -1
- package/.pi/extensions/trace-recorder.ts +1 -1
- package/.pi/extensions/ultimate-pi-vcc.ts +1 -1
- package/.pi/harness/agents.manifest.json +82 -78
- package/.pi/harness/docs/adrs/0031-harness-run-context.md +6 -3
- package/.pi/harness/docs/adrs/0032-harness-command-orchestration.md +37 -0
- package/.pi/harness/docs/adrs/README.md +1 -0
- package/.pi/harness/specs/budget-exhausted-event.schema.json +3 -1
- package/.pi/harness/specs/harness-spawn-context.schema.json +65 -0
- package/.pi/harness/specs/harness-turn.schema.json +18 -0
- package/.pi/lib/harness-agent-output.ts +41 -0
- package/.pi/lib/harness-run-context.ts +516 -37
- package/.pi/lib/harness-ui-state.ts +1 -1
- package/.pi/prompts/harness-auto.md +36 -61
- package/.pi/prompts/harness-critic.md +15 -28
- package/.pi/prompts/harness-eval.md +19 -27
- package/.pi/prompts/harness-incident.md +15 -34
- package/.pi/prompts/harness-plan.md +28 -49
- package/.pi/prompts/harness-review.md +16 -30
- package/.pi/prompts/harness-router-tune.md +16 -38
- package/.pi/prompts/harness-run.md +21 -38
- package/.pi/prompts/harness-setup.md +2 -0
- package/.pi/prompts/harness-trace.md +13 -30
- package/.pi/scripts/harness-generate-model-router.mjs +16 -13
- package/.pi/scripts/harness-verify.mjs +17 -0
- package/.pi/scripts/vendor-sync-pi-model-router.sh +10 -10
- package/CHANGELOG.md +25 -1
- package/README.md +4 -5
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/package.json +13 -8
- package/vendor/pi-model-router/UPSTREAM_PIN.md +1 -1
- package/vendor/pi-model-router/extensions/commands.ts +2 -2
- package/vendor/pi-model-router/extensions/config.ts +2 -2
- package/vendor/pi-model-router/extensions/index.ts +1 -1
- package/vendor/pi-model-router/extensions/provider.ts +2 -2
- package/vendor/pi-model-router/extensions/routing.ts +2 -2
- package/vendor/pi-model-router/extensions/types.ts +1 -1
- package/vendor/pi-model-router/extensions/ui.ts +1 -1
- package/vendor/pi-model-router/package.json +4 -4
- package/vendor/pi-vcc/index.ts +1 -1
- package/vendor/pi-vcc/package.json +1 -1
- package/vendor/pi-vcc/src/commands/pi-vcc.ts +1 -1
- package/vendor/pi-vcc/src/commands/vcc-recall.ts +1 -1
- package/vendor/pi-vcc/src/core/content.ts +1 -1
- package/vendor/pi-vcc/src/core/load-messages.ts +1 -1
- package/vendor/pi-vcc/src/core/normalize.ts +1 -1
- package/vendor/pi-vcc/src/core/render-entries.ts +1 -1
- package/vendor/pi-vcc/src/core/report.ts +1 -1
- package/vendor/pi-vcc/src/core/search-entries.ts +1 -1
- package/vendor/pi-vcc/src/core/summarize.ts +1 -1
- package/vendor/pi-vcc/src/hooks/before-compact.ts +2 -2
- package/vendor/pi-vcc/src/tools/recall.ts +1 -1
- package/vendor/pi-vcc/src/types.ts +1 -1
- package/vendor/pi-vcc/tests/fixtures.ts +1 -1
- package/vendor/pi-vcc/tests/render-entries.test.ts +1 -1
- package/vendor/pi-vcc/tests/search-entries.test.ts +1 -1
- 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: `@
|
|
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 @
|
|
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 @
|
|
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 @
|
|
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 @
|
|
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 "@
|
|
17
|
-
import { formatSkillsForPrompt } from "@
|
|
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 "@
|
|
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 ?? "
|
|
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
|
-
|
|
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:
|
|
231
|
+
run_id: runId,
|
|
213
232
|
debate_id: `${phase}-budget-guard`,
|
|
214
233
|
round_count: 1,
|
|
215
|
-
budget_used:
|
|
216
|
-
exhaustion_reason:
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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 "@
|
|
13
|
-
import type { TUI } from "@
|
|
14
|
-
import {
|
|
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 "@
|
|
10
|
-
import { truncateToWidth } from "@
|
|
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 "@
|
|
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 "@
|
|
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 "@
|
|
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 "@
|
|
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";
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* in before_agent_start so trace-recorder reuses it on agent_start.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
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
|
-
|
|
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(
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
if (
|
|
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
|
|
95
|
+
approvedPlan,
|
|
77
96
|
planId,
|
|
78
97
|
budgetBypass: false,
|
|
79
98
|
aborted: false,
|
|
80
99
|
abortReason: null,
|
|
81
100
|
abortedAt: null,
|
|
82
|
-
updatedAt:
|
|
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
|
|
189
|
+
const turn = getLatestHarnessTurn(entries);
|
|
190
|
+
const parsed = turn
|
|
191
|
+
? { command: turn.command, args: turn.args }
|
|
192
|
+
: parseHarnessSlashInput(userPrompt);
|
|
153
193
|
const harnessTurn =
|
|
154
|
-
|
|
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
|
-
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
523
|
-
|
|
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) => {
|
|
@@ -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 "@
|
|
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 "@
|
|
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 "@
|
|
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 "@
|
|
8
|
+
} from "@earendil-works/pi-tui";
|
|
9
9
|
import type { AskResponse, DialogResult, ValidatedAskParams } from "./types.js";
|
|
10
10
|
|
|
11
11
|
type DisplayOption = {
|