pi-better-harness 0.40.0 → 0.40.2
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/extensions/execution-policy.ts +54 -14
- package/node_modules/pi-better-background-tasks/package.json +1 -1
- package/node_modules/pi-better-background-tasks/src/index.ts +5 -2
- package/node_modules/pi-better-background-tasks/src/navigator-provider.ts +24 -3
- package/node_modules/pi-better-background-tasks/src/observed-waits.ts +3 -1
- package/node_modules/pi-better-background-tasks/src/process-identity.ts +108 -31
- package/node_modules/pi-better-background-tasks/src/prompt.ts +72 -0
- package/node_modules/pi-better-background-tasks/src/shared-execution-policy.ts +54 -14
- package/node_modules/pi-better-background-tasks/src/supervisor/generated/process-identity.mjs +75 -5
- package/node_modules/pi-better-background-tasks/src/supervisor/supervisor.ts +76 -2
- package/node_modules/pi-better-background-tasks/src/supervisor/types.ts +10 -0
- package/node_modules/pi-better-goal/package.json +1 -1
- package/node_modules/pi-better-goal/src/index.ts +31 -32
- package/node_modules/pi-better-goal/src/prompt.ts +53 -0
- package/node_modules/pi-better-subagents/child-ownership.ts +14 -1
- package/node_modules/pi-better-subagents/effective-settings.ts +80 -6
- package/node_modules/pi-better-subagents/index.ts +5 -14
- package/node_modules/pi-better-subagents/package.json +1 -1
- package/node_modules/pi-better-subagents/shared-execution-policy.ts +54 -14
- package/package.json +4 -4
- package/extensions/task-context/shared-execution-policy.ts +0 -19
|
@@ -1,20 +1,60 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Harness-wide scheduling guidance, vendored so standalone extensions share it.
|
|
3
|
+
*
|
|
4
|
+
* Published as a named system-prompt section, never as appended text. A
|
|
5
|
+
* `before_agent_start` handler that returns `systemPrompt` sets
|
|
6
|
+
* `forceSystemPrompt`, and a forced prompt is opaque to pi: `buildSystemPromptState`
|
|
7
|
+
* returns `{ content }` with no sections while `diffSystemPromptSections` keeps
|
|
8
|
+
* diffing the sections the request therefore never shows. The request carries a
|
|
9
|
+
* rendered snapshot, so any section published after that snapshot never reaches
|
|
10
|
+
* it, so every package that speaks here publishes a section.
|
|
11
|
+
*/
|
|
12
|
+
export const EXECUTION_POLICY_SECTION = "harness_execution_policy";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The minimum shape this module needs. `sections` stays optional because this
|
|
16
|
+
* repo typechecks against older pi typings whose event options predate sections;
|
|
17
|
+
* the shipped runtime always supplies the map.
|
|
18
|
+
*/
|
|
19
|
+
export interface PromptSectionTarget {
|
|
20
|
+
sections?: Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const EXECUTION_POLICY_TEXT: string = [
|
|
3
24
|
"Harness execution policy: concurrency is separate from subagent delegation.",
|
|
4
|
-
"Before multi-unit work, identify ready, authorized, independent units, actual prerequisites, shared mutable resources, and capacity or availability constraints. Represent independently verifiable
|
|
25
|
+
"Before multi-unit work, identify ready, authorized, independent units, actual prerequisites, shared mutable resources, and capacity or availability constraints. Represent each independently verifiable deliverable as its own plan step; do not create dependencies from list order.",
|
|
5
26
|
"Start all ready, authorized, non-conflicting units concurrently within known resource limits before waiting for any one to finish. Reassess ready work after launches and results; continue unblocked foreground work instead of polling running tasks. Do not equate one background launch with scheduling all ready work.",
|
|
6
27
|
"Serialize only for a concrete dependency, conflicting writes or ownership, resource or concurrency limit, documented rollout gate, or explicit availability requirement. Record the reason in the plan or explain it to the user. Do not invent a canary or host-by-host availability requirement.",
|
|
7
|
-
"If concurrency raises an unresolved material operational trade-off
|
|
8
|
-
"Choose the execution mechanism after deciding what can run together: foreground tools for small, tightly coupled, or interactive work; subagents for bounded independent reasoning or implementation when the active delegation mode permits; background tasks for independent long-running commands or waits. Manual delegation forbids proactive subagents, not concurrent foreground tools or background commands.
|
|
9
|
-
"Established command-only jobs and external waits belong to background operations, including in coordinator mode. Use bg_task_spawn for a long command
|
|
10
|
-
"If a worker hands back a command,
|
|
28
|
+
"If concurrency raises an unresolved material operational trade-off, ask the user once before the affected operations and continue independent preparation meanwhile. Existing authorization, safety checks, workflow prerequisites, and human-approved rollout constraints remain binding; urgency does not waive them.",
|
|
29
|
+
"Choose the execution mechanism after deciding what can run together: foreground tools for small, tightly coupled, or interactive work; subagents for bounded independent reasoning or implementation when the active delegation mode permits; background tasks for independent long-running commands or waits. Manual delegation forbids proactive subagents, not concurrent foreground tools or background commands. Inspect every launch receipt for failures or skipped work.",
|
|
30
|
+
"Established command-only jobs and external waits belong to background operations, including in coordinator mode. Use bg_task_spawn for a long command, bg_task_watch for a repeated check, and watch_delivery for supported merge delivery. A subagent assignment must require reasoning or implementation beyond launching and waiting. Workers retain their assigned validation: launch or watch, continue independent work, then park with wait_for_operations and inspect the terminal evidence on continuation. Continue independent work while it runs; a task launch is not completion of the assignment.",
|
|
31
|
+
"If a worker hands back a command, launch it through bg_task_spawn or bg_task_watch with the supplied cwd, and inspect its terminal result before claiming verification.",
|
|
11
32
|
"A unit waiting or failing does not block other ready independent units unless the evidence shows a shared blocker. Preserve each unit's guards and rollback boundary. Before completing verification, the plan, or the goal, inspect and integrate every relevant terminal result or failure; launch is not completion.",
|
|
12
33
|
].join("\n");
|
|
13
34
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
/**
|
|
36
|
+
* True when this pi runtime renders custom prompt sections.
|
|
37
|
+
*
|
|
38
|
+
* pi gained `systemPromptOptions.sections` after 0.84.4, and the packages here
|
|
39
|
+
* declare support back to 0.84.4. A section-only publisher is a silent no-op on
|
|
40
|
+
* an older host: the handler runs, the map is populated, and the prompt the model
|
|
41
|
+
* receives is unchanged. Callers must fall back to appending text instead.
|
|
42
|
+
*/
|
|
43
|
+
export function supportsPromptSections(options: unknown): boolean {
|
|
44
|
+
const sections = (options as { sections?: unknown } | undefined)?.sections;
|
|
45
|
+
return typeof sections === "object" && sections !== null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Publish the policy into `options.sections`.
|
|
50
|
+
*
|
|
51
|
+
* Assignment, not concatenation: two packages publishing the same section write
|
|
52
|
+
* identical text, so load order cannot duplicate or drop it, and a repeated turn
|
|
53
|
+
* rewrites the same value so pi's section diff reports no change.
|
|
54
|
+
*/
|
|
55
|
+
export function publishExecutionPolicy<T extends object>(options: T): T {
|
|
56
|
+
const target = options as PromptSectionTarget;
|
|
57
|
+
const sections = (target.sections ??= {});
|
|
58
|
+
sections[EXECUTION_POLICY_SECTION] = EXECUTION_POLICY_TEXT;
|
|
59
|
+
return options;
|
|
60
|
+
}
|
|
@@ -8,7 +8,7 @@ import { createObservedWaitHost } from "./observed-waits.js";
|
|
|
8
8
|
import { getCallbackBatcher } from "./shared-callback-batcher.js";
|
|
9
9
|
import { observeForegroundSandboxPolicy } from "./sandbox.js";
|
|
10
10
|
import { registerTools } from "./tools.js";
|
|
11
|
-
import {
|
|
11
|
+
import { deliverBackgroundTasksPrompt } from "./prompt.js";
|
|
12
12
|
|
|
13
13
|
export default function backgroundTasksExtension(pi: ExtensionAPI): void {
|
|
14
14
|
// Created before registerTools so the tools can resolve it lazily; the durable
|
|
@@ -36,7 +36,10 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
|
|
|
36
36
|
},
|
|
37
37
|
});
|
|
38
38
|
initializeNavigatorPanels(pi);
|
|
39
|
-
|
|
39
|
+
// Sections on a host that renders them; text only where it renders none.
|
|
40
|
+
// Returning a prompt on modern pi would set forceSystemPrompt, and the request
|
|
41
|
+
// would then carry a snapshot, so sections published after it never arrive.
|
|
42
|
+
pi.on("before_agent_start", (event) => deliverBackgroundTasksPrompt(event));
|
|
40
43
|
registerBackgroundTasksGoalProvider(pi);
|
|
41
44
|
ensureBackgroundTasksNavigatorProvider(pi);
|
|
42
45
|
// Subscribed at load so a sandbox extension that publishes later is heard, and
|
|
@@ -101,8 +101,12 @@ const provider: BackgroundWorkProvider = {
|
|
|
101
101
|
},
|
|
102
102
|
onVisibleChanged: (notify) => {
|
|
103
103
|
const offMeta = onMetaChanged(notify);
|
|
104
|
-
const offDurable = getObservedWaitHost(piRef)?.onDurableTaskChange(
|
|
104
|
+
const offDurable = getObservedWaitHost(piRef)?.onDurableTaskChange(() => {
|
|
105
|
+
invalidateDurableProjection();
|
|
106
|
+
notify();
|
|
107
|
+
});
|
|
105
108
|
return () => {
|
|
109
|
+
invalidateDurableProjection();
|
|
106
110
|
offMeta();
|
|
107
111
|
offDurable?.();
|
|
108
112
|
};
|
|
@@ -113,13 +117,30 @@ function navigatorHost() {
|
|
|
113
117
|
return piRef ? getObservedWaitHost(piRef) : undefined;
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
/**
|
|
121
|
+
* One refresh asks for {@link provider.visibleCount} and then {@link provider.listRows},
|
|
122
|
+
* and a detail view asks again. Rebuilding the projection re-reads every durable
|
|
123
|
+
* record, so a single paint would pay for the same list up to three times. Hold
|
|
124
|
+
* one result per paint window; a durable-change notification clears it.
|
|
125
|
+
*/
|
|
126
|
+
const PROJECTION_MEMO_MS = 250;
|
|
127
|
+
let projectionMemo: { at: number; tasks: DurableTaskProjection[] } | undefined;
|
|
128
|
+
|
|
129
|
+
function invalidateDurableProjection(): void {
|
|
130
|
+
projectionMemo = undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
116
133
|
function visibleDurableTasks(now = Date.now()): DurableTaskProjection[] {
|
|
117
134
|
const active = activeNavigatorOrigin;
|
|
118
135
|
const host = navigatorHost();
|
|
119
136
|
if (!active || !host) return [];
|
|
137
|
+
if (projectionMemo && now - projectionMemo.at < PROJECTION_MEMO_MS) {
|
|
138
|
+
return projectionMemo.tasks.filter((task) => !isExpiredDurableRow(task, now));
|
|
139
|
+
}
|
|
120
140
|
const listed = host.listDurableTasks(active, { retentionMs: TERMINAL_NAVIGATOR_RETENTION_MS });
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
const tasks = listed.filter((task) => task.dismissedAt === undefined);
|
|
142
|
+
projectionMemo = { at: now, tasks };
|
|
143
|
+
return tasks.filter((task) => !isExpiredDurableRow(task, now));
|
|
123
144
|
}
|
|
124
145
|
|
|
125
146
|
function durableTask(id: string): DurableTaskProjection | undefined {
|
|
@@ -1366,8 +1366,10 @@ export class ObservedWaitHost {
|
|
|
1366
1366
|
|
|
1367
1367
|
private projectDurable(operation: OperationRecord, now: number): DurableTaskProjection | undefined {
|
|
1368
1368
|
const context = this.launchContext(operation.taskId);
|
|
1369
|
+
// Projection is the rendering path: a snapshot reads the durable records
|
|
1370
|
+
// without taking the cross-process fence or spawning `ps`.
|
|
1369
1371
|
const inspection = LOCAL_DURABLE_ID.test(operation.taskId)
|
|
1370
|
-
? (() => { try { return this.supervisor.
|
|
1372
|
+
? (() => { try { return this.supervisor.snapshot(operation.taskId); } catch { return undefined; } })()
|
|
1371
1373
|
: undefined;
|
|
1372
1374
|
const storeStatus = operation.terminalRevision > 0 ? this.statusOf(operation.operationId) : undefined;
|
|
1373
1375
|
const status: SupervisorTaskStatus = inspection?.terminal ? inspection.status : storeStatus ?? inspection?.status ?? "running";
|
|
@@ -1,41 +1,118 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Process start-token reads.
|
|
5
|
+
*
|
|
6
|
+
* `ps` is a process spawn. Reading it on a rendering path — once per task, per
|
|
7
|
+
* frame, for every terminal task kept in the panel — is a measurable CPU drain
|
|
8
|
+
* on its own, and it competes with the very commands the panel is showing.
|
|
9
|
+
*
|
|
10
|
+
* Two rules keep that bounded:
|
|
11
|
+
*
|
|
12
|
+
* 1. A pid's start token is immutable for the life of that pid, so a short-lived
|
|
13
|
+
* memo is safe. Entries expire so a recycled pid is re-read rather than
|
|
14
|
+
* mistaking the previous owner's token for the current one, and failures are
|
|
15
|
+
* memoized too, so a pid that cannot be read is not retried on every frame.
|
|
16
|
+
* 2. Callers that decide to *signal* a pid can ask for a fresh read. Liveness
|
|
17
|
+
* display may use the memo; authorization may not.
|
|
18
|
+
*/
|
|
4
19
|
|
|
5
|
-
|
|
6
|
-
|
|
20
|
+
/** How long one pid→token answer is trusted. Short enough to bound pid reuse. */
|
|
21
|
+
const TOKEN_MEMO_TTL_MS = 1_000;
|
|
22
|
+
/** Bound on memo entries; the panel is small, but a long session is not. */
|
|
23
|
+
const TOKEN_MEMO_MAX_ENTRIES = 512;
|
|
24
|
+
|
|
25
|
+
interface TokenMemoEntry {
|
|
26
|
+
token: string | undefined;
|
|
27
|
+
readAt: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const tokenMemo = new Map<number, TokenMemoEntry>();
|
|
31
|
+
|
|
32
|
+
function readTokenUncached(pid: number): string | undefined {
|
|
33
|
+
if (!Number.isInteger(pid) || pid <= 0 || process.platform === "win32") return undefined;
|
|
34
|
+
try {
|
|
35
|
+
const output = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
38
|
+
timeout: 2_000,
|
|
39
|
+
}).trim();
|
|
40
|
+
return output || undefined;
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function remember(pid: number, token: string | undefined, now: number): void {
|
|
47
|
+
if (tokenMemo.size >= TOKEN_MEMO_MAX_ENTRIES) {
|
|
48
|
+
// Drop expired entries first; only evict live ones if that was not enough.
|
|
49
|
+
for (const [candidate, entry] of tokenMemo) {
|
|
50
|
+
if (now - entry.readAt >= TOKEN_MEMO_TTL_MS) tokenMemo.delete(candidate);
|
|
51
|
+
}
|
|
52
|
+
if (tokenMemo.size >= TOKEN_MEMO_MAX_ENTRIES) {
|
|
53
|
+
const oldest = tokenMemo.keys().next();
|
|
54
|
+
if (!oldest.done) tokenMemo.delete(oldest.value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
tokenMemo.set(pid, { token, readAt: now });
|
|
7
58
|
}
|
|
8
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Start token for `pid`, memoized for {@link TOKEN_MEMO_TTL_MS}.
|
|
62
|
+
*
|
|
63
|
+
* Suitable for liveness and display. A caller that is about to send a signal
|
|
64
|
+
* must use {@link readProcessStartTokenFresh} instead.
|
|
65
|
+
*/
|
|
9
66
|
export function readProcessStartToken(pid: number): string | undefined {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
67
|
+
if (!Number.isInteger(pid) || pid <= 0 || process.platform === "win32") return undefined;
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
const entry = tokenMemo.get(pid);
|
|
70
|
+
if (entry && now - entry.readAt < TOKEN_MEMO_TTL_MS) return entry.token;
|
|
71
|
+
const token = readTokenUncached(pid);
|
|
72
|
+
remember(pid, token, now);
|
|
73
|
+
return token;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Start token for `pid`, always read from the OS.
|
|
78
|
+
*
|
|
79
|
+
* Used where the answer authorizes a signal, so a recycled pid cannot inherit
|
|
80
|
+
* a memoized identity.
|
|
81
|
+
*/
|
|
82
|
+
export function readProcessStartTokenFresh(pid: number): string | undefined {
|
|
83
|
+
if (!Number.isInteger(pid) || pid <= 0 || process.platform === "win32") return undefined;
|
|
84
|
+
const token = readTokenUncached(pid);
|
|
85
|
+
remember(pid, token, Date.now());
|
|
86
|
+
return token;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Drop every memoized identity. Tests and reloads start from a clean slate. */
|
|
90
|
+
export function clearProcessIdentityMemo(): void {
|
|
91
|
+
tokenMemo.clear();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const selfStartToken = readTokenUncached(process.pid);
|
|
95
|
+
|
|
96
|
+
export function currentProcessStartToken(): string | undefined {
|
|
97
|
+
return selfStartToken;
|
|
21
98
|
}
|
|
22
99
|
|
|
23
100
|
export function processIdentityAlive(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
101
|
+
pid: number | undefined,
|
|
102
|
+
recordedStartToken?: string,
|
|
103
|
+
recordedAt?: number,
|
|
27
104
|
): boolean {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
105
|
+
if (!pid || pid <= 0) return false;
|
|
106
|
+
try {
|
|
107
|
+
process.kill(pid, 0);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if ((error as NodeJS.ErrnoException).code !== "EPERM") return false;
|
|
110
|
+
}
|
|
111
|
+
const current = readProcessStartToken(pid);
|
|
112
|
+
if (recordedStartToken && current) return recordedStartToken === current;
|
|
113
|
+
if (!recordedStartToken && current && recordedAt !== undefined) {
|
|
114
|
+
const currentStartedAt = Date.parse(current);
|
|
115
|
+
if (Number.isFinite(currentStartedAt) && currentStartedAt > recordedAt + 2_000) return false;
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operational prompt this package owns: how to wait, and how to launch.
|
|
3
|
+
*
|
|
4
|
+
* Published as a named section so pi can diff it. It exists because the tool
|
|
5
|
+
* descriptions cannot carry it: a description explains one call, while the
|
|
6
|
+
* failure this prevents is choosing `sleep` over a watcher in the first place.
|
|
7
|
+
*
|
|
8
|
+
* Instructions only. No rationale, no worked examples, and no defaults that the
|
|
9
|
+
* tool schemas already state: the model has every schema in context, and repeated
|
|
10
|
+
* reference just spends prefix tokens on text it can already read.
|
|
11
|
+
*/
|
|
12
|
+
export const BACKGROUND_TASKS_SECTION = "background_tasks";
|
|
13
|
+
|
|
14
|
+
import { EXECUTION_POLICY_TEXT, publishExecutionPolicy, supportsPromptSections } from "./shared-execution-policy.js";
|
|
15
|
+
|
|
16
|
+
/** `sections` is optional because the repo typechecks against older pi typings. */
|
|
17
|
+
export interface PromptSectionTarget {
|
|
18
|
+
sections?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const BACKGROUND_TASKS_PROMPT: string = [
|
|
22
|
+
"Background tasks: never wait in the foreground.",
|
|
23
|
+
"",
|
|
24
|
+
"- Never use foreground `sleep` to wait or poll, whether alone, before another command, or inside a loop. The sole permitted foreground sleep is at most 3 s immediately after `pkill`, `kill`, or `systemctl stop`, while the process exits.",
|
|
25
|
+
"- Use `bg_task_spawn` and `bg_task_watch` instead of `nohup`, `&`, or `disown`.",
|
|
26
|
+
"- Treat the wait, not the check, as the long part. Waiting for `gh pr checks` to pass is a `bg_task_watch`, not `sleep 300; gh pr checks`.",
|
|
27
|
+
"- Waiting on an external system (`gh run view`, `gh pr checks`, `mergify queue show`, a redeploy, a dev server, `cloud-init`): `bg_task_watch` with the check command and `success_when`. For a PR you just enqueued, `watch_delivery`.",
|
|
28
|
+
"- Waiting on your own long command (tests, build, migration): `bg_task_spawn`, then `wait_for_operations`. Do not sleep and re-read the log.",
|
|
29
|
+
"- Waiting for a server you just started: `bg_task_spawn` the server, then `bg_task_watch` on a readiness check (`curl -fsS ...`).",
|
|
30
|
+
"- Retry until a condition with one `bg_task_watch`, never `for i in ...; do ...; sleep N; done`.",
|
|
31
|
+
"- If a dependency is pending and no watcher is running, start `bg_task_watch`; do not loop short sleeps.",
|
|
32
|
+
"- Nothing here automatically diverts a foreground command. A sleep or monitor loop submitted through foreground `bash` blocks the turn.",
|
|
33
|
+
"- Omit `timeout_seconds` on `bg_task_spawn` unless you need a bound. Raise it on `bg_task_watch` for a wait longer than 15 minutes, and leave `callback` alone: do not poll.",
|
|
34
|
+
"- Write the check so a broken check is visible; do not end it with `exit 0` or `|| true`.",
|
|
35
|
+
"- Stop tasks you started for a turn-scoped purpose before finishing the turn.",
|
|
36
|
+
].join("\n");
|
|
37
|
+
|
|
38
|
+
/** Publish the guidance into `options.sections`; assignment keeps it idempotent across turns. */
|
|
39
|
+
export function publishBackgroundTasksGuidance<T extends object>(options: T): T {
|
|
40
|
+
const target = options as PromptSectionTarget;
|
|
41
|
+
const sections = (target.sections ??= {});
|
|
42
|
+
sections[BACKGROUND_TASKS_SECTION] = BACKGROUND_TASKS_PROMPT;
|
|
43
|
+
return options;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** What a handler receives; only these two fields are used. */
|
|
47
|
+
export interface BeforeAgentStartEvent {
|
|
48
|
+
systemPrompt: string;
|
|
49
|
+
systemPromptOptions?: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Deliver this package's guidance on both kinds of host.
|
|
54
|
+
*
|
|
55
|
+
* Modern pi renders custom sections, so publishing keeps the prompt structured
|
|
56
|
+
* and lets pi diff it. Older pi (the packages declare support back to 0.84.4)
|
|
57
|
+
* renders no sections at all, so publishing there would silently deliver nothing;
|
|
58
|
+
* returning the text is the behaviour those hosts already had.
|
|
59
|
+
*/
|
|
60
|
+
export function deliverBackgroundTasksPrompt(event: BeforeAgentStartEvent): { systemPrompt: string } | undefined {
|
|
61
|
+
if (!supportsPromptSections(event.systemPromptOptions)) {
|
|
62
|
+
// Several packages carry the same policy, so appending blindly would print it
|
|
63
|
+
// once per publisher. The text path stays idempotent exactly as it used to be.
|
|
64
|
+
const policy = event.systemPrompt.includes(EXECUTION_POLICY_TEXT) ? "" : `${EXECUTION_POLICY_TEXT}\n\n`;
|
|
65
|
+
const guidance = event.systemPrompt.includes(BACKGROUND_TASKS_PROMPT) ? "" : BACKGROUND_TASKS_PROMPT;
|
|
66
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${policy}${guidance}` };
|
|
67
|
+
}
|
|
68
|
+
const options = event.systemPromptOptions as { sections?: Record<string, string> };
|
|
69
|
+
publishExecutionPolicy(options);
|
|
70
|
+
publishBackgroundTasksGuidance(options);
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
@@ -1,21 +1,61 @@
|
|
|
1
1
|
// Generated from packages/pi-better-harness/extensions/execution-policy.ts. Do not edit directly.
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Harness-wide scheduling guidance, vendored so standalone extensions share it.
|
|
4
|
+
*
|
|
5
|
+
* Published as a named system-prompt section, never as appended text. A
|
|
6
|
+
* `before_agent_start` handler that returns `systemPrompt` sets
|
|
7
|
+
* `forceSystemPrompt`, and a forced prompt is opaque to pi: `buildSystemPromptState`
|
|
8
|
+
* returns `{ content }` with no sections while `diffSystemPromptSections` keeps
|
|
9
|
+
* diffing the sections the request therefore never shows. The request carries a
|
|
10
|
+
* rendered snapshot, so any section published after that snapshot never reaches
|
|
11
|
+
* it, so every package that speaks here publishes a section.
|
|
12
|
+
*/
|
|
13
|
+
export const EXECUTION_POLICY_SECTION = "harness_execution_policy";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The minimum shape this module needs. `sections` stays optional because this
|
|
17
|
+
* repo typechecks against older pi typings whose event options predate sections;
|
|
18
|
+
* the shipped runtime always supplies the map.
|
|
19
|
+
*/
|
|
20
|
+
export interface PromptSectionTarget {
|
|
21
|
+
sections?: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const EXECUTION_POLICY_TEXT: string = [
|
|
4
25
|
"Harness execution policy: concurrency is separate from subagent delegation.",
|
|
5
|
-
"Before multi-unit work, identify ready, authorized, independent units, actual prerequisites, shared mutable resources, and capacity or availability constraints. Represent independently verifiable
|
|
26
|
+
"Before multi-unit work, identify ready, authorized, independent units, actual prerequisites, shared mutable resources, and capacity or availability constraints. Represent each independently verifiable deliverable as its own plan step; do not create dependencies from list order.",
|
|
6
27
|
"Start all ready, authorized, non-conflicting units concurrently within known resource limits before waiting for any one to finish. Reassess ready work after launches and results; continue unblocked foreground work instead of polling running tasks. Do not equate one background launch with scheduling all ready work.",
|
|
7
28
|
"Serialize only for a concrete dependency, conflicting writes or ownership, resource or concurrency limit, documented rollout gate, or explicit availability requirement. Record the reason in the plan or explain it to the user. Do not invent a canary or host-by-host availability requirement.",
|
|
8
|
-
"If concurrency raises an unresolved material operational trade-off
|
|
9
|
-
"Choose the execution mechanism after deciding what can run together: foreground tools for small, tightly coupled, or interactive work; subagents for bounded independent reasoning or implementation when the active delegation mode permits; background tasks for independent long-running commands or waits. Manual delegation forbids proactive subagents, not concurrent foreground tools or background commands.
|
|
10
|
-
"Established command-only jobs and external waits belong to background operations, including in coordinator mode. Use bg_task_spawn for a long command
|
|
11
|
-
"If a worker hands back a command,
|
|
29
|
+
"If concurrency raises an unresolved material operational trade-off, ask the user once before the affected operations and continue independent preparation meanwhile. Existing authorization, safety checks, workflow prerequisites, and human-approved rollout constraints remain binding; urgency does not waive them.",
|
|
30
|
+
"Choose the execution mechanism after deciding what can run together: foreground tools for small, tightly coupled, or interactive work; subagents for bounded independent reasoning or implementation when the active delegation mode permits; background tasks for independent long-running commands or waits. Manual delegation forbids proactive subagents, not concurrent foreground tools or background commands. Inspect every launch receipt for failures or skipped work.",
|
|
31
|
+
"Established command-only jobs and external waits belong to background operations, including in coordinator mode. Use bg_task_spawn for a long command, bg_task_watch for a repeated check, and watch_delivery for supported merge delivery. A subagent assignment must require reasoning or implementation beyond launching and waiting. Workers retain their assigned validation: launch or watch, continue independent work, then park with wait_for_operations and inspect the terminal evidence on continuation. Continue independent work while it runs; a task launch is not completion of the assignment.",
|
|
32
|
+
"If a worker hands back a command, launch it through bg_task_spawn or bg_task_watch with the supplied cwd, and inspect its terminal result before claiming verification.",
|
|
12
33
|
"A unit waiting or failing does not block other ready independent units unless the evidence shows a shared blocker. Preserve each unit's guards and rollback boundary. Before completing verification, the plan, or the goal, inspect and integrate every relevant terminal result or failure; launch is not completion.",
|
|
13
34
|
].join("\n");
|
|
14
35
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
36
|
+
/**
|
|
37
|
+
* True when this pi runtime renders custom prompt sections.
|
|
38
|
+
*
|
|
39
|
+
* pi gained `systemPromptOptions.sections` after 0.84.4, and the packages here
|
|
40
|
+
* declare support back to 0.84.4. A section-only publisher is a silent no-op on
|
|
41
|
+
* an older host: the handler runs, the map is populated, and the prompt the model
|
|
42
|
+
* receives is unchanged. Callers must fall back to appending text instead.
|
|
43
|
+
*/
|
|
44
|
+
export function supportsPromptSections(options: unknown): boolean {
|
|
45
|
+
const sections = (options as { sections?: unknown } | undefined)?.sections;
|
|
46
|
+
return typeof sections === "object" && sections !== null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Publish the policy into `options.sections`.
|
|
51
|
+
*
|
|
52
|
+
* Assignment, not concatenation: two packages publishing the same section write
|
|
53
|
+
* identical text, so load order cannot duplicate or drop it, and a repeated turn
|
|
54
|
+
* rewrites the same value so pi's section diff reports no change.
|
|
55
|
+
*/
|
|
56
|
+
export function publishExecutionPolicy<T extends object>(options: T): T {
|
|
57
|
+
const target = options as PromptSectionTarget;
|
|
58
|
+
const sections = (target.sections ??= {});
|
|
59
|
+
sections[EXECUTION_POLICY_SECTION] = EXECUTION_POLICY_TEXT;
|
|
60
|
+
return options;
|
|
61
|
+
}
|
package/node_modules/pi-better-background-tasks/src/supervisor/generated/process-identity.mjs
CHANGED
|
@@ -2,11 +2,28 @@
|
|
|
2
2
|
// Do not edit: change the source and re-run the emitter (a drift test enforces this).
|
|
3
3
|
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Process start-token reads.
|
|
7
|
+
*
|
|
8
|
+
* `ps` is a process spawn. Reading it on a rendering path — once per task, per
|
|
9
|
+
* frame, for every terminal task kept in the panel — is a measurable CPU drain
|
|
10
|
+
* on its own, and it competes with the very commands the panel is showing.
|
|
11
|
+
*
|
|
12
|
+
* Two rules keep that bounded:
|
|
13
|
+
*
|
|
14
|
+
* 1. A pid's start token is immutable for the life of that pid, so a short-lived
|
|
15
|
+
* memo is safe. Entries expire so a recycled pid is re-read rather than
|
|
16
|
+
* mistaking the previous owner's token for the current one, and failures are
|
|
17
|
+
* memoized too, so a pid that cannot be read is not retried on every frame.
|
|
18
|
+
* 2. Callers that decide to *signal* a pid can ask for a fresh read. Liveness
|
|
19
|
+
* display may use the memo; authorization may not.
|
|
20
|
+
*/
|
|
21
|
+
/** How long one pid→token answer is trusted. Short enough to bound pid reuse. */
|
|
22
|
+
const TOKEN_MEMO_TTL_MS = 1_000;
|
|
23
|
+
/** Bound on memo entries; the panel is small, but a long session is not. */
|
|
24
|
+
const TOKEN_MEMO_MAX_ENTRIES = 512;
|
|
25
|
+
const tokenMemo = new Map();
|
|
26
|
+
function readTokenUncached(pid) {
|
|
10
27
|
if (!Number.isInteger(pid) || pid <= 0 || process.platform === "win32")
|
|
11
28
|
return undefined;
|
|
12
29
|
try {
|
|
@@ -21,6 +38,59 @@ export function readProcessStartToken(pid) {
|
|
|
21
38
|
return undefined;
|
|
22
39
|
}
|
|
23
40
|
}
|
|
41
|
+
function remember(pid, token, now) {
|
|
42
|
+
if (tokenMemo.size >= TOKEN_MEMO_MAX_ENTRIES) {
|
|
43
|
+
// Drop expired entries first; only evict live ones if that was not enough.
|
|
44
|
+
for (const [candidate, entry] of tokenMemo) {
|
|
45
|
+
if (now - entry.readAt >= TOKEN_MEMO_TTL_MS)
|
|
46
|
+
tokenMemo.delete(candidate);
|
|
47
|
+
}
|
|
48
|
+
if (tokenMemo.size >= TOKEN_MEMO_MAX_ENTRIES) {
|
|
49
|
+
const oldest = tokenMemo.keys().next();
|
|
50
|
+
if (!oldest.done)
|
|
51
|
+
tokenMemo.delete(oldest.value);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
tokenMemo.set(pid, { token, readAt: now });
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Start token for `pid`, memoized for {@link TOKEN_MEMO_TTL_MS}.
|
|
58
|
+
*
|
|
59
|
+
* Suitable for liveness and display. A caller that is about to send a signal
|
|
60
|
+
* must use {@link readProcessStartTokenFresh} instead.
|
|
61
|
+
*/
|
|
62
|
+
export function readProcessStartToken(pid) {
|
|
63
|
+
if (!Number.isInteger(pid) || pid <= 0 || process.platform === "win32")
|
|
64
|
+
return undefined;
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
const entry = tokenMemo.get(pid);
|
|
67
|
+
if (entry && now - entry.readAt < TOKEN_MEMO_TTL_MS)
|
|
68
|
+
return entry.token;
|
|
69
|
+
const token = readTokenUncached(pid);
|
|
70
|
+
remember(pid, token, now);
|
|
71
|
+
return token;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Start token for `pid`, always read from the OS.
|
|
75
|
+
*
|
|
76
|
+
* Used where the answer authorizes a signal, so a recycled pid cannot inherit
|
|
77
|
+
* a memoized identity.
|
|
78
|
+
*/
|
|
79
|
+
export function readProcessStartTokenFresh(pid) {
|
|
80
|
+
if (!Number.isInteger(pid) || pid <= 0 || process.platform === "win32")
|
|
81
|
+
return undefined;
|
|
82
|
+
const token = readTokenUncached(pid);
|
|
83
|
+
remember(pid, token, Date.now());
|
|
84
|
+
return token;
|
|
85
|
+
}
|
|
86
|
+
/** Drop every memoized identity. Tests and reloads start from a clean slate. */
|
|
87
|
+
export function clearProcessIdentityMemo() {
|
|
88
|
+
tokenMemo.clear();
|
|
89
|
+
}
|
|
90
|
+
const selfStartToken = readTokenUncached(process.pid);
|
|
91
|
+
export function currentProcessStartToken() {
|
|
92
|
+
return selfStartToken;
|
|
93
|
+
}
|
|
24
94
|
export function processIdentityAlive(pid, recordedStartToken, recordedAt) {
|
|
25
95
|
if (!pid || pid <= 0)
|
|
26
96
|
return false;
|
|
@@ -376,9 +376,22 @@ class Supervisor implements LocalSupervisor {
|
|
|
376
376
|
const command = readCommandRecord(this.directory, taskId).value;
|
|
377
377
|
const stop = readStopRequest(this.directory, taskId);
|
|
378
378
|
const cleanup = readCleanupRecord(this.directory, taskId);
|
|
379
|
+
// Reading a process start token spawns `ps`. A task that already published a
|
|
380
|
+
// terminal/lost record cannot change state again, so its liveness only
|
|
381
|
+
// matters to the one caller that may still act on a surviving command group
|
|
382
|
+
// (`stop`). Every other reader would pay for an answer it does not use, once
|
|
383
|
+
// per task per panel frame. Probe only when that authority is actually needed.
|
|
384
|
+
const needsLiveness = result === undefined && lostRecord === undefined
|
|
385
|
+
? true
|
|
386
|
+
: result !== undefined && result.groupCleanup === "unproven"
|
|
387
|
+
|| lostRecord !== undefined;
|
|
379
388
|
// One identity source per instance: liveness and signalling agree.
|
|
380
|
-
const claimAlive =
|
|
381
|
-
|
|
389
|
+
const claimAlive = needsLiveness && claim
|
|
390
|
+
? identityAlive(claim.pid, claim.pidStartTime, claim.claimedAt, this.readToken)
|
|
391
|
+
: false;
|
|
392
|
+
const commandAlive = needsLiveness && command
|
|
393
|
+
? identityAlive(command.pid, command.pidStartTime, command.startedAt, this.readToken)
|
|
394
|
+
: false;
|
|
382
395
|
|
|
383
396
|
// A published terminal/lost record needs no fence. Anything else is decided
|
|
384
397
|
// under the fence, so a `lost` attestation and a claim cannot both win.
|
|
@@ -426,6 +439,67 @@ class Supervisor implements LocalSupervisor {
|
|
|
426
439
|
};
|
|
427
440
|
}
|
|
428
441
|
|
|
442
|
+
/**
|
|
443
|
+
* Read-only projection for rendering: no fence, no lost publication, no `ps`.
|
|
444
|
+
*
|
|
445
|
+
* `inspect` is the authoritative path and pays for that authority — a
|
|
446
|
+
* cross-process SQLite fence and a process-identity probe per recorded pid.
|
|
447
|
+
* A panel asks per task per frame, so it reads what the records already say.
|
|
448
|
+
*/
|
|
449
|
+
snapshot(taskId: string): SupervisorInspection | undefined {
|
|
450
|
+
const launchRead = readLaunchRecord(this.directory, taskId);
|
|
451
|
+
if (launchRead.missing) return undefined;
|
|
452
|
+
if (!launchRead.value) {
|
|
453
|
+
throw new Error(`Unreadable supervisor launch record for ${taskId}: ${launchRead.error}`);
|
|
454
|
+
}
|
|
455
|
+
const launch = launchRead.value;
|
|
456
|
+
const paths = supervisorPaths(this.directory, taskId);
|
|
457
|
+
const result = readTerminalRecord(this.directory, taskId).value;
|
|
458
|
+
const lostRecord = readLostRecord(this.directory, taskId).value;
|
|
459
|
+
const claim = readClaimRecord(this.directory, taskId).value;
|
|
460
|
+
const command = readCommandRecord(this.directory, taskId).value;
|
|
461
|
+
const stop = readStopRequest(this.directory, taskId);
|
|
462
|
+
const cleanup = readCleanupRecord(this.directory, taskId);
|
|
463
|
+
const status: SupervisorTaskStatus = result
|
|
464
|
+
? (result.status as SupervisorTaskStatus)
|
|
465
|
+
: lostRecord
|
|
466
|
+
? "lost"
|
|
467
|
+
: claim
|
|
468
|
+
? "running"
|
|
469
|
+
: "starting";
|
|
470
|
+
return {
|
|
471
|
+
taskId,
|
|
472
|
+
token: launch.token,
|
|
473
|
+
kind: launch.kind,
|
|
474
|
+
status,
|
|
475
|
+
terminal: result !== undefined || lostRecord !== undefined,
|
|
476
|
+
launch: launchSummary(launch),
|
|
477
|
+
// Liveness is deliberately not asserted here: a snapshot never spawns `ps`.
|
|
478
|
+
...(claim === undefined
|
|
479
|
+
? {}
|
|
480
|
+
: { claim: { pid: claim.pid, pidStartTime: claim.pidStartTime, claimedAt: claim.claimedAt, alive: false } }),
|
|
481
|
+
...(command === undefined
|
|
482
|
+
? {}
|
|
483
|
+
: {
|
|
484
|
+
command: {
|
|
485
|
+
pid: command.pid,
|
|
486
|
+
pgid: command.pgid,
|
|
487
|
+
pidStartTime: command.pidStartTime,
|
|
488
|
+
startedAt: command.startedAt,
|
|
489
|
+
attempt: command.attempt ?? 1,
|
|
490
|
+
alive: false,
|
|
491
|
+
},
|
|
492
|
+
}),
|
|
493
|
+
...(result === undefined ? {} : { result }),
|
|
494
|
+
...(lostRecord === undefined ? {} : { lost: lostRecord }),
|
|
495
|
+
...(cleanup === undefined ? {} : { cleanup }),
|
|
496
|
+
...(stop === undefined ? {} : { stopRequestedAt: stop.requestedAt }),
|
|
497
|
+
taskDir: paths.dir,
|
|
498
|
+
logs: { stdout: paths.stdout, stderr: paths.stderr, events: paths.events },
|
|
499
|
+
observedAt: Date.now(),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
429
503
|
/**
|
|
430
504
|
* Decide the state of a launch with no terminal result, under the cross-process
|
|
431
505
|
* fence. This is the only place a `lost` record is published, so it can never
|
|
@@ -380,5 +380,15 @@ export interface LocalSupervisorOptions {
|
|
|
380
380
|
export interface LocalSupervisor {
|
|
381
381
|
start(options: SupervisorStartOptions): Promise<SupervisorStartResult>;
|
|
382
382
|
inspect(taskId: string, options?: SupervisorInspectOptions): SupervisorInspection | undefined;
|
|
383
|
+
/**
|
|
384
|
+
* Read-only projection of the durable records: no state fence, no lost
|
|
385
|
+
* publication and no process-identity probe.
|
|
386
|
+
*
|
|
387
|
+
* This is the rendering path. `inspect` may take a cross-process SQLite fence
|
|
388
|
+
* and spawn `ps` per task, which must not happen once per task per frame. A
|
|
389
|
+
* snapshot reports what the records already say — `running` for a claim,
|
|
390
|
+
* `starting` before one — and leaves reconciliation to the caller's cadence.
|
|
391
|
+
*/
|
|
392
|
+
snapshot(taskId: string): SupervisorInspection | undefined;
|
|
383
393
|
stop(taskId: string, options?: SupervisorStopOptions): Promise<SupervisorStopResult | undefined>;
|
|
384
394
|
}
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from "./goal-state.js";
|
|
31
31
|
import { continuationEvidence, type ContinuationEvidence } from "./continuation.js";
|
|
32
32
|
import { currentPermissionHold, logicalPermissionBlockerKey, MAX_PERMISSION_BLOCKERS, MAX_PERMISSION_RECORDS, permissionHoldRecord, permissionInstruction, permissionRecord, reportedPermissionBlockers, reportedPermissionGap, type PermissionHold } from "./permission-hold.js";
|
|
33
|
+
import { publishGoalGuidance } from "./prompt.js";
|
|
33
34
|
import { observeGoalStall } from "./stall.js";
|
|
34
35
|
import {
|
|
35
36
|
flattenObjectiveForRail,
|
|
@@ -1360,12 +1361,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1360
1361
|
(permissions.blockers.length === 0 ? "\nBlocker evidence is unavailable. Stay held; do not invent a retry scope." : "") : "";
|
|
1361
1362
|
// A held goal overrides workflow ownership without invoking or invalidating that workflow.
|
|
1362
1363
|
if (permissionPrompt && goal?.status === "paused") {
|
|
1363
|
-
return
|
|
1364
|
+
return publishGoalGuidance(event, permissionPrompt);
|
|
1364
1365
|
}
|
|
1365
1366
|
const pausedInstruction = permissionPrompt || (goal?.status === "paused" && (goal.pauseReason === "interrupt" || goal.pauseReason === "handback")
|
|
1366
1367
|
? pausedGoalPrompt(goal, preferences.conversationalResume, !harnessPresent) : "");
|
|
1367
1368
|
if (!isPokeable(goal) && !owner) {
|
|
1368
|
-
return
|
|
1369
|
+
return publishGoalGuidance(event, pausedInstruction);
|
|
1369
1370
|
}
|
|
1370
1371
|
|
|
1371
1372
|
const questionInstruction = snapshot.backgroundRunning
|
|
@@ -1379,31 +1380,28 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1379
1380
|
if (!workflowAvailable(owner)) {
|
|
1380
1381
|
if (isPokeable(goal)) setGoal(goalWithStatus(goal, "paused"), ctx, "runtime");
|
|
1381
1382
|
recordWorkflow(null);
|
|
1382
|
-
return
|
|
1383
|
+
return publishGoalGuidance(event, `Workflow ${owner.name} is no longer a registered, valid skill. Stop work and ask the user to reinvoke the skill.`);
|
|
1383
1384
|
}
|
|
1384
1385
|
let instructions: string;
|
|
1385
1386
|
try {
|
|
1386
1387
|
instructions = readFileSync(owner.path, "utf8");
|
|
1387
1388
|
} catch {
|
|
1388
1389
|
if (isPokeable(goal)) setGoal(goalWithStatus(goal, "paused"), ctx, "runtime");
|
|
1389
|
-
return
|
|
1390
|
+
return publishGoalGuidance(event, `Workflow ${owner.name} is unavailable. Stop work and ask the user to restore or reinvoke the skill.`);
|
|
1390
1391
|
}
|
|
1391
|
-
return
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
(questionInstruction ? `\n\n${questionInstruction.trim()}` : "") +
|
|
1405
|
-
(heldInstruction ? `\n\n${heldInstruction}` : ""),
|
|
1406
|
-
};
|
|
1392
|
+
return publishGoalGuidance(
|
|
1393
|
+
event,
|
|
1394
|
+
pausedInstruction &&
|
|
1395
|
+
`${pausedInstruction}\n` + (permissionPrompt
|
|
1396
|
+
? "These permission constraints override the workflow instructions below.\n\n"
|
|
1397
|
+
: "While the goal is paused, this overrides the workflow instructions below: do not advance the workflow until the goal is resumed.\n\n"),
|
|
1398
|
+
`Active workflow: ${owner.name} (${owner.path}). Its task plan owns planning and the parent is a coordinator, not a product-code implementer. Follow the workflow instructions below, including on resumed turns:\n\n${instructions}`,
|
|
1399
|
+
isPokeable(goal) && (harnessPresent
|
|
1400
|
+
? "Complete the goal only after the workflow completion audit."
|
|
1401
|
+
: `Active objective: ${goal.objective}. Complete it only after the workflow completion audit.`),
|
|
1402
|
+
questionInstruction && questionInstruction.trim(),
|
|
1403
|
+
heldInstruction,
|
|
1404
|
+
);
|
|
1407
1405
|
}
|
|
1408
1406
|
|
|
1409
1407
|
if (!isPokeable(goal)) return;
|
|
@@ -1413,18 +1411,19 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1413
1411
|
if (harnessPresent) {
|
|
1414
1412
|
// Harness owns the outcome text. Retain only the Goal-specific guidance
|
|
1415
1413
|
// that the Outcome & Todo prompt cannot replace.
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1414
|
+
return publishGoalGuidance(
|
|
1415
|
+
event,
|
|
1416
|
+
heldInstruction,
|
|
1417
|
+
backgroundInstruction.trim(),
|
|
1418
|
+
questionInstruction.trim(),
|
|
1419
|
+
permissionPrompt,
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
return publishGoalGuidance(
|
|
1423
|
+
event,
|
|
1424
|
+
`Pi Better Goal active objective: ${goal.objective}. ${heldInstruction || "Keep working through clear low-risk next steps, and mark complete only after an evidence-backed completion audit."}${backgroundInstruction}${questionInstruction}`,
|
|
1425
|
+
permissionPrompt,
|
|
1426
|
+
);
|
|
1428
1427
|
});
|
|
1429
1428
|
|
|
1430
1429
|
pi.on("agent_start", async (_event, ctx) => {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal-owned system-prompt guidance.
|
|
3
|
+
*
|
|
4
|
+
* Published as a named section on hosts that render sections, never as a returned
|
|
5
|
+
* `systemPrompt` there: returning text sets `forceSystemPrompt`, and the request
|
|
6
|
+
* then carries an opaque rendered snapshot, so a section published afterwards
|
|
7
|
+
* never reaches the model.
|
|
8
|
+
*
|
|
9
|
+
* On older hosts (this package declares support back to pi 0.84.4) custom sections
|
|
10
|
+
* do not render at all, so the same text is appended instead. Publishing alone
|
|
11
|
+
* would silently deliver nothing on those versions.
|
|
12
|
+
*/
|
|
13
|
+
export const GOAL_SECTION = "goal";
|
|
14
|
+
|
|
15
|
+
/** The minimum shape this module needs; both the event options and a test double satisfy it. */
|
|
16
|
+
export interface PromptSectionTarget {
|
|
17
|
+
sections?: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** What a handler receives; only these two fields are used. */
|
|
21
|
+
export interface BeforeAgentStartEvent {
|
|
22
|
+
systemPrompt: string;
|
|
23
|
+
systemPromptOptions?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Mirrors `supportsPromptSections` in the vendored execution policy. */
|
|
27
|
+
function rendersSections(options: unknown): boolean {
|
|
28
|
+
const sections = (options as { sections?: unknown } | undefined)?.sections;
|
|
29
|
+
return typeof sections === "object" && sections !== null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Deliver guidance, choosing the channel this host understands.
|
|
34
|
+
*
|
|
35
|
+
* Appending within one turn is safe: pi normalizes a fresh options object for
|
|
36
|
+
* every `before_agent_start`, so this never accumulates between requests, and
|
|
37
|
+
* within a turn the parts belong together and keep their order.
|
|
38
|
+
*/
|
|
39
|
+
export function publishGoalGuidance(
|
|
40
|
+
event: BeforeAgentStartEvent,
|
|
41
|
+
...parts: Array<string | undefined | false>
|
|
42
|
+
): { systemPrompt: string } | undefined {
|
|
43
|
+
const text = parts.filter((part): part is string => typeof part === "string" && part.trim().length > 0).join("\n\n");
|
|
44
|
+
if (!text) return undefined;
|
|
45
|
+
if (!rendersSections(event.systemPromptOptions)) {
|
|
46
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${text}` };
|
|
47
|
+
}
|
|
48
|
+
const target = event.systemPromptOptions as PromptSectionTarget;
|
|
49
|
+
const sections = (target.sections ??= {});
|
|
50
|
+
const existing = sections[GOAL_SECTION];
|
|
51
|
+
sections[GOAL_SECTION] = existing ? `${existing}\n\n${text}` : text;
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { parse } from "shell-quote";
|
|
3
3
|
import { STEER_FILE_ENV } from "./child-steer.ts";
|
|
4
|
+
import { supportsPromptSections } from "./shared-execution-policy.ts";
|
|
4
5
|
|
|
5
6
|
export const WORKER_HANDOFF = "Worker ownership: complete your assigned implementation or diagnosis, run focused checks, push only when authorized, write the requested result, then return. Pending CI is a handoff, not a failed implementation or a CI pass. The coordinator owns CI/PR monitoring, review scheduling, readiness, enqueueing and merge. Do not start external delivery watchers or background monitoring. Short status reads and failed-log retrieval for diagnosis are allowed. Legitimate local tests and builds may run to completion within their command limits. Use Bash for short diagnostic commands and focused checks needed to implement the change. When supervised background tools are available, retain ownership of assigned validation: launch a known long command with bg_task_spawn or repeated observation with bg_task_watch, continue independent work, then call wait_for_operations to park when no independent work remains. On continuation, inspect the operation's terminal evidence and acknowledge its interpretation before continuing the assignment. Parking is a saved-conversation dependency, not a request for the parent model to relay commands or results. An explicit restricted tool loadout may omit background support; return the exact command, cwd, required environment variable names (never secret values), current evidence and remaining validation to the coordinator. Do not emulate background execution with polling, sleep loops, nohup or shell detachment. Established command-only operations and external delivery monitoring belong to the coordinator's background tools.";
|
|
7
|
+
export const WORKER_HANDOFF_SECTION = "worker_handoff";
|
|
6
8
|
const REFUSAL = "Worker ownership: CI/PR monitoring belongs to the coordinator. Return the PR/run ID, pushed HEAD and current evidence with CI pending; do not retry the watcher through another tool or script. Local tests/builds and short diagnostic status/log reads remain allowed.";
|
|
7
9
|
const WATCH_SCRIPTS = new Set(["watch-pr-delivery.mjs", "watch-delivery.mjs", "watch-candidate-delivery.mjs"]);
|
|
8
10
|
const nameOf = (value: string) => value.replaceAll("\\", "/").split("/").at(-1)!.replace(/\.exe$/i, "");
|
|
@@ -133,7 +135,18 @@ export function workerMonitoringRefusal(toolName: string, input: unknown): strin
|
|
|
133
135
|
export default function childOwnership(pi: Pick<ExtensionAPI, "on">, env: NodeJS.ProcessEnv = process.env): void {
|
|
134
136
|
// This marker is supplied by the parent on every child launch, including clean/unsandboxed runs.
|
|
135
137
|
if (!env[STEER_FILE_ENV]) return;
|
|
136
|
-
|
|
138
|
+
// A section on hosts that render them; appended text on hosts that do not.
|
|
139
|
+
// Returning a prompt on a modern host would set forceSystemPrompt, and the
|
|
140
|
+
// request would carry a snapshot, so later sections never arrive.
|
|
141
|
+
pi.on("before_agent_start", (event) => {
|
|
142
|
+
if (!supportsPromptSections(event.systemPromptOptions)) {
|
|
143
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${WORKER_HANDOFF}` };
|
|
144
|
+
}
|
|
145
|
+
const options = event.systemPromptOptions as { sections?: Record<string, string> };
|
|
146
|
+
const sections = (options.sections ??= {});
|
|
147
|
+
sections[WORKER_HANDOFF_SECTION] = WORKER_HANDOFF;
|
|
148
|
+
return undefined;
|
|
149
|
+
});
|
|
137
150
|
pi.on("tool_call", (event) => {
|
|
138
151
|
const reason = workerMonitoringRefusal(event.toolName, event.input);
|
|
139
152
|
if (reason) return { block: true, reason };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { delegationPrompt, type DelegationMode } from "./delegation.ts";
|
|
3
3
|
|
|
4
|
+
import { EXECUTION_POLICY_TEXT, publishExecutionPolicy, supportsPromptSections } from "./shared-execution-policy.ts";
|
|
5
|
+
|
|
4
6
|
export const SUBAGENT_SETTINGS_CONTEXT = "pi-better-subagents-settings";
|
|
5
7
|
|
|
6
8
|
export interface EffectiveSubagentSettings {
|
|
@@ -28,19 +30,64 @@ export function effectiveSubagentSettings(
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export function formatSubagentSettings(settings: EffectiveSubagentSettings): string {
|
|
33
|
+
const manual = settings.delegationMode === "manual";
|
|
34
|
+
const capacity = manual
|
|
35
|
+
? "The limit is a ceiling, not a target. In manual mode it bounds only work the user or an active workflow explicitly delegates; no slot count is an invitation to launch work."
|
|
36
|
+
: "The limit is a ceiling, not a target. Launch ready, independent, useful work when a suitable subagent is available.";
|
|
37
|
+
// No occupancy counters here. This text is injected into the conversation and
|
|
38
|
+
// may only ever be appended to, so anything volatile would force either a
|
|
39
|
+
// rewrite of already-sent content (which invalidates the provider's cached
|
|
40
|
+
// prefix) or one more copy per change. Admission rechecks capacity at the
|
|
41
|
+
// spawn anyway; live counts stay available through the subagent_settings tool.
|
|
31
42
|
return [
|
|
32
43
|
`Subagent settings: revision ${settings.revision}`,
|
|
33
44
|
`Delegation: ${settings.delegationMode} (${settings.sources.delegationMode})`,
|
|
34
45
|
`Concurrency limit: ${settings.maxConcurrent} (${settings.sources.maxConcurrent})`,
|
|
35
|
-
|
|
36
|
-
`Occupied slots: ${settings.occupiedSlots}`,
|
|
37
|
-
`Available slots: ${settings.availableSlots}`,
|
|
38
|
-
"The limit is a ceiling, not a target. Launch ready, independent, useful work within available capacity. " +
|
|
39
|
-
"This is a point-in-time snapshot; spawn admission rechecks capacity. Lowering the limit does not stop existing runs.",
|
|
46
|
+
// Mode first: the policy governs the settings that follow it.
|
|
40
47
|
delegationPrompt(settings.delegationMode),
|
|
48
|
+
capacity + " Spawn admission rechecks capacity; lower the limit without stopping existing runs.",
|
|
41
49
|
].join("\n");
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Publish the delegation policy as its own prompt section.
|
|
54
|
+
*
|
|
55
|
+
* A section survives a summary that rewrites the conversation without being
|
|
56
|
+
* appended text, and it is part of the cached prefix, so it costs nothing per
|
|
57
|
+
* request while it stays unchanged. Assignment is idempotent: a repeated turn
|
|
58
|
+
* rewrites the same value and pi's section diff reports no change.
|
|
59
|
+
*
|
|
60
|
+
* Nothing volatile goes in here: live occupancy would rewrite the cached prefix
|
|
61
|
+
* on every run, and admission rechecks capacity at spawn anyway.
|
|
62
|
+
*/
|
|
63
|
+
export const SUBAGENT_SETTINGS_SECTION = "subagents";
|
|
64
|
+
|
|
65
|
+
/** `sections` is optional because the repo typechecks against older pi typings. */
|
|
66
|
+
export interface PromptSectionTarget {
|
|
67
|
+
sections?: Record<string, string>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Published with the settings block: the block is the authority on delegation, so
|
|
72
|
+
* the sentence that says so has to sit beside it rather than in a separate section
|
|
73
|
+
* whose position would depend on package load order.
|
|
74
|
+
*
|
|
75
|
+
* The note must not say the block is at the end of the prompt. Custom sections
|
|
76
|
+
* render in publication order, so Background Tasks and Goal publish after this one;
|
|
77
|
+
* naming its own section keeps the sentence true whatever the load order.
|
|
78
|
+
*/
|
|
79
|
+
const SUBAGENT_DELEGATION_NOTE = "Treat the Subagent settings block in this section as the authoritative delegation mode and cap. Its delegation guidance replaces earlier mode-specific guidance, including guidance in a plan. Live occupancy is not pushed into context: inspect subagent_settings when it matters, and never treat the concurrency limit as a target number of agents.";
|
|
80
|
+
|
|
81
|
+
export function publishSubagentSettings<T extends object>(options: T, settings: EffectiveSubagentSettings): T {
|
|
82
|
+
// Note first, block last inside the section: the note introduces the block it
|
|
83
|
+
// describes. Section order in the prompt is a separate concern, decided by
|
|
84
|
+
// publication order, so the note never claims a position in the whole prompt.
|
|
85
|
+
const target = options as PromptSectionTarget;
|
|
86
|
+
const sections = (target.sections ??= {});
|
|
87
|
+
sections[SUBAGENT_SETTINGS_SECTION] = `${SUBAGENT_DELEGATION_NOTE}\n\n${formatSubagentSettings(settings)}`;
|
|
88
|
+
return options;
|
|
89
|
+
}
|
|
90
|
+
|
|
44
91
|
export function subagentSettingsTool(Type: any, read: () => EffectiveSubagentSettings) {
|
|
45
92
|
return {
|
|
46
93
|
name: "subagent_settings",
|
|
@@ -60,4 +107,31 @@ export function subagentSettingsTool(Type: any, read: () => EffectiveSubagentSet
|
|
|
60
107
|
details: settings, structuredContent: settings };
|
|
61
108
|
},
|
|
62
109
|
};
|
|
63
|
-
}
|
|
110
|
+
}
|
|
111
|
+
/** What a handler receives; only these two fields are used. */
|
|
112
|
+
export interface BeforeAgentStartEvent {
|
|
113
|
+
systemPrompt: string;
|
|
114
|
+
systemPromptOptions?: unknown;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Deliver the policy and the settings on both kinds of host.
|
|
119
|
+
*
|
|
120
|
+
* pi before 0.84.4+ renders no custom sections, so publishing alone would
|
|
121
|
+
* deliver nothing on a host this package still declares support for. There the
|
|
122
|
+
* guidance is appended as text, which is what those hosts did before sections.
|
|
123
|
+
*/
|
|
124
|
+
export function deliverSubagentPrompt(event: BeforeAgentStartEvent, settings: EffectiveSubagentSettings): { systemPrompt: string } | undefined {
|
|
125
|
+
if (!supportsPromptSections(event.systemPromptOptions)) {
|
|
126
|
+
// Both packages carry this policy, so appending blindly would print it
|
|
127
|
+
// once per publisher; keep the text path idempotent as it used to be.
|
|
128
|
+
const policy = event.systemPrompt.includes(EXECUTION_POLICY_TEXT) ? "" : `${EXECUTION_POLICY_TEXT}\n\n`;
|
|
129
|
+
const block = `${SUBAGENT_DELEGATION_NOTE}\n\n${formatSubagentSettings(settings)}`;
|
|
130
|
+
const guidance = event.systemPrompt.includes(block) ? "" : block;
|
|
131
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${policy}${guidance}` };
|
|
132
|
+
}
|
|
133
|
+
const options = event.systemPromptOptions as PromptSectionTarget;
|
|
134
|
+
publishExecutionPolicy(options);
|
|
135
|
+
publishSubagentSettings(options, settings);
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
@@ -38,7 +38,7 @@ import { selectSessionByName } from "./session-selection.ts";
|
|
|
38
38
|
import { isWithinWorkspace } from "./workspace-scope.ts";
|
|
39
39
|
import { loadConfig, writeDelegationMode, writeSubagentSettings, isConcurrencyCap, normalizeConcurrencyCap, normalizeTools, resolveExtensionPath, configPath, selfDir, SAFE_DEFAULT_TOOLS, SAFE_CLEAN_TOOLS } from "./config.ts";
|
|
40
40
|
import { createSubagentSettingsPage } from "./settings-page.ts";
|
|
41
|
-
import { effectiveSubagentSettings, formatSubagentSettings, subagentSettingsTool, SUBAGENT_SETTINGS_CONTEXT } from "./effective-settings.ts";
|
|
41
|
+
import { deliverSubagentPrompt, effectiveSubagentSettings, formatSubagentSettings, subagentSettingsTool, SUBAGENT_SETTINGS_CONTEXT } from "./effective-settings.ts";
|
|
42
42
|
import { STEER_FILE_ENV, readSteerReceipt } from "./child-steer.ts";
|
|
43
43
|
import {
|
|
44
44
|
decideTiming,
|
|
@@ -56,7 +56,6 @@ import {
|
|
|
56
56
|
} from "./timing.ts";
|
|
57
57
|
import { readAppendedLines, type LogCursor } from "./log-cursor.ts";
|
|
58
58
|
import { DELEGATION_MODES, DELEGATION_MODE_REQUEST, isDelegationMode, normalizeDelegationMode, type DelegationMode } from "./delegation.ts";
|
|
59
|
-
import { appendExecutionPolicy } from "./shared-execution-policy.ts";
|
|
60
59
|
import { resolveChildClosure, resolveChildLaunch, configSnapshot } from "./child-dependencies.mjs";
|
|
61
60
|
import { PREFLIGHT_ENV } from "./child-preflight.mjs";
|
|
62
61
|
import { resolveExtensions, extensionArgs } from "./extensions.ts";
|
|
@@ -3602,18 +3601,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
3602
3601
|
const discoveryTool = agentOperations.createDiscoveryTool(Type as never);
|
|
3603
3602
|
pi.registerTool(discoveryTool as Parameters<ExtensionAPI["registerTool"]>[0]);
|
|
3604
3603
|
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
pi.on("context", (event) => {
|
|
3610
|
-
const settings = effectiveSettings();
|
|
3611
|
-
return { messages: [
|
|
3612
|
-
...event.messages.filter(message => message.role !== "custom" || message.customType !== SUBAGENT_SETTINGS_CONTEXT),
|
|
3613
|
-
{ role: "custom" as const, customType: SUBAGENT_SETTINGS_CONTEXT,
|
|
3614
|
-
content: formatSubagentSettings(settings), display: false, details: settings, timestamp: Date.now() },
|
|
3615
|
-
] };
|
|
3616
|
-
});
|
|
3604
|
+
// Sections on a host that renders them; text only where it renders none.
|
|
3605
|
+
// Returning a prompt on modern pi would set forceSystemPrompt, and the request
|
|
3606
|
+
// would then carry a snapshot, so sections published after it never arrive.
|
|
3607
|
+
pi.on("before_agent_start", (event) => deliverSubagentPrompt(event, effectiveSettings()));
|
|
3617
3608
|
|
|
3618
3609
|
// ---- live-status lifecycle -----------------------------------------
|
|
3619
3610
|
pi.on("agent_start", async (_event, ctx) => {
|
|
@@ -1,21 +1,61 @@
|
|
|
1
1
|
// Generated from packages/pi-better-harness/extensions/execution-policy.ts. Do not edit directly.
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Harness-wide scheduling guidance, vendored so standalone extensions share it.
|
|
4
|
+
*
|
|
5
|
+
* Published as a named system-prompt section, never as appended text. A
|
|
6
|
+
* `before_agent_start` handler that returns `systemPrompt` sets
|
|
7
|
+
* `forceSystemPrompt`, and a forced prompt is opaque to pi: `buildSystemPromptState`
|
|
8
|
+
* returns `{ content }` with no sections while `diffSystemPromptSections` keeps
|
|
9
|
+
* diffing the sections the request therefore never shows. The request carries a
|
|
10
|
+
* rendered snapshot, so any section published after that snapshot never reaches
|
|
11
|
+
* it, so every package that speaks here publishes a section.
|
|
12
|
+
*/
|
|
13
|
+
export const EXECUTION_POLICY_SECTION = "harness_execution_policy";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The minimum shape this module needs. `sections` stays optional because this
|
|
17
|
+
* repo typechecks against older pi typings whose event options predate sections;
|
|
18
|
+
* the shipped runtime always supplies the map.
|
|
19
|
+
*/
|
|
20
|
+
export interface PromptSectionTarget {
|
|
21
|
+
sections?: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const EXECUTION_POLICY_TEXT: string = [
|
|
4
25
|
"Harness execution policy: concurrency is separate from subagent delegation.",
|
|
5
|
-
"Before multi-unit work, identify ready, authorized, independent units, actual prerequisites, shared mutable resources, and capacity or availability constraints. Represent independently verifiable
|
|
26
|
+
"Before multi-unit work, identify ready, authorized, independent units, actual prerequisites, shared mutable resources, and capacity or availability constraints. Represent each independently verifiable deliverable as its own plan step; do not create dependencies from list order.",
|
|
6
27
|
"Start all ready, authorized, non-conflicting units concurrently within known resource limits before waiting for any one to finish. Reassess ready work after launches and results; continue unblocked foreground work instead of polling running tasks. Do not equate one background launch with scheduling all ready work.",
|
|
7
28
|
"Serialize only for a concrete dependency, conflicting writes or ownership, resource or concurrency limit, documented rollout gate, or explicit availability requirement. Record the reason in the plan or explain it to the user. Do not invent a canary or host-by-host availability requirement.",
|
|
8
|
-
"If concurrency raises an unresolved material operational trade-off
|
|
9
|
-
"Choose the execution mechanism after deciding what can run together: foreground tools for small, tightly coupled, or interactive work; subagents for bounded independent reasoning or implementation when the active delegation mode permits; background tasks for independent long-running commands or waits. Manual delegation forbids proactive subagents, not concurrent foreground tools or background commands.
|
|
10
|
-
"Established command-only jobs and external waits belong to background operations, including in coordinator mode. Use bg_task_spawn for a long command
|
|
11
|
-
"If a worker hands back a command,
|
|
29
|
+
"If concurrency raises an unresolved material operational trade-off, ask the user once before the affected operations and continue independent preparation meanwhile. Existing authorization, safety checks, workflow prerequisites, and human-approved rollout constraints remain binding; urgency does not waive them.",
|
|
30
|
+
"Choose the execution mechanism after deciding what can run together: foreground tools for small, tightly coupled, or interactive work; subagents for bounded independent reasoning or implementation when the active delegation mode permits; background tasks for independent long-running commands or waits. Manual delegation forbids proactive subagents, not concurrent foreground tools or background commands. Inspect every launch receipt for failures or skipped work.",
|
|
31
|
+
"Established command-only jobs and external waits belong to background operations, including in coordinator mode. Use bg_task_spawn for a long command, bg_task_watch for a repeated check, and watch_delivery for supported merge delivery. A subagent assignment must require reasoning or implementation beyond launching and waiting. Workers retain their assigned validation: launch or watch, continue independent work, then park with wait_for_operations and inspect the terminal evidence on continuation. Continue independent work while it runs; a task launch is not completion of the assignment.",
|
|
32
|
+
"If a worker hands back a command, launch it through bg_task_spawn or bg_task_watch with the supplied cwd, and inspect its terminal result before claiming verification.",
|
|
12
33
|
"A unit waiting or failing does not block other ready independent units unless the evidence shows a shared blocker. Preserve each unit's guards and rollback boundary. Before completing verification, the plan, or the goal, inspect and integrate every relevant terminal result or failure; launch is not completion.",
|
|
13
34
|
].join("\n");
|
|
14
35
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
36
|
+
/**
|
|
37
|
+
* True when this pi runtime renders custom prompt sections.
|
|
38
|
+
*
|
|
39
|
+
* pi gained `systemPromptOptions.sections` after 0.84.4, and the packages here
|
|
40
|
+
* declare support back to 0.84.4. A section-only publisher is a silent no-op on
|
|
41
|
+
* an older host: the handler runs, the map is populated, and the prompt the model
|
|
42
|
+
* receives is unchanged. Callers must fall back to appending text instead.
|
|
43
|
+
*/
|
|
44
|
+
export function supportsPromptSections(options: unknown): boolean {
|
|
45
|
+
const sections = (options as { sections?: unknown } | undefined)?.sections;
|
|
46
|
+
return typeof sections === "object" && sections !== null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Publish the policy into `options.sections`.
|
|
51
|
+
*
|
|
52
|
+
* Assignment, not concatenation: two packages publishing the same section write
|
|
53
|
+
* identical text, so load order cannot duplicate or drop it, and a repeated turn
|
|
54
|
+
* rewrites the same value so pi's section diff reports no change.
|
|
55
|
+
*/
|
|
56
|
+
export function publishExecutionPolicy<T extends object>(options: T): T {
|
|
57
|
+
const target = options as PromptSectionTarget;
|
|
58
|
+
const sections = (target.sections ??= {});
|
|
59
|
+
sections[EXECUTION_POLICY_SECTION] = EXECUTION_POLICY_TEXT;
|
|
60
|
+
return options;
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-harness",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.2",
|
|
4
4
|
"description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, model routing, and Outcome & Todo.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -55,13 +55,13 @@
|
|
|
55
55
|
"test": "node --import tsx --import ../../scripts/isolate-registry.mjs --test test/*.test.mjs test/*.test.ts test/task-context/*.test.ts"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"pi-better-background-tasks": "0.14.
|
|
59
|
-
"pi-better-goal": "0.12.
|
|
58
|
+
"pi-better-background-tasks": "0.14.2",
|
|
59
|
+
"pi-better-goal": "0.12.1",
|
|
60
60
|
"pi-better-model-router": "0.1.2",
|
|
61
61
|
"pi-ollama-cloud": "0.13.1",
|
|
62
62
|
"pi-better-sandbox": "0.14.1",
|
|
63
63
|
"pi-better-ssh": "0.1.4",
|
|
64
|
-
"pi-better-subagents": "0.27.
|
|
64
|
+
"pi-better-subagents": "0.27.2",
|
|
65
65
|
"proper-lockfile": "^4.1.2",
|
|
66
66
|
"shell-quote": "1.12.0",
|
|
67
67
|
"smol-toml": "1.9.0",
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// Generated from packages/pi-better-harness/extensions/execution-policy.ts. Do not edit directly.
|
|
2
|
-
/** Harness-wide scheduling guidance, vendored so standalone extensions share it. */
|
|
3
|
-
const EXECUTION_POLICY = [
|
|
4
|
-
"Harness execution policy: concurrency is separate from subagent delegation.",
|
|
5
|
-
"Before multi-unit work, identify ready, authorized, independent units, actual prerequisites, shared mutable resources, and capacity or availability constraints. Represent independently verifiable deliverables (such as each host migration) separately in the plan; do not hide them in one serial rollout step or create dependencies merely from list order.",
|
|
6
|
-
"Start all ready, authorized, non-conflicting units concurrently within known resource limits before waiting for any one to finish. Reassess ready work after launches and results; continue unblocked foreground work instead of polling running tasks. Do not equate one background launch with scheduling all ready work.",
|
|
7
|
-
"Serialize only for a concrete dependency, conflicting writes or ownership, resource or concurrency limit, documented rollout gate, or explicit availability requirement. Record the reason in the plan or explain it to the user. Do not invent a canary or host-by-host availability requirement.",
|
|
8
|
-
"If concurrency raises an unresolved material operational trade-off (for example, draining all hosts could leave no available runners), ask the user once before the affected operations. Continue independent preparation meanwhile. Existing authorization, safety checks, workflow prerequisites, and human-approved rollout constraints remain binding; urgency does not waive them.",
|
|
9
|
-
"Choose the execution mechanism after deciding what can run together: foreground tools for small, tightly coupled, or interactive work; subagents for bounded independent reasoning or implementation when the active delegation mode permits; background tasks for independent long-running commands or waits. Manual delegation forbids proactive subagents, not concurrent foreground tools or background commands. Use available batch or parallel tool interfaces where appropriate and inspect every launch receipt for failures or skipped work.",
|
|
10
|
-
"A unit waiting or failing does not block other ready independent units unless the evidence shows a shared blocker. Preserve each unit's guards and rollback boundary. Before completing verification, the plan, or the goal, inspect and integrate every relevant terminal result or failure; launch is not completion.",
|
|
11
|
-
].join("\n");
|
|
12
|
-
|
|
13
|
-
export function appendExecutionPolicy(systemPrompt: string): string {
|
|
14
|
-
// Inspect the prompt, not module-global state: vendored copies have separate
|
|
15
|
-
// module identities, and every fresh turn or reload needs the policy again.
|
|
16
|
-
return systemPrompt.includes(EXECUTION_POLICY)
|
|
17
|
-
? systemPrompt
|
|
18
|
-
: `${systemPrompt}\n\n${EXECUTION_POLICY}`;
|
|
19
|
-
}
|