pi-subagents 0.63.0 → 0.65.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/CHANGELOG.md +57 -1
- package/README.md +2 -2
- package/agents/reviewer.md +1 -1
- package/agents/scout.md +1 -1
- package/docs/agents.md +18 -16
- package/docs/configuration.md +7 -15
- package/docs/extension-api.md +15 -6
- package/docs/missions.md +2 -0
- package/docs/observability.md +10 -12
- package/docs/tool-reference.md +7 -6
- package/docs/watchdog.md +92 -114
- package/docs/workflows.md +5 -3
- package/package.json +3 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/skills/pi-subagents/references/execution-controls.md +6 -5
- package/src/agents/agent-management.ts +47 -15
- package/src/api/capability-ceiling.ts +0 -1
- package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
- package/src/api/preflight.ts +2 -3
- package/src/extension/doctor.ts +2 -10
- package/src/extension/fanout-child.ts +9 -11
- package/src/extension/index.ts +27 -5
- package/src/extension/public-execution.ts +14 -0
- package/src/extension/rpc.ts +3 -2
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +9 -8
- package/src/intercom/native-supervisor-channel.ts +138 -60
- package/src/intercom/supervisor-ui.ts +243 -0
- package/src/runs/background/async-execution.ts +46 -24
- package/src/runs/background/async-job-tracker.ts +11 -0
- package/src/runs/background/async-resume.ts +11 -2
- package/src/runs/background/control-channel.ts +2 -204
- package/src/runs/background/notify.ts +41 -2
- package/src/runs/background/process-terminal.ts +1 -1
- package/src/runs/background/run-child-session.ts +613 -0
- package/src/runs/background/run-status.ts +0 -1
- package/src/runs/background/runner-aliases.ts +125 -0
- package/src/runs/background/runner-child-sessions.ts +31 -0
- package/src/runs/background/scheduled-runs.ts +18 -4
- package/src/runs/background/subagent-runner.ts +203 -898
- package/src/runs/foreground/async-steering-action.ts +1 -17
- package/src/runs/foreground/execution.ts +196 -378
- package/src/runs/foreground/foreground-control.ts +4 -0
- package/src/runs/foreground/subagent-executor.ts +121 -58
- package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
- package/src/runs/shared/abort-recovery.ts +3 -3
- package/src/runs/shared/acceptance.ts +10 -0
- package/src/runs/shared/async-status-projection.ts +11 -43
- package/src/runs/shared/capability-ceiling.ts +1 -2
- package/src/runs/shared/child-hooks.ts +25 -0
- package/src/runs/shared/child-identity.ts +13 -2
- package/src/runs/shared/child-launch.ts +314 -0
- package/src/runs/shared/child-lifecycle.ts +25 -0
- package/src/runs/shared/child-runtime-config.ts +126 -0
- package/src/runs/shared/child-session.ts +342 -0
- package/src/runs/shared/child-tool-plan.ts +530 -0
- package/src/runs/shared/claude-code-adapter.ts +5 -1
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/external-cli-preflight.ts +16 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
- package/src/runs/shared/model-exclusions.ts +82 -14
- package/src/runs/shared/model-fallback.ts +47 -4
- package/src/runs/shared/nested-events.ts +29 -45
- package/src/runs/shared/nested-path.ts +0 -14
- package/src/runs/shared/orca-progress-tabs.ts +12 -7
- package/src/runs/shared/parallel-utils.ts +0 -2
- package/src/runs/shared/permissions.ts +0 -13
- package/src/runs/shared/process-signal.ts +4 -1
- package/src/runs/shared/run-fanout-budget.ts +0 -13
- package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
- package/src/runs/shared/structured-output.ts +17 -4
- package/src/runs/shared/subagent-control.ts +6 -2
- package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
- package/src/runs/shared/tool-availability.ts +18 -62
- package/src/runs/shared/tool-budget.ts +0 -14
- package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
- package/src/runs/shared/worktree.ts +117 -30
- package/src/shared/child-session-name.ts +1 -1
- package/src/shared/jsonl-writer.ts +11 -0
- package/src/shared/thinking-ceiling.ts +0 -6
- package/src/shared/types.ts +64 -30
- package/src/shared/utils.ts +3 -4
- package/src/slash/slash-commands.ts +0 -6
- package/src/tui/fleet.ts +0 -1
- package/src/tui/render.ts +238 -31
- package/src/watchdog/child-status.ts +54 -34
- package/src/watchdog/diff-tool.ts +77 -0
- package/src/watchdog/emission-guard.ts +5 -3
- package/src/watchdog/guidance.ts +20 -0
- package/src/watchdog/register-child.ts +28 -25
- package/src/watchdog/register-main.ts +10 -9
- package/src/watchdog/render.ts +4 -5
- package/src/watchdog/review.ts +15 -4
- package/src/watchdog/rules.ts +70 -0
- package/src/watchdog/runtime.ts +75 -92
- package/src/watchdog/scope.ts +0 -11
- package/src/watchdog/settings.ts +48 -104
- package/src/watchdog/types.ts +18 -32
- package/src/watchdog/warning-format.ts +0 -1
- package/src/workflows/chat-progress.ts +3 -2
- package/src/workflows/scripted-workflow.ts +48 -1
- package/src/workflows/workflow-checklist.ts +10 -12
- package/src/workflows/workflow-preflight.ts +28 -1
- package/src/runs/shared/child-protocol.ts +0 -415
- package/src/runs/shared/pi-args.ts +0 -1062
- package/src/runs/shared/subagent-startup-retry.ts +0 -116
- package/src/shared/post-exit-stdio-guard.ts +0 -85
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module aliases for the detached async runner.
|
|
3
|
+
*
|
|
4
|
+
* The runner is a plain Node process started through jiti. The child hooks it
|
|
5
|
+
* loads import pi's host packages (`@earendil-works/pi-coding-agent`,
|
|
6
|
+
* `pi-agent-core`, `pi-ai`, `pi-tui`, `typebox`), which are peer packages of
|
|
7
|
+
* this extension and are not installed next to it. pi's own extension loader
|
|
8
|
+
* aliases those specifiers to the copies shipped inside the installed pi
|
|
9
|
+
* package; the parent computes the same map and hands it to the runner
|
|
10
|
+
* through `JITI_ALIAS`, so the runner's child sessions and hooks share one
|
|
11
|
+
* copy of every host package.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
|
|
16
|
+
export const JITI_ALIAS_ENV = "JITI_ALIAS";
|
|
17
|
+
|
|
18
|
+
/** Specifiers the runner's import graph may use, with the package and export subpath each resolves to. */
|
|
19
|
+
export const HOST_PEER_ALIASES: ReadonlyArray<{ specifier: string; pkg: string; subpath: string }> = [
|
|
20
|
+
{ specifier: "@earendil-works/pi-coding-agent", pkg: "@earendil-works/pi-coding-agent", subpath: "." },
|
|
21
|
+
{ specifier: "@earendil-works/pi-agent-core", pkg: "@earendil-works/pi-agent-core", subpath: "." },
|
|
22
|
+
{ specifier: "@earendil-works/pi-tui", pkg: "@earendil-works/pi-tui", subpath: "." },
|
|
23
|
+
{ specifier: "@earendil-works/pi-ai", pkg: "@earendil-works/pi-ai", subpath: "./compat" },
|
|
24
|
+
{ specifier: "@earendil-works/pi-ai/compat", pkg: "@earendil-works/pi-ai", subpath: "./compat" },
|
|
25
|
+
{ specifier: "@earendil-works/pi-ai/oauth", pkg: "@earendil-works/pi-ai", subpath: "./oauth" },
|
|
26
|
+
{ specifier: "@earendil-works/pi-ai/providers/all", pkg: "@earendil-works/pi-ai", subpath: "./providers/all" },
|
|
27
|
+
{ specifier: "typebox", pkg: "typebox", subpath: "." },
|
|
28
|
+
{ specifier: "typebox/compile", pkg: "typebox", subpath: "./compile" },
|
|
29
|
+
{ specifier: "typebox/value", pkg: "typebox", subpath: "./value" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
interface PackageManifest {
|
|
33
|
+
name?: unknown;
|
|
34
|
+
main?: unknown;
|
|
35
|
+
exports?: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readManifest(packageDir: string): PackageManifest | undefined {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(fs.readFileSync(path.join(packageDir, "package.json"), "utf-8")) as PackageManifest;
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Pick the import target of one exports entry (string, conditions object, or array of those). */
|
|
47
|
+
function exportTarget(entry: unknown): string | undefined {
|
|
48
|
+
if (typeof entry === "string") return entry;
|
|
49
|
+
if (Array.isArray(entry)) {
|
|
50
|
+
for (const candidate of entry) {
|
|
51
|
+
const target = exportTarget(candidate);
|
|
52
|
+
if (target) return target;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (entry && typeof entry === "object") {
|
|
57
|
+
const conditions = entry as Record<string, unknown>;
|
|
58
|
+
for (const condition of ["import", "node", "default"]) {
|
|
59
|
+
if (condition in conditions) {
|
|
60
|
+
const target = exportTarget(conditions[condition]);
|
|
61
|
+
if (target) return target;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Resolve `subpath` of the package at `packageDir` through its `exports` map (with `*` patterns) or `main`. */
|
|
69
|
+
export function resolvePackageSubpath(packageDir: string, subpath: string): string | undefined {
|
|
70
|
+
const manifest = readManifest(packageDir);
|
|
71
|
+
if (!manifest) return undefined;
|
|
72
|
+
const exportsField = manifest.exports;
|
|
73
|
+
if (exportsField !== undefined) {
|
|
74
|
+
const map: Record<string, unknown> = typeof exportsField === "string" || Array.isArray(exportsField) || (exportsField && typeof exportsField === "object" && !Object.keys(exportsField as object).some((key) => key.startsWith(".")))
|
|
75
|
+
? { ".": exportsField }
|
|
76
|
+
: exportsField as Record<string, unknown>;
|
|
77
|
+
const exact = exportTarget(map[subpath]);
|
|
78
|
+
if (exact) return path.resolve(packageDir, exact);
|
|
79
|
+
for (const [pattern, entry] of Object.entries(map)) {
|
|
80
|
+
const star = pattern.indexOf("*");
|
|
81
|
+
if (star === -1) continue;
|
|
82
|
+
const prefix = pattern.slice(0, star);
|
|
83
|
+
const suffix = pattern.slice(star + 1);
|
|
84
|
+
if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix) || subpath.length < prefix.length + suffix.length) continue;
|
|
85
|
+
const target = exportTarget(entry);
|
|
86
|
+
if (!target) continue;
|
|
87
|
+
const wildcard = subpath.slice(prefix.length, subpath.length - suffix.length);
|
|
88
|
+
return path.resolve(packageDir, target.replace("*", wildcard));
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
if (subpath !== ".") return undefined;
|
|
93
|
+
return path.resolve(packageDir, typeof manifest.main === "string" && manifest.main.trim() ? manifest.main : "index.js");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Find `pkg` as the pi package itself, one of its dependencies, or a sibling in a hoisted install. */
|
|
97
|
+
export function findHostPeerPackageDir(piPackageRoot: string, pkg: string): string | undefined {
|
|
98
|
+
if (readManifest(piPackageRoot)?.name === pkg) return piPackageRoot;
|
|
99
|
+
const candidates = [path.join(piPackageRoot, "node_modules", pkg)];
|
|
100
|
+
let dir = piPackageRoot;
|
|
101
|
+
for (;;) {
|
|
102
|
+
const parent = path.dirname(dir);
|
|
103
|
+
if (parent === dir) break;
|
|
104
|
+
if (path.basename(parent) === "node_modules" || path.basename(path.dirname(parent)) === "node_modules") {
|
|
105
|
+
const modulesRoot = path.basename(parent) === "node_modules" ? parent : path.dirname(parent);
|
|
106
|
+
candidates.push(path.join(modulesRoot, pkg));
|
|
107
|
+
}
|
|
108
|
+
candidates.push(path.join(parent, "node_modules", pkg));
|
|
109
|
+
dir = parent;
|
|
110
|
+
}
|
|
111
|
+
return candidates.find((candidate) => readManifest(candidate)?.name === pkg);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The alias map the runner needs, or the specifiers that could not be resolved. */
|
|
115
|
+
export function resolveHostPeerAliases(piPackageRoot: string): { aliases: Record<string, string>; missing: string[] } {
|
|
116
|
+
const aliases: Record<string, string> = {};
|
|
117
|
+
const missing: string[] = [];
|
|
118
|
+
for (const { specifier, pkg, subpath } of HOST_PEER_ALIASES) {
|
|
119
|
+
const packageDir = findHostPeerPackageDir(piPackageRoot, pkg);
|
|
120
|
+
const target = packageDir ? resolvePackageSubpath(packageDir, subpath) : undefined;
|
|
121
|
+
if (target && fs.existsSync(target)) aliases[specifier] = target;
|
|
122
|
+
else missing.push(specifier);
|
|
123
|
+
}
|
|
124
|
+
return { aliases, missing };
|
|
125
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child session factory for the detached async runner.
|
|
3
|
+
*
|
|
4
|
+
* The runner imports `@earendil-works/pi-coding-agent` like the parent does;
|
|
5
|
+
* the parent aliases that specifier (and the other host peer packages) to the
|
|
6
|
+
* installed pi package through `JITI_ALIAS` when it spawns the runner, see
|
|
7
|
+
* `runner-aliases.ts`. Tests replace the factory with a scripted one by
|
|
8
|
+
* naming a module in the runner config.
|
|
9
|
+
*/
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
import { createDefaultChildSessionFactory, type ChildSessionFactory } from "../shared/child-session.ts";
|
|
13
|
+
|
|
14
|
+
export interface RunnerChildSessionConfig {
|
|
15
|
+
/** Test seam: module whose default export is a `ChildSessionFactory`, or a function returning one. */
|
|
16
|
+
childSessionFactoryModule?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isChildSessionFactory(value: unknown): value is ChildSessionFactory {
|
|
20
|
+
return Boolean(value) && typeof value === "object" && typeof (value as ChildSessionFactory).create === "function" && typeof (value as ChildSessionFactory).dispose === "function";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function loadRunnerChildSessionFactory(config: RunnerChildSessionConfig): Promise<ChildSessionFactory> {
|
|
24
|
+
if (!config.childSessionFactoryModule) return createDefaultChildSessionFactory();
|
|
25
|
+
const loaded = await import(pathToFileURL(path.resolve(config.childSessionFactoryModule)).href) as { default?: unknown };
|
|
26
|
+
const candidate = typeof loaded.default === "function" ? (loaded.default as () => unknown)() : loaded.default;
|
|
27
|
+
if (!isChildSessionFactory(candidate)) {
|
|
28
|
+
throw new Error(`Child session factory module '${config.childSessionFactoryModule}' must default-export a ChildSessionFactory or a function returning one.`);
|
|
29
|
+
}
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
@@ -14,6 +14,7 @@ import type { ResolvedSubagentCapabilityCeiling } from "../shared/capability-cei
|
|
|
14
14
|
import { previewSimpleWorkflowRun } from "../../workflows/scripted-workflow.ts";
|
|
15
15
|
import { resolveGitRepositoryIdentity } from "../../workflows/chat-progress.ts";
|
|
16
16
|
import { getConfigDirName } from "../../shared/utils.ts";
|
|
17
|
+
import { normalizeWorktreeBaseRef } from "../shared/worktree.ts";
|
|
17
18
|
|
|
18
19
|
export const SCHEDULED_RUN_ACTIONS = [
|
|
19
20
|
"schedule.create",
|
|
@@ -39,7 +40,7 @@ export type ScheduleRunState = "running" | "skipped" | "missed" | "completed" |
|
|
|
39
40
|
export type ScheduleTrigger =
|
|
40
41
|
| { kind: "once"; at: string; nextRunAt?: string }
|
|
41
42
|
| { kind: "interval"; every: string; everyMs: number; anchorAt: string; nextRunAt: string };
|
|
42
|
-
export type ScheduleTarget = { workflowScript: string };
|
|
43
|
+
export type ScheduleTarget = { workflowScript: string; baseRef?: string };
|
|
43
44
|
|
|
44
45
|
export interface ScheduleRecord {
|
|
45
46
|
schemaVersion: 1;
|
|
@@ -281,8 +282,15 @@ function readJson(file: string, label: string): unknown {
|
|
|
281
282
|
|
|
282
283
|
function parseScheduleTarget(value: unknown, file: string): ScheduleTarget {
|
|
283
284
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Schedule record '${file}' has invalid trigger or target.`);
|
|
284
|
-
const target = value as { workflowScript?: unknown; agent?: unknown; task?: unknown };
|
|
285
|
-
if (typeof target.workflowScript === "string" && target.workflowScript.trim())
|
|
285
|
+
const target = value as { workflowScript?: unknown; baseRef?: unknown; agent?: unknown; task?: unknown };
|
|
286
|
+
if (typeof target.workflowScript === "string" && target.workflowScript.trim()) {
|
|
287
|
+
try {
|
|
288
|
+
const baseRef = normalizeWorktreeBaseRef(target.baseRef);
|
|
289
|
+
return { workflowScript: target.workflowScript.trim(), ...(baseRef === undefined ? {} : { baseRef }) };
|
|
290
|
+
} catch (error) {
|
|
291
|
+
throw new Error(`Schedule record '${file}' has an invalid baseRef: ${error instanceof Error ? error.message : String(error)}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
286
294
|
if (target.agent !== undefined || target.task !== undefined) throw new Error(`Schedule record '${file}' uses a removed legacy agent target; recreate it with target.workflowScript.`);
|
|
287
295
|
throw new Error(`Schedule record '${file}' requires a workflowScript target.`);
|
|
288
296
|
}
|
|
@@ -429,9 +437,15 @@ function sanitizeTarget(params: SubagentParamsLike): { target?: ScheduleTarget;
|
|
|
429
437
|
if (typeof params.workflowScript !== "string" || !params.workflowScript.trim()) return { error: "schedule.create requires a non-empty workflowScript." };
|
|
430
438
|
if (params.context === "fork") return { error: "Scheduled runs require fresh context." };
|
|
431
439
|
if (params.async === false) return { error: "Scheduled runs are always async." };
|
|
440
|
+
let baseRef: string | undefined;
|
|
441
|
+
try {
|
|
442
|
+
baseRef = normalizeWorktreeBaseRef(params.baseRef);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
445
|
+
}
|
|
432
446
|
const acceptanceErrors = validateExecutionAcceptance(params as Parameters<typeof validateExecutionAcceptance>[0]);
|
|
433
447
|
if (acceptanceErrors.length) return { error: acceptanceErrors.join(" ") };
|
|
434
|
-
return { target: { workflowScript: params.workflowScript.trim() } };
|
|
448
|
+
return { target: { workflowScript: params.workflowScript.trim(), ...(baseRef === undefined ? {} : { baseRef }) } };
|
|
435
449
|
}
|
|
436
450
|
|
|
437
451
|
function executionParams(schedule: ScheduleRecord): SubagentParamsLike {
|