pi-subagents 0.65.1 → 0.67.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 (148) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/researcher.md +23 -13
  5. package/agents/reviewer.md +3 -2
  6. package/docs/agents.md +20 -3
  7. package/docs/configuration.md +25 -5
  8. package/docs/extension-api.md +124 -18
  9. package/docs/missions.md +8 -0
  10. package/docs/models.md +59 -2
  11. package/docs/observability.md +46 -6
  12. package/docs/standalone-background.md +49 -0
  13. package/docs/tool-reference.md +20 -10
  14. package/docs/watchdog.md +35 -4
  15. package/docs/workflows.md +40 -19
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +2 -1
  18. package/prompts/parallel-review.md +1 -1
  19. package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
  20. package/skills/pi-subagents/SKILL.md +14 -0
  21. package/skills/pi-subagents/references/execution-controls.md +20 -5
  22. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  23. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  24. package/src/agents/advertised-agent-prompt.ts +94 -0
  25. package/src/agents/agent-management.ts +14 -1
  26. package/src/agents/agent-serializer.ts +2 -0
  27. package/src/agents/agents.ts +14 -0
  28. package/src/agents/builtin-names.ts +1 -0
  29. package/src/api/delegation.ts +4 -0
  30. package/src/api/preflight.ts +76 -45
  31. package/src/api/shared-types.ts +3 -1
  32. package/src/api/workflow-resources.ts +6 -0
  33. package/src/extension/fanout-child.ts +63 -4
  34. package/src/extension/index.ts +58 -8
  35. package/src/extension/public-execution.ts +4 -3
  36. package/src/extension/rpc.ts +8 -21
  37. package/src/extension/schemas.ts +71 -80
  38. package/src/extension/tool-description.ts +29 -81
  39. package/src/inspectors/actions.ts +148 -0
  40. package/src/inspectors/ghostty/actions.ts +74 -0
  41. package/src/inspectors/ghostty/plugin.ts +17 -0
  42. package/src/inspectors/herdr/actions.ts +99 -179
  43. package/src/inspectors/herdr/plugin.ts +20 -0
  44. package/src/inspectors/herdr/project-panes.ts +1 -1
  45. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  46. package/src/inspectors/plugins.ts +8 -0
  47. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  48. package/src/inspectors/types.ts +51 -0
  49. package/src/intercom/intercom-bridge.ts +50 -8
  50. package/src/intercom/native-supervisor-channel.ts +104 -67
  51. package/src/runs/background/active-async-capacity.ts +22 -18
  52. package/src/runs/background/async-execution.ts +45 -56
  53. package/src/runs/background/async-job-tracker.ts +35 -3
  54. package/src/runs/background/async-resume.ts +5 -9
  55. package/src/runs/background/async-status-snapshot.ts +10 -12
  56. package/src/runs/background/async-status.ts +17 -9
  57. package/src/runs/background/auto-drain.ts +44 -30
  58. package/src/runs/background/binary-bootstrap.ts +33 -0
  59. package/src/runs/background/chain-root-attachment.ts +8 -0
  60. package/src/runs/background/control-channel.ts +78 -44
  61. package/src/runs/background/fleet-view.ts +30 -2
  62. package/src/runs/background/notify.ts +117 -13
  63. package/src/runs/background/owned-process-tree.ts +35 -8
  64. package/src/runs/background/process-terminal.ts +23 -23
  65. package/src/runs/background/run-child-session.ts +121 -36
  66. package/src/runs/background/run-status.ts +78 -5
  67. package/src/runs/background/runner-aliases.ts +28 -9
  68. package/src/runs/background/runner-child-launch.ts +88 -0
  69. package/src/runs/background/runner-child-sessions.ts +5 -4
  70. package/src/runs/background/scheduled-runs.ts +40 -13
  71. package/src/runs/background/stale-run-reconciler.ts +3 -1
  72. package/src/runs/background/steering.ts +20 -2
  73. package/src/runs/background/subagent-runner.ts +458 -239
  74. package/src/runs/background/subagent-wait.ts +54 -8
  75. package/src/runs/background/wait-completions.ts +4 -0
  76. package/src/runs/background/wait-tool.ts +1 -1
  77. package/src/runs/foreground/async-steering-action.ts +37 -7
  78. package/src/runs/foreground/execution.ts +145 -56
  79. package/src/runs/foreground/prompt-audit.ts +3 -1
  80. package/src/runs/foreground/subagent-executor.ts +584 -297
  81. package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
  82. package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
  83. package/src/runs/shared/acceptance.ts +7 -4
  84. package/src/runs/shared/agent-contract.ts +1 -1
  85. package/src/runs/shared/async-status-projection.ts +51 -47
  86. package/src/runs/shared/capability-ceiling.ts +2 -0
  87. package/src/runs/shared/child-hooks.ts +167 -3
  88. package/src/runs/shared/child-launch.ts +28 -13
  89. package/src/runs/shared/child-lifecycle.ts +6 -3
  90. package/src/runs/shared/child-runtime-config.ts +3 -1
  91. package/src/runs/shared/child-session.ts +75 -8
  92. package/src/runs/shared/child-tool-plan.ts +124 -5
  93. package/src/runs/shared/completion-evidence.ts +2 -2
  94. package/src/runs/shared/completion-guard.ts +6 -3
  95. package/src/runs/shared/effective-system-prompt.ts +33 -0
  96. package/src/runs/shared/external-cli-runner.ts +9 -7
  97. package/src/runs/shared/host-step-status.ts +11 -11
  98. package/src/runs/shared/llm-intent-arbiter.ts +21 -11
  99. package/src/runs/shared/model-fallback.ts +12 -6
  100. package/src/runs/shared/nested-events.ts +5 -5
  101. package/src/runs/shared/orca-progress-tabs.ts +7 -1
  102. package/src/runs/shared/parallel-handoff.ts +57 -12
  103. package/src/runs/shared/parallel-utils.ts +2 -2
  104. package/src/runs/shared/pi-spawn.ts +10 -0
  105. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  106. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  107. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  108. package/src/runs/shared/run-fanout-budget.ts +8 -8
  109. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  110. package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
  111. package/src/runs/shared/task-intent.ts +46 -13
  112. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  113. package/src/runs/shared/worktree-setup-command.ts +190 -0
  114. package/src/runs/shared/worktree.ts +366 -208
  115. package/src/shared/fork-context.ts +15 -72
  116. package/src/shared/launch-contract.ts +65 -2
  117. package/src/shared/opencode-session-headers.ts +30 -0
  118. package/src/shared/types.ts +85 -61
  119. package/src/shared/utils.ts +7 -2
  120. package/src/shared/workflow-child-permit.ts +18 -13
  121. package/src/slash/delegation-adapters.ts +3 -1
  122. package/src/slash/delegation-request.ts +14 -0
  123. package/src/slash/slash-commands.ts +2 -1
  124. package/src/slash/subagents-admin.ts +11 -4
  125. package/src/tui/fleet-status.ts +164 -19
  126. package/src/tui/fleet.ts +27 -19
  127. package/src/tui/render.ts +172 -33
  128. package/src/watchdog/child-status.ts +8 -0
  129. package/src/watchdog/model-selection.ts +20 -0
  130. package/src/watchdog/permission-arbiter.ts +3 -1
  131. package/src/watchdog/register-child.ts +1 -0
  132. package/src/watchdog/register-main.ts +31 -27
  133. package/src/watchdog/review.ts +132 -67
  134. package/src/watchdog/runtime.ts +82 -20
  135. package/src/watchdog/scope.ts +1 -1
  136. package/src/watchdog/settings.ts +9 -3
  137. package/src/watchdog/tool-actions.ts +13 -12
  138. package/src/watchdog/turn-delta.ts +23 -0
  139. package/src/watchdog/types.ts +4 -0
  140. package/src/workflows/chat-progress.ts +3 -3
  141. package/src/workflows/scripted-workflow.ts +275 -17
  142. package/src/workflows/workflow-checklist.ts +13 -17
  143. package/src/workflows/workflow-child-summary.ts +57 -8
  144. package/src/workflows/workflow-preflight.ts +19 -19
  145. package/src/workflows/workflow-receipt.ts +3 -3
  146. package/src/workflows/workflow-resources.ts +96 -21
  147. package/src/workflows/workflow-settlement.ts +3 -0
  148. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -1,9 +1,42 @@
