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.
Files changed (43) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/docs/tool-reference.md +4 -1
  3. package/docs/workflows.md +2 -2
  4. package/package.json +1 -1
  5. package/skills/council-mode/SKILL.md +48 -243
  6. package/skills/council-mode/references/pass-contracts.md +150 -0
  7. package/skills/pi-subagents/SKILL.md +87 -37
  8. package/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
  9. package/skills/pi-subagents/references/execution-controls.md +47 -6
  10. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  11. package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
  12. package/skills/pi-subagents/references/prompting-and-roles.md +34 -27
  13. package/skills/pi-subagents/references/review-and-validation.md +73 -0
  14. package/src/agents/agent-management.ts +154 -25
  15. package/src/api/shared-types.ts +2 -0
  16. package/src/extension/public-execution.ts +1 -0
  17. package/src/extension/schemas.ts +1 -0
  18. package/src/extension/tool-description.ts +8 -4
  19. package/src/runs/background/async-execution.ts +2 -2
  20. package/src/runs/background/async-job-tracker.ts +3 -0
  21. package/src/runs/background/async-status.ts +45 -2
  22. package/src/runs/background/control-channel.ts +3 -2
  23. package/src/runs/background/run-status.ts +13 -2
  24. package/src/runs/background/subagent-runner.ts +5 -1
  25. package/src/runs/background/subagent-wait.ts +10 -2
  26. package/src/runs/background/wait-completions.ts +3 -0
  27. package/src/runs/foreground/execution.ts +11 -2
  28. package/src/runs/foreground/subagent-executor.ts +98 -1
  29. package/src/runs/shared/async-status-projection.ts +138 -4
  30. package/src/runs/shared/background-process-options.ts +9 -0
  31. package/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
  32. package/src/runs/shared/mutation-evidence.ts +52 -3
  33. package/src/runs/shared/pi-args.ts +47 -1
  34. package/src/runs/shared/single-output.ts +45 -18
  35. package/src/runs/shared/subagent-prompt-runtime.ts +20 -2
  36. package/src/runs/shared/workflow-graph.ts +15 -0
  37. package/src/shared/types.ts +34 -1
  38. package/src/tui/fleet-status.ts +11 -3
  39. package/src/tui/render-helpers.ts +31 -0
  40. package/src/tui/render.ts +597 -112
  41. package/src/watchdog/change-signature.ts +40 -1
  42. package/src/workflows/host-command.ts +6 -1
  43. 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 statusOutput = git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
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) throw new Error(`${label} has unsupported fields: ${unknownFields.join(", ")}.`);
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) throw new Error("runs.host('" + key + "') params have unsupported fields: " + unknown.join(", ") + ".");
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");