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
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - `.pi/harness/active-run.json` (cross-session pointer)
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
9
|
+
import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
|
|
10
10
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
11
11
|
|
|
12
12
|
export type HarnessPhase =
|
|
@@ -114,6 +114,412 @@ export function canonicalPlanPath(runId: string, projectRoot: string): string {
|
|
|
114
114
|
return join(harnessRunsRoot(projectRoot), runId, "plan-packet.json");
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
const PLAN_PACKET_BASENAME = "plan-packet.json";
|
|
118
|
+
|
|
119
|
+
const MUTATING_FILE_TOOLS = new Set(["write", "edit"]);
|
|
120
|
+
|
|
121
|
+
const PLAN_APPROVE_OPTION =
|
|
122
|
+
/^(approve(d)?(\s+plan)?|yes,?\s+proceed|looks\s+good)$/i;
|
|
123
|
+
const PLAN_CANCEL_OPTION =
|
|
124
|
+
/^(cancel(led)?|revise|request\s+changes|needs?\s+clarification)$/i;
|
|
125
|
+
|
|
126
|
+
export interface PlanUserApproval {
|
|
127
|
+
plan_id: string | null;
|
|
128
|
+
approved_at: string;
|
|
129
|
+
source: "ask_user" | "harness-plan-approval" | "noninteractive";
|
|
130
|
+
}
|
|
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
|
+
|
|
162
|
+
export interface PlanPhaseMutationDecision {
|
|
163
|
+
allowed: boolean;
|
|
164
|
+
reason?: string;
|
|
165
|
+
isScopedPlanWrite?: boolean;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Resolve path relative to project root when not absolute. */
|
|
169
|
+
export function normalizeHarnessPath(
|
|
170
|
+
path: string,
|
|
171
|
+
projectRoot: string,
|
|
172
|
+
): string {
|
|
173
|
+
const trimmed = path.trim();
|
|
174
|
+
if (!trimmed) return resolve(projectRoot);
|
|
175
|
+
if (isAbsolute(trimmed)) return resolve(trimmed);
|
|
176
|
+
return resolve(projectRoot, trimmed);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function isCanonicalPlanPacketPath(
|
|
180
|
+
absPath: string,
|
|
181
|
+
projectRoot: string,
|
|
182
|
+
runId: string,
|
|
183
|
+
): boolean {
|
|
184
|
+
const expected = resolve(canonicalPlanPath(runId, projectRoot));
|
|
185
|
+
return resolve(absPath) === expected;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function extractWritePathFromToolInput(
|
|
189
|
+
input: Record<string, unknown>,
|
|
190
|
+
): string {
|
|
191
|
+
const raw =
|
|
192
|
+
(typeof input.path === "string" && input.path) ||
|
|
193
|
+
(typeof input.filePath === "string" && input.filePath) ||
|
|
194
|
+
"";
|
|
195
|
+
return raw.trim();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** True when absPath is the canonical plan-packet.json for the active run. */
|
|
199
|
+
export async function isPlanPhaseScopedWrite(
|
|
200
|
+
absPath: string,
|
|
201
|
+
runCtx: HarnessRunContext | null,
|
|
202
|
+
projectRoot: string,
|
|
203
|
+
): Promise<boolean> {
|
|
204
|
+
if (!runCtx?.run_id) return false;
|
|
205
|
+
let resolved: string;
|
|
206
|
+
try {
|
|
207
|
+
resolved = await realpath(normalizeHarnessPath(absPath, projectRoot));
|
|
208
|
+
} catch {
|
|
209
|
+
resolved = normalizeHarnessPath(absPath, projectRoot);
|
|
210
|
+
}
|
|
211
|
+
const runsRoot = resolve(harnessRunsRoot(projectRoot));
|
|
212
|
+
let runsReal: string;
|
|
213
|
+
try {
|
|
214
|
+
runsReal = await realpath(runsRoot);
|
|
215
|
+
} catch {
|
|
216
|
+
runsReal = runsRoot;
|
|
217
|
+
}
|
|
218
|
+
const rel = relative(runsReal, resolved);
|
|
219
|
+
if (rel.startsWith("..") || isAbsolute(rel)) return false;
|
|
220
|
+
const parts = rel.split(/[/\\]/);
|
|
221
|
+
if (parts.length !== 2 || parts[1] !== PLAN_PACKET_BASENAME) return false;
|
|
222
|
+
if (parts[0] !== runCtx.run_id) return false;
|
|
223
|
+
return isCanonicalPlanPacketPath(resolved, projectRoot, runCtx.run_id);
|
|
224
|
+
}
|
|
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
|
+
|
|
249
|
+
export function indexOfLastPlanCommand(entries: unknown[]): number {
|
|
250
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
251
|
+
const entry = entries[i] as SessionEntryLike & {
|
|
252
|
+
message?: { role?: string; content?: string | unknown[] };
|
|
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
|
+
}
|
|
260
|
+
if (
|
|
261
|
+
entry.type === "custom" &&
|
|
262
|
+
entry.customType === "harness-plan-attempt"
|
|
263
|
+
) {
|
|
264
|
+
return i;
|
|
265
|
+
}
|
|
266
|
+
if (entry.type !== "message" || entry.message?.role !== "user") continue;
|
|
267
|
+
const content = entry.message.content;
|
|
268
|
+
const text =
|
|
269
|
+
typeof content === "string"
|
|
270
|
+
? content
|
|
271
|
+
: Array.isArray(content)
|
|
272
|
+
? content
|
|
273
|
+
.filter(
|
|
274
|
+
(c): c is { type: string; text?: string } =>
|
|
275
|
+
typeof c === "object" &&
|
|
276
|
+
c !== null &&
|
|
277
|
+
(c as { type?: string }).type === "text",
|
|
278
|
+
)
|
|
279
|
+
.map((c) => c.text ?? "")
|
|
280
|
+
.join("\n")
|
|
281
|
+
: "";
|
|
282
|
+
const visible = userVisiblePromptSlice(text);
|
|
283
|
+
const parsed = parseHarnessSlashInput(visible);
|
|
284
|
+
if (
|
|
285
|
+
parsed?.command === "harness-plan" ||
|
|
286
|
+
parsed?.command === "harness-auto"
|
|
287
|
+
) {
|
|
288
|
+
return i;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return -1;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function parseAskUserApprovalFromMessage(msg: {
|
|
295
|
+
toolName?: string;
|
|
296
|
+
details?: unknown;
|
|
297
|
+
content?: { type?: string; text?: string }[];
|
|
298
|
+
}): 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;
|
|
310
|
+
if (details?.cancelled) return null;
|
|
311
|
+
const response = details?.response;
|
|
312
|
+
if (!response) return null;
|
|
313
|
+
if (response.kind === "freeform") {
|
|
314
|
+
const text = (response.text ?? "").trim();
|
|
315
|
+
if (/^approve(d)?\b/i.test(text)) {
|
|
316
|
+
return {
|
|
317
|
+
plan_id: null,
|
|
318
|
+
approved_at: nowIso(),
|
|
319
|
+
source: "ask_user",
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
const selection = (response.selections?.[0] ?? "").trim();
|
|
325
|
+
if (!selection || PLAN_CANCEL_OPTION.test(selection)) return null;
|
|
326
|
+
if (PLAN_APPROVE_OPTION.test(selection)) {
|
|
327
|
+
return {
|
|
328
|
+
plan_id: null,
|
|
329
|
+
approved_at: nowIso(),
|
|
330
|
+
source: "ask_user",
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function getLatestPlanUserApproval(
|
|
337
|
+
entries: unknown[],
|
|
338
|
+
sinceIndex = 0,
|
|
339
|
+
): PlanUserApproval | null {
|
|
340
|
+
for (let i = entries.length - 1; i >= sinceIndex; i--) {
|
|
341
|
+
const entry = entries[i] as SessionEntryLike & {
|
|
342
|
+
message?: {
|
|
343
|
+
role?: string;
|
|
344
|
+
toolName?: string;
|
|
345
|
+
details?: unknown;
|
|
346
|
+
content?: { type?: string; text?: string }[];
|
|
347
|
+
};
|
|
348
|
+
};
|
|
349
|
+
if (
|
|
350
|
+
entry.type === "custom" &&
|
|
351
|
+
entry.customType === "harness-plan-approval"
|
|
352
|
+
) {
|
|
353
|
+
const data = entry.data as Partial<PlanUserApproval> | undefined;
|
|
354
|
+
if (data?.approved_at) {
|
|
355
|
+
return {
|
|
356
|
+
plan_id: typeof data.plan_id === "string" ? data.plan_id : null,
|
|
357
|
+
approved_at: data.approved_at,
|
|
358
|
+
source:
|
|
359
|
+
data.source === "noninteractive"
|
|
360
|
+
? "noninteractive"
|
|
361
|
+
: "harness-plan-approval",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (entry.type !== "message" || entry.message?.role !== "toolResult") {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const fromAsk = parseAskUserApprovalFromMessage(entry.message);
|
|
369
|
+
if (fromAsk) return fromAsk;
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function hasPlanUserApproval(
|
|
375
|
+
entries: unknown[],
|
|
376
|
+
opts?: { planId?: string | null; sincePlanCommand?: boolean },
|
|
377
|
+
): boolean {
|
|
378
|
+
if (process.env.HARNESS_PLAN_NONINTERACTIVE === "1") {
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
const since = opts?.sincePlanCommand
|
|
382
|
+
? Math.max(0, indexOfLastPlanCommand(entries))
|
|
383
|
+
: 0;
|
|
384
|
+
const approval = getLatestPlanUserApproval(entries, since);
|
|
385
|
+
if (!approval) return false;
|
|
386
|
+
if (opts?.planId && approval.plan_id && approval.plan_id !== opts.planId) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function isHarnessAutoSession(entries: unknown[]): boolean {
|
|
393
|
+
const since = indexOfLastPlanCommand(entries);
|
|
394
|
+
if (since < 0) return false;
|
|
395
|
+
for (let i = since; i < entries.length; i++) {
|
|
396
|
+
const entry = entries[i] as SessionEntryLike & {
|
|
397
|
+
message?: { role?: string; content?: string };
|
|
398
|
+
};
|
|
399
|
+
if (entry.type !== "message" || entry.message?.role !== "user") continue;
|
|
400
|
+
const text =
|
|
401
|
+
typeof entry.message.content === "string"
|
|
402
|
+
? userVisiblePromptSlice(entry.message.content)
|
|
403
|
+
: "";
|
|
404
|
+
const parsed = parseHarnessSlashInput(text);
|
|
405
|
+
if (parsed?.command === "harness-auto") return true;
|
|
406
|
+
}
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export async function isPlanPhaseAllowedMutation(
|
|
411
|
+
toolName: string,
|
|
412
|
+
input: Record<string, unknown>,
|
|
413
|
+
phase: HarnessPhase,
|
|
414
|
+
runCtx: HarnessRunContext | null,
|
|
415
|
+
projectRoot: string,
|
|
416
|
+
opts: {
|
|
417
|
+
aborted: boolean;
|
|
418
|
+
entries: unknown[];
|
|
419
|
+
ownerSessionId?: string;
|
|
420
|
+
currentSessionId?: string;
|
|
421
|
+
},
|
|
422
|
+
): Promise<PlanPhaseMutationDecision> {
|
|
423
|
+
if (!MUTATING_FILE_TOOLS.has(toolName)) {
|
|
424
|
+
if (phase === "execute" || phase === "merge") {
|
|
425
|
+
return { allowed: true };
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
allowed: false,
|
|
429
|
+
reason: `policy-gate: ${toolName} blocked in phase '${phase}'.`,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (
|
|
434
|
+
runCtx?.owner_pi_session_id &&
|
|
435
|
+
opts.currentSessionId &&
|
|
436
|
+
runCtx.owner_pi_session_id !== opts.currentSessionId
|
|
437
|
+
) {
|
|
438
|
+
return {
|
|
439
|
+
allowed: false,
|
|
440
|
+
reason:
|
|
441
|
+
"harness-run-context: this session does not own the active run; plan writes are read-only here.",
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const target = extractWritePathFromToolInput(input);
|
|
446
|
+
if (!target) {
|
|
447
|
+
return {
|
|
448
|
+
allowed: false,
|
|
449
|
+
reason: "policy-gate: write/edit requires a path.",
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const scoped = runCtx
|
|
454
|
+
? await isPlanPhaseScopedWrite(target, runCtx, projectRoot)
|
|
455
|
+
: false;
|
|
456
|
+
|
|
457
|
+
if (scoped) {
|
|
458
|
+
if (!runCtx) {
|
|
459
|
+
return {
|
|
460
|
+
allowed: false,
|
|
461
|
+
reason:
|
|
462
|
+
'policy-gate: no active harness run. Run /harness-plan "<task>" first.',
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (
|
|
466
|
+
!hasPlanUserApproval(opts.entries, {
|
|
467
|
+
sincePlanCommand: true,
|
|
468
|
+
planId: runCtx.plan_id,
|
|
469
|
+
})
|
|
470
|
+
) {
|
|
471
|
+
return {
|
|
472
|
+
allowed: false,
|
|
473
|
+
isScopedPlanWrite: true,
|
|
474
|
+
reason:
|
|
475
|
+
"policy-gate: plan-packet.json write blocked until the user approves via ask_user (present the full plan, then Approve).",
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
if (opts.aborted) {
|
|
479
|
+
return { allowed: true, isScopedPlanWrite: true };
|
|
480
|
+
}
|
|
481
|
+
if (phase === "plan") {
|
|
482
|
+
return { allowed: true, isScopedPlanWrite: true };
|
|
483
|
+
}
|
|
484
|
+
if (phase === "execute" || phase === "merge") {
|
|
485
|
+
return { allowed: true, isScopedPlanWrite: true };
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
allowed: false,
|
|
489
|
+
isScopedPlanWrite: true,
|
|
490
|
+
reason: `harness-run-context: plan-packet.json is read-only in phase '${phase}'.`,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (opts.aborted) {
|
|
495
|
+
return {
|
|
496
|
+
allowed: false,
|
|
497
|
+
reason:
|
|
498
|
+
"policy-gate: mutating tool blocked because harness-abort lock is active. Attach a new approved plan via plan-packet.json first.",
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (phase === "execute" || phase === "merge") {
|
|
503
|
+
return { allowed: true };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (phase === "plan" && !runCtx) {
|
|
507
|
+
return {
|
|
508
|
+
allowed: false,
|
|
509
|
+
reason:
|
|
510
|
+
'policy-gate: no active harness run. Run /harness-plan "<task>" first.',
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const allowedPath = runCtx?.run_id
|
|
515
|
+
? canonicalPlanPath(runCtx.run_id, projectRoot)
|
|
516
|
+
: ".pi/harness/runs/<run_id>/plan-packet.json";
|
|
517
|
+
return {
|
|
518
|
+
allowed: false,
|
|
519
|
+
reason: `policy-gate: ${toolName} blocked in phase '${phase}'. In plan phase only ${allowedPath} is writable after ask_user approval.`,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
117
523
|
export function allocateRunId(sessionId: string): string {
|
|
118
524
|
return `${sessionId}-${Date.now()}`;
|
|
119
525
|
}
|
|
@@ -122,18 +528,16 @@ export function nowIso(): string {
|
|
|
122
528
|
return new Date().toISOString();
|
|
123
529
|
}
|
|
124
530
|
|
|
531
|
+
/** @deprecated Use parseHarnessSlashInput on raw `input` event text only. */
|
|
125
532
|
export function isHarnessSlashCommand(prompt: string): boolean {
|
|
126
|
-
|
|
127
|
-
if (!trimmed.startsWith("/harness-")) return false;
|
|
128
|
-
const match = trimmed.match(/^\/(harness-[a-z0-9-]+)/);
|
|
129
|
-
if (!match) return false;
|
|
130
|
-
return HARNESS_COMMANDS.has(match[1]);
|
|
533
|
+
return parseHarnessSlashInput(prompt) !== null;
|
|
131
534
|
}
|
|
132
535
|
|
|
133
|
-
|
|
134
|
-
|
|
536
|
+
/** Parse raw user input before prompt-template expansion (`input` hook only). */
|
|
537
|
+
export function parseHarnessSlashInput(
|
|
538
|
+
text: string,
|
|
135
539
|
): { command: string; args: string } | null {
|
|
136
|
-
const trimmed =
|
|
540
|
+
const trimmed = text.trim();
|
|
137
541
|
const match = trimmed.match(/^\/(harness-[a-z0-9-]+)(?:\s+([\s\S]*))?$/);
|
|
138
542
|
if (!match) return null;
|
|
139
543
|
const command = match[1];
|
|
@@ -141,6 +545,13 @@ export function parseHarnessSlashCommand(
|
|
|
141
545
|
return { command, args: (match[2] ?? "").trim() };
|
|
142
546
|
}
|
|
143
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
|
+
|
|
144
555
|
/** User-visible prompt slice for policy signals (exclude injected blocks). */
|
|
145
556
|
export function userVisiblePromptSlice(prompt: string): string {
|
|
146
557
|
const markers = [
|
|
@@ -373,7 +784,32 @@ export function planPacketSummary(
|
|
|
373
784
|
};
|
|
374
785
|
}
|
|
375
786
|
|
|
376
|
-
export function
|
|
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 {
|
|
377
813
|
const lines = [
|
|
378
814
|
"[HarnessRunContext]",
|
|
379
815
|
`run_id=${ctx.run_id}`,
|
|
@@ -388,6 +824,12 @@ export function formatPlanContextBlock(ctx: HarnessRunContext): string {
|
|
|
388
824
|
if (ctx.plan_packet_path) {
|
|
389
825
|
lines.push(`plan_packet_path=${ctx.plan_packet_path}`);
|
|
390
826
|
}
|
|
827
|
+
if (ctx.task_summary) {
|
|
828
|
+
lines.push(`task_summary=${ctx.task_summary}`);
|
|
829
|
+
}
|
|
830
|
+
lines.push(
|
|
831
|
+
`HarnessSpawnContext=${buildHarnessSpawnContextSnippet(ctx, opts)}`,
|
|
832
|
+
);
|
|
391
833
|
return lines.join("\n");
|
|
392
834
|
}
|
|
393
835
|
|
|
@@ -471,13 +913,11 @@ export function validatePlanOverridePath(
|
|
|
471
913
|
runId: string,
|
|
472
914
|
projectRoot: string,
|
|
473
915
|
): { ok: boolean; reason?: string } {
|
|
474
|
-
const absPlan =
|
|
475
|
-
|
|
476
|
-
const rel = relative(runsDir, absPlan);
|
|
477
|
-
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
916
|
+
const absPlan = normalizeHarnessPath(planPath, projectRoot);
|
|
917
|
+
if (!isCanonicalPlanPacketPath(absPlan, projectRoot, runId)) {
|
|
478
918
|
return {
|
|
479
919
|
ok: false,
|
|
480
|
-
reason: `--plan must be
|
|
920
|
+
reason: `--plan must be runs/${runId}/plan-packet.json (canonical plan packet only)`,
|
|
481
921
|
};
|
|
482
922
|
}
|
|
483
923
|
return { ok: true };
|
|
@@ -505,7 +945,7 @@ export function shouldReuseHarnessRunId(
|
|
|
505
945
|
ctx: HarnessRunContext | null,
|
|
506
946
|
command: string | null,
|
|
507
947
|
): boolean {
|
|
508
|
-
if (!command
|
|
948
|
+
if (!command) return false;
|
|
509
949
|
if (command === "harness-new-run") return false;
|
|
510
950
|
if (!ctx) return false;
|
|
511
951
|
if (command === "harness-plan" || command === "harness-auto") {
|
|
@@ -530,27 +970,43 @@ export interface HarnessPolicyState {
|
|
|
530
970
|
aborted: boolean;
|
|
531
971
|
}
|
|
532
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. */
|
|
533
996
|
export function inferHarnessPhaseFromPrompt(prompt: string): HarnessPhase {
|
|
534
|
-
const p = prompt.toLowerCase();
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
p.includes("/harness-auto") ||
|
|
539
|
-
p.includes("harness-auto")
|
|
540
|
-
) {
|
|
541
|
-
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];
|
|
542
1001
|
}
|
|
543
|
-
if (p.
|
|
544
|
-
|
|
545
|
-
return "evaluate";
|
|
1002
|
+
if (p.startsWith("/harness-plan") || p.startsWith("/harness-auto")) {
|
|
1003
|
+
return "plan";
|
|
546
1004
|
}
|
|
547
|
-
if (p.
|
|
1005
|
+
if (p.startsWith("/harness-run")) return "execute";
|
|
1006
|
+
if (p.startsWith("/harness-eval") || p.startsWith("/harness-review")) {
|
|
548
1007
|
return "evaluate";
|
|
549
1008
|
}
|
|
550
|
-
if (p.
|
|
551
|
-
return "adversary";
|
|
552
|
-
}
|
|
553
|
-
if (p.includes("adversary")) return "adversary";
|
|
1009
|
+
if (p.startsWith("/harness-critic")) return "adversary";
|
|
554
1010
|
if (p.includes("merge gate") || p.includes("policy decision")) return "merge";
|
|
555
1011
|
return "execute";
|
|
556
1012
|
}
|
|
@@ -569,8 +1025,8 @@ export function isValidHarnessPhaseTransition(
|
|
|
569
1025
|
|
|
570
1026
|
export function getLatestPolicyState(entries: unknown[]): HarnessPolicyState {
|
|
571
1027
|
const fallback: HarnessPolicyState = {
|
|
572
|
-
phase: "
|
|
573
|
-
approvedPlan:
|
|
1028
|
+
phase: "plan",
|
|
1029
|
+
approvedPlan: false,
|
|
574
1030
|
planId: null,
|
|
575
1031
|
aborted: false,
|
|
576
1032
|
};
|
|
@@ -625,7 +1081,7 @@ export function getPolicyTransitionBlock(
|
|
|
625
1081
|
return { blocked: false };
|
|
626
1082
|
}
|
|
627
1083
|
const state = getLatestPolicyState(entries);
|
|
628
|
-
const nextPhase =
|
|
1084
|
+
const nextPhase = inferHarnessPhase(entries, userPrompt);
|
|
629
1085
|
if (!isValidHarnessPhaseTransition(state.phase, nextPhase)) {
|
|
630
1086
|
return {
|
|
631
1087
|
blocked: true,
|
|
@@ -669,7 +1125,7 @@ export function isNewTaskPlanBlocked(
|
|
|
669
1125
|
): boolean {
|
|
670
1126
|
if (ctx.status !== "active") return false;
|
|
671
1127
|
if (isAmendPlanAllowed(ctx, prompt, false)) return false;
|
|
672
|
-
const cmd =
|
|
1128
|
+
const cmd = parseHarnessSlashInput(userVisiblePromptSlice(prompt));
|
|
673
1129
|
if (cmd?.command !== "harness-plan") return false;
|
|
674
1130
|
const taskMatch = prompt.match(/"([^"]+)"/);
|
|
675
1131
|
if (!taskMatch || !ctx.task_summary) return true;
|
|
@@ -701,7 +1157,7 @@ export function nextStepAfterOutcome(input: {
|
|
|
701
1157
|
return "/harness-plan or /harness-abort";
|
|
702
1158
|
}
|
|
703
1159
|
if (exec === "completed") {
|
|
704
|
-
return "
|
|
1160
|
+
return "/harness-eval";
|
|
705
1161
|
}
|
|
706
1162
|
}
|
|
707
1163
|
if (input.phase === "evaluate") {
|
|
@@ -792,3 +1248,26 @@ export function driftGateActive(entries: unknown[]): boolean {
|
|
|
792
1248
|
export function phaseTraceFileName(phase: HarnessPhase): string {
|
|
793
1249
|
return `trace-${phase}.json`;
|
|
794
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
|
+
}
|