1
- import type { AsyncStatus, WorkflowChildSummaryV1 } from "../shared/types.ts";
1
+ import type { AgentProgress, AsyncStatus, WorkflowChildActivity, WorkflowChildSummary } from "../shared/types.ts";
2
2
  import type { WorkflowScriptChildResult, WorkflowScriptTraceEntry } from "./scripted-workflow.ts";
3
3
 
4
4
  const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
5
5
  const TERMINAL_STATES = new Set(["completed", "failed", "paused", "stopped", "rejected", "detached"]);
6
6
  const MAX_REQUIRED_ID_BYTES = 4_096;
7
+ const ACTIVITY_COUNTERS = ["currentToolStartedAt", "lastActivityAt", "durationMs", "toolCount", "turnCount", "tokens", "inputTokens", "outputTokens"] as const;
8
+ type WorkflowChildLiveProgress = WorkflowChildActivity & Pick<AgentProgress, "agent" | "sessionName" | "model" | "thinking">;
9
+
10
+ export function workflowChildProgress(progress: AgentProgress): WorkflowChildLiveProgress {
11
+ return {
12
+ ...workflowChildActivity(progress),
13
+ agent: bounded(progress.agent, 256) ?? "",
14
+ ...(bounded(progress.sessionName, 256) ? { sessionName: progress.sessionName } : {}),
15
+ ...(bounded(progress.model, 256) ? { model: progress.model } : {}),
16
+ ...(bounded(progress.thinking, 32) ? { thinking: progress.thinking } : {}),
17
+ };
18
+ }
19
+
20
+ /** Fixed field count and a 256 UTF-8 byte tool name keep JSON activity below 2 KiB, including escaping. */
21
+ export function workflowChildActivity(progress: WorkflowChildActivity): WorkflowChildActivity {
22
+ const activity: WorkflowChildActivity = {};
23
+ if (bounded(progress.currentTool, 256)) activity.currentTool = progress.currentTool;
24
+ for (const field of ACTIVITY_COUNTERS) {
25
+ const value = progress[field];
26
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) activity[field] = value;
27
+ }
28
+ return activity;
29
+ }
30
+
31
+ function parseActivity(value: unknown): WorkflowChildActivity | undefined {
32
+ if (value === undefined) return undefined;
33
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("workflowChildren child activity is invalid.");
34
+ const input = value as Record<string, unknown>;
35
+ for (const [key, value] of Object.entries(input)) {
36
+ if (key === "currentTool" ? !bounded(value, 256) : !ACTIVITY_COUNTERS.includes(key as typeof ACTIVITY_COUNTERS[number]) || typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new Error("workflowChildren child activity is invalid.");
37
+ }
38
+ return { ...input } as WorkflowChildActivity;
39
+ }
7
40
 
8
41
  function bounded(value: unknown, maxBytes: number): string | undefined {
9
42
  if (typeof value !== "string" || !value.trim() || Buffer.byteLength(value, "utf8") > maxBytes) return undefined;
@@ -19,13 +52,15 @@ function requiredId(value: string, label: string): string {
19
52
  export function workflowChildSummary(input: {
20
53
  parentToolCallId: string;
21
54
  workflowRunId: string;
22
- workflowState: WorkflowChildSummaryV1["workflowState"];
55
+ workflowState: WorkflowChildSummary["workflowState"];
23
56
  inventoryComplete: boolean;
24
57
  trace?: WorkflowScriptTraceEntry[];
25
58
  children?: WorkflowScriptChildResult[];
26
59
  steps?: NonNullable<AsyncStatus["steps"]>;
27
- }): WorkflowChildSummaryV1 {
28
- const rows = new Map<string, WorkflowChildSummaryV1["children"][number]>();
60
+ /** In-memory snapshots from synchronous child callbacks, keyed by workflow key. */
61
+ progress?: ReadonlyMap<string, WorkflowChildLiveProgress>;
62
+ }): WorkflowChildSummary {
63
+ const rows = new Map<string, WorkflowChildSummary["children"][number]>();
29
64
  for (const entry of input.trace ?? []) {
30
65
  if (entry.operation !== "run" || !KEY_PATTERN.test(entry.key)) continue;
31
66
  const previous = rows.get(entry.key);
@@ -83,6 +118,18 @@ export function workflowChildSummary(input: {
83
118
  if (!TERMINAL_STATES.has(row.state)) rows.set(key, { ...row, state: input.workflowState === "stopped" ? "stopped" : "failed" });
84
119
  }
85
120
  }
121
+ for (const [key, progress] of input.progress ?? []) {
122
+ const row = rows.get(key);
123
+ if (!row || row.state !== "running") continue;
124
+ rows.set(key, {
125
+ ...row,
126
+ ...(bounded(progress.agent, 256) ? { agent: progress.agent } : {}),
127
+ ...(bounded(progress.sessionName, 256) ? { sessionName: progress.sessionName } : {}),
128
+ ...(bounded(progress.model, 256) ? { model: progress.model } : {}),
129
+ ...(bounded(progress.thinking, 32) ? { thinking: progress.thinking } : {}),
130
+ activity: workflowChildActivity(progress),
131
+ });
132
+ }
86
133
  return {
87
134
  version: 1,
88
135
  parentToolCallId: requiredId(input.parentToolCallId, "parentToolCallId"),
@@ -93,7 +140,7 @@ export function workflowChildSummary(input: {
93
140
  };
94
141
  }
95
142
 
96
- export function parseWorkflowChildSummary(value: unknown): WorkflowChildSummaryV1 | undefined {
143
+ export function parseWorkflowChildSummary(value: unknown): WorkflowChildSummary | undefined {
97
144
  if (value === undefined) return undefined;
98
145
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("workflowChildren must be an object.");
99
146
  const input = value as Record<string, unknown>;
@@ -102,17 +149,19 @@ export function parseWorkflowChildSummary(value: unknown): WorkflowChildSummaryV
102
149
  if (input.version !== 1 || typeof input.inventoryComplete !== "boolean" || !Array.isArray(input.children)) throw new Error("workflowChildren is invalid.");
103
150
  const workflowState = input.workflowState;
104
151
  if (workflowState !== "queued" && workflowState !== "running" && workflowState !== "completed" && workflowState !== "failed" && workflowState !== "paused" && workflowState !== "stopped") throw new Error("workflowChildren.workflowState is invalid.");
105
- const children = input.children.map((row): WorkflowChildSummaryV1["children"][number] => {
152
+ const children = input.children.map((row): WorkflowChildSummary["children"][number] => {
106
153
  if (!row || typeof row !== "object" || Array.isArray(row)) throw new Error("workflowChildren child row is invalid.");
107
154
  const child = row as Record<string, unknown>;
108
- if (Object.keys(child).some((key) => !["childId", "runId", "agent", "sessionName", "model", "thinking", "state"].includes(key))) throw new Error("workflowChildren child row has unsupported fields.");
155
+ if (Object.keys(child).some((key) => !["childId", "runId", "agent", "sessionName", "model", "thinking", "state", "activity"].includes(key))) throw new Error("workflowChildren child row has unsupported fields.");
109
156
  if (typeof child.childId !== "string" || !KEY_PATTERN.test(child.childId)) throw new Error("workflowChildren childId is invalid.");
110
157
  const state = child.state;
111
158
  if (state !== "pending" && state !== "running" && state !== "completed" && state !== "failed" && state !== "paused" && state !== "stopped" && state !== "rejected" && state !== "detached") throw new Error("workflowChildren child state is invalid.");
112
159
  for (const [field, maxBytes] of [["runId", 256], ["agent", 256], ["sessionName", 256], ["model", 256], ["thinking", 32]] as const) {
113
160
  if (child[field] !== undefined && bounded(child[field], maxBytes) === undefined) throw new Error(`workflowChildren child ${field} is invalid.`);
114
161
  }
115
- return { childId: child.childId, state, ...(child.runId ? { runId: child.runId as string } : {}), ...(child.agent ? { agent: child.agent as string } : {}), ...(child.sessionName ? { sessionName: child.sessionName as string } : {}), ...(child.model ? { model: child.model as string } : {}), ...(child.thinking ? { thinking: child.thinking as string } : {}) };
162
+ const activity = parseActivity(child.activity);
163
+ if (activity && state !== "running") throw new Error("workflowChildren child activity requires running state.");
164
+ return { childId: child.childId, state, ...(activity ? { activity } : {}), ...(child.runId ? { runId: child.runId as string } : {}), ...(child.agent ? { agent: child.agent as string } : {}), ...(child.sessionName ? { sessionName: child.sessionName as string } : {}), ...(child.model ? { model: child.model as string } : {}), ...(child.thinking ? { thinking: child.thinking as string } : {}) };
116
165
  });
117
166
  if (new Set(children.map((child) => child.childId)).size !== children.length) throw new Error("workflowChildren has duplicate childId values.");
118
167
  if (typeof input.parentToolCallId !== "string") throw new Error("workflowChildren.parentToolCallId is invalid.");
@@ -1,4 +1,4 @@
1
- import type { WorkflowPreflightLaneV1, WorkflowPreflightV1 } from "../shared/types.ts";
1
+ import type { WorkflowPreflightLane, WorkflowPreflight } from "../shared/types.ts";
2
2
 
3
3
  export const WORKFLOW_PREFLIGHT_VERSION = 1 as const;
4
4
  export const WORKFLOW_PREFLIGHT_MAX_LANES = 64;
@@ -11,7 +11,7 @@ export const WORKFLOW_PREFLIGHT_MAX_WARNINGS = 16;
11
11
  const WORKFLOW_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
12
12
  const PREFLIGHT_FIELDS = new Set(["version", "coverage", "lanes"]);
13
13
  const LANE_FIELDS = new Set(["key", "mode", "decision", "claims", "expectedOutput", "independence"]);
14
- const PREFLIGHT_MODES = new Set<WorkflowPreflightLaneV1["mode"]>(["mutation", "review", "scout", "gate"]);
14
+ const PREFLIGHT_MODES = new Set<WorkflowPreflightLane["mode"]>(["mutation", "review", "scout", "gate"]);
15
15
 
16
16
  type WorkflowTraceLike = {
17
17
  operation: string;
@@ -23,7 +23,7 @@ type WorkflowTraceLike = {
23
23
 
24
24
  export interface WorkflowPreflightValidationResult {
25
25
  ok: boolean;
26
- preflight?: WorkflowPreflightV1;
26
+ preflight?: WorkflowPreflight;
27
27
  error?: string;
28
28
  }
29
29
 
@@ -60,18 +60,18 @@ function normalizeDisplayString(value: unknown, path: string): string {
60
60
  return normalized;
61
61
  }
62
62
 
63
- function normalizeLane(value: unknown, index: number): WorkflowPreflightLaneV1 {
63
+ function normalizeLane(value: unknown, index: number): WorkflowPreflightLane {
64
64
  const path = `preflight.lanes[${index}]`;
65
65
  assertPlainObject(value, path);
66
66
  assertKnownFields(value, LANE_FIELDS, path);
67
67
  const key = normalizeDisplayString(value.key, `${path}.key`);
68
68
  if (!WORKFLOW_KEY_PATTERN.test(key)) throw new Error(`${path}.key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.`);
69
- let mode: WorkflowPreflightLaneV1["mode"];
69
+ let mode: WorkflowPreflightLane["mode"];
70
70
  if (value.mode !== undefined) {
71
- if (typeof value.mode !== "string" || !PREFLIGHT_MODES.has(value.mode as WorkflowPreflightLaneV1["mode"])) {
71
+ if (typeof value.mode !== "string" || !PREFLIGHT_MODES.has(value.mode as WorkflowPreflightLane["mode"])) {
72
72
  throw new Error(`${path}.mode must be one of: mutation, review, scout, gate.`);
73
73
  }
74
- mode = value.mode as WorkflowPreflightLaneV1["mode"];
74
+ mode = value.mode as WorkflowPreflightLane["mode"];
75
75
  }
76
76
  let claims: string[] | undefined;
77
77
  if (value.claims !== undefined) {
@@ -97,7 +97,7 @@ function normalizeLane(value: unknown, index: number): WorkflowPreflightLaneV1 {
97
97
  * independent of workflowScript parsing: callers may describe dynamic fanout
98
98
  * without making the metadata a second execution graph.
99
99
  */
100
- export function normalizeWorkflowPreflight(input: unknown): WorkflowPreflightV1 | undefined {
100
+ export function normalizeWorkflowPreflight(input: unknown): WorkflowPreflight | undefined {
101
101
  if (input === undefined) return undefined;
102
102
  assertPlainObject(input, "preflight");
103
103
  assertKnownFields(input, PREFLIGHT_FIELDS, "preflight");
@@ -115,7 +115,7 @@ export function normalizeWorkflowPreflight(input: unknown): WorkflowPreflightV1
115
115
  if (seenKeys.has(lane.key)) throw new Error(`preflight.lanes contains duplicate key '${lane.key}'.`);
116
116
  seenKeys.add(lane.key);
117
117
  }
118
- const normalized = { version: WORKFLOW_PREFLIGHT_VERSION, coverage, lanes } satisfies WorkflowPreflightV1;
118
+ const normalized = { version: WORKFLOW_PREFLIGHT_VERSION, coverage, lanes } satisfies WorkflowPreflight;
119
119
  const depth = 1 + (lanes.length > 0 ? 1 : 0) + (lanes.some((lane) => lane.claims !== undefined) ? 1 : 0);
120
120
  if (depth > WORKFLOW_PREFLIGHT_MAX_DEPTH) throw new Error(`preflight exceeds the maximum nesting depth of ${WORKFLOW_PREFLIGHT_MAX_DEPTH}.`);
121
121
  const bytes = Buffer.byteLength(JSON.stringify(normalized), "utf8");
@@ -138,7 +138,7 @@ function warningLimit(messages: string[]): string[] {
138
138
  return [...messages.slice(0, WORKFLOW_PREFLIGHT_MAX_WARNINGS - 1), `Preflight advisory: ${omitted} additional mismatch warning(s) omitted.`];
139
139
  }
140
140
 
141
- function declaredKeys(preflight: WorkflowPreflightV1): Set<string> {
141
+ function declaredKeys(preflight: WorkflowPreflight): Set<string> {
142
142
  return new Set(preflight.lanes.map((lane) => lane.key));
143
143
  }
144
144
 
@@ -152,15 +152,15 @@ export function workflowKeyMatchesPreflightLane(key: string, laneKey: string, ge
152
152
 
153
153
  /** Select the most specific advisory lane without allowing declaration order to override an exact runtime key. */
154
154
  export function workflowPreflightLaneForRuntimeKey(
155
- preflight: WorkflowPreflightV1 | undefined,
155
+ preflight: WorkflowPreflight | undefined,
156
156
  key: string,
157
157
  preferredKeys: readonly (string | undefined)[] = [],
158
- ): WorkflowPreflightLaneV1 | undefined {
158
+ ): WorkflowPreflightLane | undefined {
159
159
  const lanes = preflight?.lanes;
160
160
  if (!lanes) return undefined;
161
161
  const exact = lanes.find((lane) => lane.key === key);
162
162
  if (exact) return exact;
163
- let closestRoot: WorkflowPreflightLaneV1 | undefined;
163
+ let closestRoot: WorkflowPreflightLane | undefined;
164
164
  for (const lane of lanes) {
165
165
  if (key.startsWith(`${lane.key}.`) && (!closestRoot || lane.key.length > closestRoot.key.length)) closestRoot = lane;
166
166
  }
@@ -203,7 +203,7 @@ function unlaunchedWarning(key: string): string {
203
203
 
204
204
  /** Return bounded advisory mismatch warnings; these never reject a child launch. */
205
205
  export function workflowPreflightWarnings(
206
- preflight: WorkflowPreflightV1 | undefined,
206
+ preflight: WorkflowPreflight | undefined,
207
207
  trace: readonly WorkflowTraceLike[],
208
208
  options: { settled?: boolean } = {},
209
209
  ): string[] {
@@ -218,7 +218,7 @@ export function workflowPreflightWarnings(
218
218
  }
219
219
 
220
220
  /** Attach the first undeclared-key warning to its first trace row for status/debug views. */
221
- export function annotateWorkflowPreflightTrace<T extends WorkflowTraceLike>(trace: readonly T[], preflight: WorkflowPreflightV1 | undefined): T[] {
221
+ export function annotateWorkflowPreflightTrace<T extends WorkflowTraceLike>(trace: readonly T[], preflight: WorkflowPreflight | undefined): T[] {
222
222
  if (!preflight) return [...trace];
223
223
  const declared = declaredKeys(preflight);
224
224
  const warned = new Set<string>();
@@ -235,14 +235,14 @@ function laneCell(value: string | undefined): string {
235
235
 
236
236
  const WORKFLOW_PREFLIGHT_PLAN_LABEL_LENGTH = 96;
237
237
 
238
- function planLaneLabel(lane: WorkflowPreflightLaneV1): string {
238
+ function planLaneLabel(lane: WorkflowPreflightLane): string {
239
239
  const value = lane.decision?.trim() || lane.key;
240
240
  if (value.length <= WORKFLOW_PREFLIGHT_PLAN_LABEL_LENGTH) return value;
241
241
  return `${value.slice(0, WORKFLOW_PREFLIGHT_PLAN_LABEL_LENGTH - 1).trimEnd()}…`;
242
242
  }
243
243
 
244
244
  /** Render the detailed bounded table reserved for tool output and expanded/debug views. */
245
- export function formatWorkflowPreflight(preflight: WorkflowPreflightV1 | undefined, options: { indent?: string } = {}): string {
245
+ export function formatWorkflowPreflight(preflight: WorkflowPreflight | undefined, options: { indent?: string } = {}): string {
246
246
  if (!preflight) return "";
247
247
  const indent = options.indent ?? "";
248
248
  const count = preflight.lanes.length;
@@ -263,7 +263,7 @@ export function formatWorkflowPreflight(preflight: WorkflowPreflightV1 | undefin
263
263
  return lines.join("\n");
264
264
  }
265
265
 
266
- export function formatWorkflowPreflightSummary(preflight: WorkflowPreflightV1 | undefined): string {
266
+ export function formatWorkflowPreflightSummary(preflight: WorkflowPreflight | undefined): string {
267
267
  if (!preflight) return "";
268
268
  const keys = preflight.lanes.slice(0, 4).map((lane) => lane.key).join(", ");
269
269
  const remainder = preflight.lanes.length > 4 ? `, +${preflight.lanes.length - 4}` : "";
@@ -271,7 +271,7 @@ export function formatWorkflowPreflightSummary(preflight: WorkflowPreflightV1 |
271
271
  }
272
272
 
273
273
  /** Render the operator-facing one-line plan shown in routine status views. */
274
- export function formatWorkflowPreflightPlanSummary(preflight: WorkflowPreflightV1 | undefined, options: { indent?: string } = {}): string {
274
+ export function formatWorkflowPreflightPlanSummary(preflight: WorkflowPreflight | undefined, options: { indent?: string } = {}): string {
275
275
  if (!preflight) return "";
276
276
  const indent = options.indent ?? "";
277
277
  const count = preflight.lanes.length;
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
4
- import type { AcceptanceRecoveryMetadata, ExternalCliReceiptMetadata, WorkflowReceipt, WorkflowReceiptEntry, WorkflowReceiptState, WorkflowRecoveryAction, WorkflowResourceProvenanceV1, WorkflowTerminalOutcome, WorkflowTerminalResolution } from "../shared/types.ts";
4
+ import type { AcceptanceRecoveryMetadata, ExternalCliReceiptMetadata, WorkflowReceipt, WorkflowReceiptEntry, WorkflowReceiptState, WorkflowRecoveryAction, WorkflowResourceProvenance, WorkflowTerminalOutcome, WorkflowTerminalResolution } from "../shared/types.ts";
5
5
  import type { WorkflowReceiptResumeReference, WorkflowScriptChildResult } from "./scripted-workflow.ts";
6
6
  import { parseWorkflowChildSummary } from "./workflow-child-summary.ts";
7
7
  import { HOST_STEP_MAX_COUNT, assertUniqueHostStepIds, parseHostStepNode } from "../runs/shared/host-step-status.ts";
@@ -39,7 +39,7 @@ export function buildWorkflowReceipt(input: {
39
39
  children: WorkflowScriptChildResult[];
40
40
  hostSteps?: WorkflowReceipt["hostSteps"];
41
41
  workflowChildren?: WorkflowReceipt["workflowChildren"];
42
- resource?: WorkflowResourceProvenanceV1;
42
+ resource?: WorkflowResourceProvenance;
43
43
  terminalOutcome?: WorkflowTerminalOutcome;
44
44
  createdAt?: number;
45
45
  }): WorkflowReceipt {
@@ -262,7 +262,7 @@ function parseWorkflowResolution(value: unknown, source: string): WorkflowTermin
262
262
  return value;
263
263
  }
264
264
 
265
- function parseWorkflowResource(value: unknown, source: string): WorkflowResourceProvenanceV1 | undefined {
265
+ function parseWorkflowResource(value: unknown, source: string): WorkflowResourceProvenance | undefined {
266
266
  if (value === undefined) return undefined;
267
267
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid workflow receipt '${source}': resource must be an object.`);
268
268
  const resource = value as Record<string, unknown>;
@@ -2,10 +2,10 @@ import { randomUUID } from "node:crypto";
2
2
  import { stableJsonDigest } from "../shared/launch-contract.ts";
3
3
  import {
4
4
  createWorkflowResourcePermit,
5
- type WorkflowResourceAuthority,
5
+ type WorkflowResourceHostAuthority,
6
6
  type WorkflowResourcePermit,
7
7
  } from "../shared/workflow-child-permit.ts";
8
- import type { WorkflowResourceProvenanceV1 } from "../shared/types.ts";
8
+ import type { WorkflowResourceProvenance } from "../shared/types.ts";
9
9
 
10
10
  const RESOURCE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
11
11
  const MAX_ARGS_BYTES = 16 * 1024;
@@ -14,18 +14,75 @@ const MAX_STRING_BYTES = 16 * 1024;
14
14
  export interface ResolvedWorkflowResource {
15
15
  script: string;
16
16
  permit: WorkflowResourcePermit;
17
- provenance: WorkflowResourceProvenanceV1;
17
+ provenance: WorkflowResourceProvenance;
18
18
  }
19
19
 
20
20
  export type WorkflowResourceResolution =
21
21
  | { ok: true; resource: ResolvedWorkflowResource }
22
22
  | { ok: false; error: string };
23
23
 
24
- type WorkflowResourceDefinition = {
24
+ export interface WorkflowResourceDefinition {
25
25
  name: string;
26
26
  version: number;
27
- resolve: (args: Record<string, unknown>) => { script: string; authority: WorkflowResourceAuthority } | { error: string };
28
- };
27
+ /** Trusted synchronous validation/expansion. The extension owns semantic command binding. */
28
+ resolve: (args: Readonly<Record<string, unknown>>) => { script: string; hostCommands?: readonly WorkflowResourceHostAuthority[] } | { error: string };
29
+ }
30
+
31
+ export interface WorkflowResourceRegistration {
32
+ dispose(): void;
33
+ }
34
+
35
+ export interface RegisterWorkflowResourceInput {
36
+ sessionId: string;
37
+ definition: WorkflowResourceDefinition;
38
+ }
39
+
40
+ interface WorkflowResourceRegistry {
41
+ version: 1;
42
+ bySession: Map<string, Map<string, WorkflowResourceDefinition>>;
43
+ }
44
+
45
+ function registry(): WorkflowResourceRegistry {
46
+ const key = Symbol.for("pi-subagents.workflow-resources.v1");
47
+ const globalObject = globalThis as Record<PropertyKey, unknown>;
48
+ const existing = globalObject[key];
49
+ if (existing === undefined) {
50
+ const created: WorkflowResourceRegistry = { version: 1, bySession: new Map() };
51
+ globalObject[key] = created;
52
+ return created;
53
+ }
54
+ if (!isPlainRecord(existing) || existing.version !== 1 || !(existing.bySession instanceof Map)) throw new Error("Malformed or unsupported workflow resource registry.");
55
+ return existing as unknown as WorkflowResourceRegistry;
56
+ }
57
+
58
+ /** Session ID scopes lookup, not authentication. Dispose on session_shutdown; issued permits remain valid. */
59
+ export function registerWorkflowResource(input: RegisterWorkflowResourceInput): WorkflowResourceRegistration {
60
+ if (!isPlainRecord(input) || Object.keys(input).some((key) => key !== "sessionId" && key !== "definition")) throw new Error("Workflow registration requires only sessionId and definition.");
61
+ const { sessionId, definition } = input;
62
+ if (typeof sessionId !== "string" || !sessionId || sessionId.trim() !== sessionId || sessionId.length > 256 || sessionId.includes("\0")) throw new Error("Workflow registration requires a non-empty trimmed sessionId of at most 256 characters without NUL.");
63
+ if (!isPlainRecord(definition) || Object.keys(definition).some((key) => !["name", "version", "resolve"].includes(key))) throw new Error("Workflow definition requires only name, version and resolve.");
64
+ const { name, version, resolve } = definition;
65
+ if (typeof name !== "string" || !RESOURCE_NAME_PATTERN.test(name)) throw new Error("Workflow definition requires a safe resource name.");
66
+ if (!Number.isSafeInteger(version) || version < 1) throw new Error("Workflow definition version must be a positive safe integer.");
67
+ if (typeof resolve !== "function") throw new Error("Workflow definition requires a synchronous resolve function.");
68
+ if (findWorkflowResource(name)) throw new Error(`Workflow resource '${name}' is a protected builtin.`);
69
+ const current = registry();
70
+ const bucket = current.bySession.get(sessionId) ?? new Map<string, WorkflowResourceDefinition>();
71
+ if (!(bucket instanceof Map)) throw new Error("Malformed workflow resource session registry.");
72
+ if (bucket.has(name)) throw new Error(`Workflow resource '${name}' is already registered in this session; dispose it first.`);
73
+ const snapshot = Object.freeze({ name, version, resolve });
74
+ bucket.set(name, snapshot);
75
+ current.bySession.set(sessionId, bucket);
76
+ let disposed = false;
77
+ return {
78
+ dispose() {
79
+ if (disposed) return;
80
+ disposed = true;
81
+ if (bucket.get(name) === snapshot) bucket.delete(name);
82
+ if (bucket.size === 0 && current.bySession.get(sessionId) === bucket) current.bySession.delete(sessionId);
83
+ },
84
+ };
85
+ }
29
86
 
30
87
  function isPlainRecord(value: unknown): value is Record<string, unknown> {
31
88
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
@@ -68,19 +125,19 @@ function validatePlainJson(value: unknown, path: string, depth = 0): void {
68
125
  }
69
126
  }
70
127
 
71
- function normalizeArgs(value: unknown): { args?: Record<string, unknown>; error?: string } {
128
+ function normalizeArgs(value: unknown): { args: Record<string, unknown> } | { error: string } {
72
129
  if (value === undefined) return { args: {} };
73
130
  if (!isPlainRecord(value)) return { error: "workflow args must be a plain JSON object." };
74
131
  try {
75
132
  validatePlainJson(value, "workflow args");
76
133
  if (jsonByteLength(value) > MAX_ARGS_BYTES) return { error: `workflow args exceed ${MAX_ARGS_BYTES} bytes.` };
77
- return { args: { ...value } };
134
+ return { args: JSON.parse(JSON.stringify(value)) };
78
135
  } catch (error) {
79
136
  return { error: error instanceof Error ? error.message : String(error) };
80
137
  }
81
138
  }
82
139
 
83
- function resolveRunCi(args: Record<string, unknown>): { script: string; authority: WorkflowResourceAuthority } | { error: string } {
140
+ function resolveRunCi(args: Readonly<Record<string, unknown>>): ReturnType<WorkflowResourceDefinition["resolve"]> {
84
141
  const allowed = new Set(["command", "timeoutMs"]);
85
142
  const unsupported = Object.keys(args).filter((key) => !allowed.has(key));
86
143
  if (unsupported.length > 0) return { error: `workflow 'run-ci' args contain unsupported fields: ${unsupported.join(", ")}.` };
@@ -91,18 +148,17 @@ function resolveRunCi(args: Record<string, unknown>): { script: string; authorit
91
148
  const params = { kind: "command", command, timeoutMs, role: "ci" };
92
149
  return {
93
150
  script: `return await runs.host("ci", ${JSON.stringify(params)});`,
94
- authority: { host: { keys: ["ci"], commands: [command] } },
151
+ hostCommands: [{ key: "ci", command }],
95
152
  };
96
153
  }
97
154
 
98
- function resolveReview(args: Record<string, unknown>): { script: string; authority: WorkflowResourceAuthority } | { error: string } {
155
+ function resolveReview(args: Readonly<Record<string, unknown>>): ReturnType<WorkflowResourceDefinition["resolve"]> {
99
156
  const unsupported = Object.keys(args).filter((key) => key !== "task");
100
157
  if (unsupported.length > 0) return { error: `workflow 'review' args contain unsupported fields: ${unsupported.join(", ")}.` };
101
158
  const task = args.task;
102
159
  if (typeof task !== "string" || !task.trim()) return { error: "workflow 'review' requires a non-empty string args.task." };
103
160
  return {
104
161
  script: `return (await runs.run("review", { agent: "reviewer", task: ${JSON.stringify(task.trim())} })).output;`,
105
- authority: {},
106
162
  };
107
163
  }
108
164
 
@@ -120,25 +176,44 @@ function listWorkflowResourceNames(): string[] {
120
176
  }
121
177
 
122
178
  /** Resolve only extension-owned resources so policy can distinguish them from raw scripts; caller-provided script text is never consulted. */
123
- export function resolveWorkflowResource(nameValue: unknown, argsValue?: unknown): WorkflowResourceResolution {
179
+ export function resolveWorkflowResource(nameValue: unknown, argsValue?: unknown, sessionId?: string): WorkflowResourceResolution {
180
+ try {
181
+ const result = resolveResource(nameValue, argsValue, sessionId);
182
+ return result.ok ? result : { ok: false, error: result.error.slice(0, 4096) };
183
+ } catch (error) {
184
+ return { ok: false, error: error instanceof Error ? error.message.slice(0, 4096) : "Workflow resource resolution failed." };
185
+ }
186
+ }
187
+
188
+ function resolveResource(nameValue: unknown, argsValue?: unknown, sessionId?: string): WorkflowResourceResolution {
124
189
  if (typeof nameValue !== "string" || !nameValue.trim()) return { ok: false, error: "workflow must be a non-empty resource name." };
125
190
  const name = nameValue.trim();
126
191
  if (!RESOURCE_NAME_PATTERN.test(name)) return { ok: false, error: "workflow must use a safe resource name." };
127
- const resource = findWorkflowResource(name);
192
+ const resource = findWorkflowResource(name) ?? (sessionId ? registry().bySession.get(sessionId)?.get(name) : undefined);
128
193
  if (!resource) return { ok: false, error: `Unknown workflow resource '${name}'. Available resources: ${listWorkflowResourceNames().join(", ")}.` };
129
194
  const normalizedArgs = normalizeArgs(argsValue);
130
- if (normalizedArgs.error) return { ok: false, error: normalizedArgs.error };
131
- const resolved = resource.resolve(normalizedArgs.args!);
132
- if ("error" in resolved) return { ok: false, error: resolved.error };
195
+ if ("error" in normalizedArgs) return { ok: false, error: normalizedArgs.error };
196
+ const resolved = resource.resolve(normalizedArgs.args);
197
+ if (resolved && typeof (resolved as unknown as { then?: unknown }).then === "function") {
198
+ void Promise.resolve(resolved).catch(() => {});
199
+ throw new Error("Workflow resource resolve must be synchronous.");
200
+ }
201
+ if (!isPlainRecord(resolved)) throw new Error("Workflow resource resolve must return an expansion or error object.");
202
+ if ("error" in resolved) {
203
+ if (Object.keys(resolved).some((key) => key !== "error") || typeof resolved.error !== "string" || !resolved.error.trim()) throw new Error("Workflow resource returned an invalid error.");
204
+ return { ok: false, error: resolved.error };
205
+ }
206
+ const { script, hostCommands } = resolved;
207
+ if (Object.keys(resolved).some((key) => key !== "script" && key !== "hostCommands") || typeof script !== "string" || !script.trim()) throw new Error("Workflow resource returned an invalid expansion.");
133
208
  const resourceId = randomUUID();
134
209
  const permit = createWorkflowResourcePermit({
135
210
  resourceName: resource.name,
136
211
  resourceVersion: resource.version,
137
212
  resourceId,
138
- scriptDigest: stableJsonDigest(resolved.script),
139
- authority: resolved.authority,
213
+ scriptDigest: stableJsonDigest(script),
214
+ authority: { host: hostCommands },
140
215
  });
141
- const provenance: WorkflowResourceProvenanceV1 = Object.freeze({
216
+ const provenance: WorkflowResourceProvenance = Object.freeze({
142
217
  kind: "workflow",
143
218
  name: resource.name,
144
219
  version: resource.version,
@@ -146,5 +221,5 @@ export function resolveWorkflowResource(nameValue: unknown, argsValue?: unknown)
146
221
  expansion: "resolved",
147
222
  id: resourceId,
148
223
  });
149
- return { ok: true, resource: { script: resolved.script, permit, provenance } };
224
+ return { ok: true, resource: { script, permit, provenance } };
150
225
  }
@@ -209,6 +209,8 @@ export function planWorkflowSettlement(input: {
209
209
  if (!resolution) resolution = classifyWorkflowSettlement(status);
210
210
  }
211
211
  const recovery = workflowRecoveryActions(receipt);
212
+ delete status.workflowReceiptPath;
213
+ if (receipt && input.receiptPath) status.workflowReceiptPath = input.receiptPath;
212
214
  if (receipt) {
213
215
  receipt = {
214
216
  ...receipt,
@@ -233,6 +235,7 @@ export function planWorkflowSettlement(input: {
233
235
  timestamp: now,
234
236
  };
235
237
  const terminal = status.state === "complete" || status.state === "failed" || status.state === "partial" || status.state === "stopped";
238
+ if (!status.workflowReceiptPath) delete publicResult.workflowReceipt;
236
239
  const completionEvent = terminal ? {
237
240
  type: "subagent.workflow.completed",
238
241
  state: status.state,