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.
Files changed (107) hide show
  1. package/CHANGELOG.md +57 -1
  2. package/README.md +2 -2
  3. package/agents/reviewer.md +1 -1
  4. package/agents/scout.md +1 -1
  5. package/docs/agents.md +18 -16
  6. package/docs/configuration.md +7 -15
  7. package/docs/extension-api.md +15 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +7 -6
  11. package/docs/watchdog.md +92 -114
  12. package/docs/workflows.md +5 -3
  13. package/package.json +3 -4
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  15. package/skills/pi-subagents/references/execution-controls.md +6 -5
  16. package/src/agents/agent-management.ts +47 -15
  17. package/src/api/capability-ceiling.ts +0 -1
  18. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  19. package/src/api/preflight.ts +2 -3
  20. package/src/extension/doctor.ts +2 -10
  21. package/src/extension/fanout-child.ts +9 -11
  22. package/src/extension/index.ts +27 -5
  23. package/src/extension/public-execution.ts +14 -0
  24. package/src/extension/rpc.ts +3 -2
  25. package/src/extension/schemas.ts +2 -1
  26. package/src/extension/tool-description.ts +9 -8
  27. package/src/intercom/native-supervisor-channel.ts +138 -60
  28. package/src/intercom/supervisor-ui.ts +243 -0
  29. package/src/runs/background/async-execution.ts +46 -24
  30. package/src/runs/background/async-job-tracker.ts +11 -0
  31. package/src/runs/background/async-resume.ts +11 -2
  32. package/src/runs/background/control-channel.ts +2 -204
  33. package/src/runs/background/notify.ts +41 -2
  34. package/src/runs/background/process-terminal.ts +1 -1
  35. package/src/runs/background/run-child-session.ts +613 -0
  36. package/src/runs/background/run-status.ts +0 -1
  37. package/src/runs/background/runner-aliases.ts +125 -0
  38. package/src/runs/background/runner-child-sessions.ts +31 -0
  39. package/src/runs/background/scheduled-runs.ts +18 -4
  40. package/src/runs/background/subagent-runner.ts +203 -898
  41. package/src/runs/foreground/async-steering-action.ts +1 -17
  42. package/src/runs/foreground/execution.ts +196 -378
  43. package/src/runs/foreground/foreground-control.ts +4 -0
  44. package/src/runs/foreground/subagent-executor.ts +121 -58
  45. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  46. package/src/runs/shared/abort-recovery.ts +3 -3
  47. package/src/runs/shared/acceptance.ts +10 -0
  48. package/src/runs/shared/async-status-projection.ts +11 -43
  49. package/src/runs/shared/capability-ceiling.ts +1 -2
  50. package/src/runs/shared/child-hooks.ts +25 -0
  51. package/src/runs/shared/child-identity.ts +13 -2
  52. package/src/runs/shared/child-launch.ts +314 -0
  53. package/src/runs/shared/child-lifecycle.ts +25 -0
  54. package/src/runs/shared/child-runtime-config.ts +126 -0
  55. package/src/runs/shared/child-session.ts +342 -0
  56. package/src/runs/shared/child-tool-plan.ts +530 -0
  57. package/src/runs/shared/claude-code-adapter.ts +5 -1
  58. package/src/runs/shared/completion-guard.ts +1 -1
  59. package/src/runs/shared/external-cli-preflight.ts +16 -0
  60. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  61. package/src/runs/shared/model-exclusions.ts +82 -14
  62. package/src/runs/shared/model-fallback.ts +47 -4
  63. package/src/runs/shared/nested-events.ts +29 -45
  64. package/src/runs/shared/nested-path.ts +0 -14
  65. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  66. package/src/runs/shared/parallel-utils.ts +0 -2
  67. package/src/runs/shared/permissions.ts +0 -13
  68. package/src/runs/shared/process-signal.ts +4 -1
  69. package/src/runs/shared/run-fanout-budget.ts +0 -13
  70. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  71. package/src/runs/shared/structured-output.ts +17 -4
  72. package/src/runs/shared/subagent-control.ts +6 -2
  73. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  74. package/src/runs/shared/tool-availability.ts +18 -62
  75. package/src/runs/shared/tool-budget.ts +0 -14
  76. package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
  77. package/src/runs/shared/worktree.ts +117 -30
  78. package/src/shared/child-session-name.ts +1 -1
  79. package/src/shared/jsonl-writer.ts +11 -0
  80. package/src/shared/thinking-ceiling.ts +0 -6
  81. package/src/shared/types.ts +64 -30
  82. package/src/shared/utils.ts +3 -4
  83. package/src/slash/slash-commands.ts +0 -6
  84. package/src/tui/fleet.ts +0 -1
  85. package/src/tui/render.ts +238 -31
  86. package/src/watchdog/child-status.ts +54 -34
  87. package/src/watchdog/diff-tool.ts +77 -0
  88. package/src/watchdog/emission-guard.ts +5 -3
  89. package/src/watchdog/guidance.ts +20 -0
  90. package/src/watchdog/register-child.ts +28 -25
  91. package/src/watchdog/register-main.ts +10 -9
  92. package/src/watchdog/render.ts +4 -5
  93. package/src/watchdog/review.ts +15 -4
  94. package/src/watchdog/rules.ts +70 -0
  95. package/src/watchdog/runtime.ts +75 -92
  96. package/src/watchdog/scope.ts +0 -11
  97. package/src/watchdog/settings.ts +48 -104
  98. package/src/watchdog/types.ts +18 -32
  99. package/src/watchdog/warning-format.ts +0 -1
  100. package/src/workflows/chat-progress.ts +3 -2
  101. package/src/workflows/scripted-workflow.ts +48 -1
  102. package/src/workflows/workflow-checklist.ts +10 -12
  103. package/src/workflows/workflow-preflight.ts +28 -1
  104. package/src/runs/shared/child-protocol.ts +0 -415
  105. package/src/runs/shared/pi-args.ts +0 -1062
  106. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  107. 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()) return { workflowScript: 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 {