pi-subagents 0.59.0 → 0.60.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 +37 -0
- package/docs/tool-reference.md +4 -1
- package/docs/workflows.md +2 -2
- package/package.json +1 -1
- package/skills/council-mode/SKILL.md +48 -243
- package/skills/council-mode/references/pass-contracts.md +150 -0
- package/skills/pi-subagents/SKILL.md +87 -37
- package/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
- package/skills/pi-subagents/references/execution-controls.md +47 -6
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +34 -27
- package/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/src/agents/agent-management.ts +154 -25
- package/src/api/shared-types.ts +2 -0
- package/src/extension/public-execution.ts +1 -0
- package/src/extension/schemas.ts +1 -0
- package/src/extension/tool-description.ts +8 -4
- package/src/runs/background/async-execution.ts +2 -2
- package/src/runs/background/async-job-tracker.ts +3 -0
- package/src/runs/background/async-status.ts +45 -2
- package/src/runs/background/control-channel.ts +3 -2
- package/src/runs/background/run-status.ts +13 -2
- package/src/runs/background/subagent-runner.ts +5 -1
- package/src/runs/background/subagent-wait.ts +10 -2
- package/src/runs/background/wait-completions.ts +3 -0
- package/src/runs/foreground/execution.ts +11 -2
- package/src/runs/foreground/subagent-executor.ts +98 -1
- package/src/runs/shared/async-status-projection.ts +138 -4
- package/src/runs/shared/background-process-options.ts +9 -0
- package/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
- package/src/runs/shared/mutation-evidence.ts +52 -3
- package/src/runs/shared/pi-args.ts +47 -1
- package/src/runs/shared/single-output.ts +45 -18
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -2
- package/src/runs/shared/workflow-graph.ts +15 -0
- package/src/shared/types.ts +34 -1
- package/src/tui/fleet-status.ts +11 -3
- package/src/tui/render-helpers.ts +31 -0
- package/src/tui/render.ts +597 -112
- package/src/watchdog/change-signature.ts +40 -1
- package/src/workflows/host-command.ts +6 -1
- package/src/workflows/scripted-workflow.ts +53 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
4
5
|
import * as path from "node:path";
|
|
5
6
|
import { PROJECT_SUBAGENTS_RELATIVE_DIR } from "../shared/artifacts.ts";
|
|
6
7
|
|
|
@@ -11,6 +12,7 @@ const IGNORED_CHANGE_SEGMENTS = new Set([".git", "node_modules"]);
|
|
|
11
12
|
const DEFAULT_MAX_HASH_FILE_BYTES = 64 * 1024 * 1024;
|
|
12
13
|
const DEFAULT_MAX_HASH_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
13
14
|
const DEFAULT_MAX_HASH_ENTRIES = 2_000;
|
|
15
|
+
const TRACKED_ENTRY_PROBE_MAX_BYTES = 1_024;
|
|
14
16
|
|
|
15
17
|
function positiveEnvNumber(name: string, fallback: number): number {
|
|
16
18
|
const parsed = Number(process.env[name]);
|
|
@@ -43,6 +45,37 @@ function git(cwd: string, args: string[]): string | undefined {
|
|
|
43
45
|
return result.stdout;
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
function comparablePath(value: string): string {
|
|
49
|
+
let resolved = path.resolve(value);
|
|
50
|
+
try {
|
|
51
|
+
resolved = fs.realpathSync.native(resolved);
|
|
52
|
+
} catch {
|
|
53
|
+
// Keep the lexical path when canonicalization is unavailable.
|
|
54
|
+
}
|
|
55
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isHomeRepoRoot(root: string): boolean {
|
|
59
|
+
return comparablePath(root) === comparablePath(os.homedir());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Probe the index without asking Git to inspect the working tree. The output
|
|
64
|
+
* cap keeps this check bounded even for repositories with a very large index;
|
|
65
|
+
* an ENOBUFS result still proves that at least one tracked path exists.
|
|
66
|
+
*/
|
|
67
|
+
function hasTrackedEntries(root: string): boolean | undefined {
|
|
68
|
+
const result = spawnSync("git", ["-C", root, "ls-files", "--cached", "--error-unmatch", "--", "."], {
|
|
69
|
+
encoding: "utf-8",
|
|
70
|
+
maxBuffer: TRACKED_ENTRY_PROBE_MAX_BYTES,
|
|
71
|
+
windowsHide: true,
|
|
72
|
+
});
|
|
73
|
+
const errorCode = (result.error as NodeJS.ErrnoException | undefined)?.code;
|
|
74
|
+
if (result.status === 0 || errorCode === "ENOBUFS") return true;
|
|
75
|
+
if (result.status === 1) return false;
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
46
79
|
function normalizeRelPath(value: string): string {
|
|
47
80
|
return value.replaceAll(path.sep, "/").replace(/^\.\//, "");
|
|
48
81
|
}
|
|
@@ -181,7 +214,13 @@ function buildRepoChangeSignature(root: string, statusOutput: string): WatchdogR
|
|
|
181
214
|
export function computeWatchdogRepoChangeSignature(cwd: string): WatchdogRepoChangeSignature | undefined {
|
|
182
215
|
const root = git(cwd, ["rev-parse", "--show-toplevel"])?.trim();
|
|
183
216
|
if (!root) return undefined;
|
|
184
|
-
const
|
|
217
|
+
const skipUntracked = isHomeRepoRoot(root) || hasTrackedEntries(root) === false;
|
|
218
|
+
const statusOutput = git(root, [
|
|
219
|
+
"status",
|
|
220
|
+
"--porcelain=v1",
|
|
221
|
+
"-z",
|
|
222
|
+
`--untracked-files=${skipUntracked ? "no" : "all"}`,
|
|
223
|
+
]);
|
|
185
224
|
if (statusOutput === undefined) return undefined;
|
|
186
225
|
try {
|
|
187
226
|
return buildRepoChangeSignature(root, statusOutput);
|
|
@@ -53,7 +53,12 @@ export function normalizeWorkflowHostCommandParams(value: unknown, label = "runs
|
|
|
53
53
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
|
|
54
54
|
const params = value as Record<string, unknown>;
|
|
55
55
|
const unknownFields = Object.keys(params).filter((field) => !["kind", "command", "timeoutMs", "output", "role", "provider"].includes(field));
|
|
56
|
-
if (unknownFields.length)
|
|
56
|
+
if (unknownFields.length) {
|
|
57
|
+
const cwdHint = unknownFields.includes("cwd")
|
|
58
|
+
? " The host step does not accept per-step cwd; commands and relative output paths use the workflow cwd. Set cwd on the outer subagent request, or put a trusted directory change in command (for example, 'cd /path/to/worktree && npm test')."
|
|
59
|
+
: "";
|
|
60
|
+
throw new Error(`${label} has unsupported fields: ${unknownFields.join(", ")}.${cwdHint}`);
|
|
61
|
+
}
|
|
57
62
|
if (params.kind !== "command") throw new Error(`${label}.kind must be 'command'.`);
|
|
58
63
|
if (typeof params.command !== "string" || !params.command.trim()) throw new Error(`${label}.command must be a non-empty string.`);
|
|
59
64
|
const command = params.command.trim();
|
|
@@ -500,8 +500,26 @@ function runLane(lane, firstResult, observe) {
|
|
|
500
500
|
return visit(0, firstResult);
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
function workflowPlanStringMetadata(params) {
|
|
504
|
+
return {
|
|
505
|
+
...(typeof params.phase === "string" && params.phase.trim() ? { phase: params.phase.trim() } : {}),
|
|
506
|
+
...(typeof params.label === "string" && params.label.trim() ? { label: params.label.trim() } : {}),
|
|
507
|
+
...(typeof params.agent === "string" && params.agent.trim() ? { agent: params.agent.trim() } : {}),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
503
511
|
function runLanes(laneSpecs) {
|
|
504
512
|
const lanes = validateLaneSpecs(laneSpecs);
|
|
513
|
+
parentPort.postMessage({ type: "lanePlan", lanes: lanes.map((lane) => ({
|
|
514
|
+
key: lane.key,
|
|
515
|
+
stages: lane.stages.map((stage) => ({
|
|
516
|
+
key: stage.key,
|
|
517
|
+
generatedKey: stage.generatedKey,
|
|
518
|
+
...workflowPlanStringMetadata(stage.params),
|
|
519
|
+
...(typeof stage.params.as === "string" && stage.params.as.trim() ? { outputName: stage.params.as.trim() } : {}),
|
|
520
|
+
...(stage.params.outputSchema !== undefined ? { structured: true } : {}),
|
|
521
|
+
})),
|
|
522
|
+
})) });
|
|
505
523
|
const firstItems = lanes.map((lane) => {
|
|
506
524
|
const first = lane.stages[0];
|
|
507
525
|
return { key: first.generatedKey, ...first.params };
|
|
@@ -624,7 +642,12 @@ function validateHostCommand(key, params) {
|
|
|
624
642
|
if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("runs.host('" + key + "') params must be an object.");
|
|
625
643
|
const allowed = new Set(["kind", "command", "timeoutMs", "output", "role", "provider"]);
|
|
626
644
|
const unknown = Object.keys(params).filter((field) => !allowed.has(field));
|
|
627
|
-
if (unknown.length)
|
|
645
|
+
if (unknown.length) {
|
|
646
|
+
const cwdHint = unknown.includes("cwd")
|
|
647
|
+
? " The host step does not accept per-step cwd; commands and relative output paths use the workflow cwd. Set cwd on the outer subagent request, or put a trusted directory change in command (for example, 'cd /path/to/worktree && npm test')."
|
|
648
|
+
: "";
|
|
649
|
+
throw new Error("runs.host('" + key + "') params have unsupported fields: " + unknown.join(", ") + "." + cwdHint);
|
|
650
|
+
}
|
|
628
651
|
if (params.kind !== "command") throw new Error("runs.host('" + key + "') kind must be 'command'.");
|
|
629
652
|
if (typeof params.command !== "string" || !params.command.trim() || params.command.includes("\u0000") || new TextEncoder().encode(params.command.trim()).byteLength > 16384) throw new Error("runs.host('" + key + "') command must be a non-empty string of at most 16384 bytes without NUL.");
|
|
630
653
|
if (!Number.isInteger(params.timeoutMs) || params.timeoutMs < 1 || params.timeoutMs > 86400000) throw new Error("runs.host('" + key + "') timeoutMs must be an integer from 1 to 86400000.");
|
|
@@ -977,6 +1000,22 @@ export interface WorkflowScriptTraceEntry {
|
|
|
977
1000
|
warning?: string;
|
|
978
1001
|
}
|
|
979
1002
|
|
|
1003
|
+
/** Bounded plan metadata emitted when a workflow materializes a runs.lanes graph. */
|
|
1004
|
+
export interface WorkflowLanePlanStage {
|
|
1005
|
+
key: string;
|
|
1006
|
+
generatedKey: string;
|
|
1007
|
+
agent?: string;
|
|
1008
|
+
phase?: string;
|
|
1009
|
+
label?: string;
|
|
1010
|
+
outputName?: string;
|
|
1011
|
+
structured?: boolean;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
export interface WorkflowLanePlan {
|
|
1015
|
+
key: string;
|
|
1016
|
+
stages: WorkflowLanePlanStage[];
|
|
1017
|
+
}
|
|
1018
|
+
|
|
980
1019
|
export interface WorkflowSteerOptions {
|
|
981
1020
|
mode?: "steer" | "follow_up" | "auto";
|
|
982
1021
|
index?: number;
|
|
@@ -1044,6 +1083,7 @@ export interface RunWorkflowScriptOptions {
|
|
|
1044
1083
|
};
|
|
1045
1084
|
registerStopChild?: (stop: ((key: string, message?: string) => boolean) | undefined) => void;
|
|
1046
1085
|
onTrace?: (trace: WorkflowScriptTraceEntry[]) => void;
|
|
1086
|
+
onLanePlan?: (lanes: WorkflowLanePlan[]) => void;
|
|
1047
1087
|
onEmit?: (emits: unknown[]) => void;
|
|
1048
1088
|
}
|
|
1049
1089
|
|
|
@@ -1227,7 +1267,7 @@ function validateStaticHostCall(call: AstNode): WorkflowScriptValidationError[]
|
|
|
1227
1267
|
for (const property of params.properties) {
|
|
1228
1268
|
if (!astNode(property)) continue;
|
|
1229
1269
|
const name = staticPropertyKey(property);
|
|
1230
|
-
if (name !== undefined && !allowed.has(name)) errors.push({ message: `runs.host params contain unsupported field '${name}'.`, ...nodeLocation(property) });
|
|
1270
|
+
if (name !== undefined && !allowed.has(name)) errors.push({ message: name === "cwd" ? "runs.host params contain unsupported field 'cwd'. The host step does not accept per-step cwd; commands and relative output paths use the workflow cwd. Set cwd on the outer subagent request, or put a trusted directory change in command (for example, 'cd /path/to/worktree && npm test')." : `runs.host params contain unsupported field '${name}'.`, ...nodeLocation(property) });
|
|
1231
1271
|
}
|
|
1232
1272
|
const kindNode = directObjectPropertyValue(params, "kind");
|
|
1233
1273
|
const commandNode = directObjectPropertyValue(params, "command");
|
|
@@ -1524,6 +1564,13 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1524
1564
|
console.error("Workflow onTrace callback failed:", error);
|
|
1525
1565
|
}
|
|
1526
1566
|
};
|
|
1567
|
+
const lanePlanChanged = (lanes: WorkflowLanePlan[]) => {
|
|
1568
|
+
try {
|
|
1569
|
+
options.onLanePlan?.(lanes);
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
console.error("Workflow onLanePlan callback failed:", error);
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1527
1574
|
const hostStepChanged = (hostStep: HostStepNodeV1) => {
|
|
1528
1575
|
try {
|
|
1529
1576
|
options.onHostStep?.(hostStep);
|
|
@@ -1626,6 +1673,10 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1626
1673
|
});
|
|
1627
1674
|
worker.on("message", (message: Record<string, unknown>) => {
|
|
1628
1675
|
if (settled) return;
|
|
1676
|
+
if (message.type === "lanePlan" && Array.isArray(message.lanes)) {
|
|
1677
|
+
lanePlanChanged(message.lanes as WorkflowLanePlan[]);
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1629
1680
|
if (message.type === "emit") {
|
|
1630
1681
|
try {
|
|
1631
1682
|
assertWorkflowJsonValue(message.value, "emit");
|