@marcoscale98/pi-extensible-workflows 5.19.1-fork.1 → 6.2.1-fork.1

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 (123) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +2 -2
  3. package/dist/src/agent-execution.d.ts +43 -10
  4. package/dist/src/agent-execution.js +365 -127
  5. package/dist/src/background-widget.d.ts +0 -1
  6. package/dist/src/background-widget.js +4 -9
  7. package/dist/src/decoders.d.ts +2 -3
  8. package/dist/src/decoders.js +33 -37
  9. package/dist/src/execution.d.ts +2 -0
  10. package/dist/src/execution.js +5 -5
  11. package/dist/src/host-delivery.js +4 -8
  12. package/dist/src/host-navigator.js +5 -2
  13. package/dist/src/host-phases.d.ts +1 -0
  14. package/dist/src/host-recovery.d.ts +6 -6
  15. package/dist/src/host-recovery.js +14 -22
  16. package/dist/src/host-runtime.js +56 -5
  17. package/dist/src/host-view.d.ts +1 -1
  18. package/dist/src/host-view.js +17 -15
  19. package/dist/src/host.js +62 -87
  20. package/dist/src/index.d.ts +1 -2
  21. package/dist/src/index.js +1523 -1630
  22. package/dist/src/index.js.map +3 -3
  23. package/dist/src/paths.d.ts +7 -6
  24. package/dist/src/paths.js +24 -15
  25. package/dist/src/persistence.d.ts +1 -0
  26. package/dist/src/persistence.js +1 -0
  27. package/dist/src/pi-runtime-runner.js +4 -1
  28. package/dist/src/registry.d.ts +9 -7
  29. package/dist/src/registry.js +28 -70
  30. package/dist/src/runtime/agent-runner.d.ts +2 -0
  31. package/dist/src/session-lease.d.ts +1 -1
  32. package/dist/src/session-lease.js +45 -11
  33. package/dist/src/settings.d.ts +6 -3
  34. package/dist/src/settings.js +36 -47
  35. package/dist/src/store.d.ts +3 -1
  36. package/dist/src/store.js +17 -1
  37. package/dist/src/trajectory.d.ts +0 -1
  38. package/dist/src/trajectory.js +2 -3
  39. package/dist/src/types.d.ts +100 -44
  40. package/dist/src/types.js +4 -7
  41. package/dist/src/utils.d.ts +3 -2
  42. package/dist/src/utils.js +20 -10
  43. package/dist/src/validation.d.ts +8 -2
  44. package/dist/src/validation.js +122 -86
  45. package/dist/starter/index.js +8 -9
  46. package/dist/starter/index.js.map +1 -1
  47. package/dist/starter/review-loop.js +8 -7
  48. package/dist/subagents/index.js +654 -814
  49. package/dist/subagents/index.js.map +3 -3
  50. package/dist/subagents/src/contracts.d.ts +4 -3
  51. package/dist/subagents/src/contracts.js +2 -2
  52. package/dist/subagents/src/index.d.ts +1 -1
  53. package/dist/subagents/src/index.js +1 -2
  54. package/dist/subagents/src/manager.js +49 -70
  55. package/dist/subagents/src/navigator.js +3 -14
  56. package/dist/subagents/src/view.d.ts +0 -1
  57. package/dist/subagents/src/view.js +2 -16
  58. package/dist/subagents/src/worktree.js +1 -1
  59. package/dist/trajectory/assets/index.html +8 -8
  60. package/dist/trajectory/index.js +487 -891
  61. package/dist/trajectory/index.js.map +3 -3
  62. package/dist/trajectory/src/assets/index.html +8 -8
  63. package/dist/trajectory/src/index.js +126 -86
  64. package/dist/trajectory/src/server.js +264 -25
  65. package/dist/trajectory/src/server.js.map +3 -3
  66. package/dist/trajectory/src/startup-mutex.d.ts +17 -0
  67. package/dist/trajectory/src/startup-mutex.js +172 -0
  68. package/examples/workflow-extension-template/README.md +4 -7
  69. package/examples/workflow-extension-template/extension.test.mjs +4 -8
  70. package/examples/workflow-extension-template/index.js +0 -3
  71. package/package.json +5 -5
  72. package/skills/pi-extensible-workflows/SKILL.md +10 -8
  73. package/src/agent-execution.ts +339 -127
  74. package/src/background-widget.ts +4 -10
  75. package/src/decoders.ts +31 -37
  76. package/src/execution.ts +5 -5
  77. package/src/host-delivery.ts +4 -8
  78. package/src/host-navigator.ts +4 -2
  79. package/src/host-phases.ts +1 -1
  80. package/src/host-recovery.ts +20 -26
  81. package/src/host-runtime.ts +44 -6
  82. package/src/host-view.ts +18 -11
  83. package/src/host.ts +62 -113
  84. package/src/index.ts +1 -2
  85. package/src/paths.ts +18 -12
  86. package/src/persistence.ts +1 -0
  87. package/src/pi-runtime-runner.ts +3 -1
  88. package/src/registry.ts +27 -63
  89. package/src/runtime/agent-runner.ts +2 -0
  90. package/src/session-lease.ts +34 -6
  91. package/src/settings.ts +35 -47
  92. package/src/store.ts +18 -2
  93. package/src/trajectory.ts +2 -4
  94. package/src/types.ts +71 -30
  95. package/src/utils.ts +18 -9
  96. package/src/validation.ts +102 -79
  97. package/starter/index.ts +0 -1
  98. package/starter/review-loop.ts +8 -7
  99. package/starter/roles/developer.md +0 -1
  100. package/starter/roles/oracle.md +0 -1
  101. package/starter/roles/researcher.md +0 -1
  102. package/starter/roles/reviewer.md +0 -1
  103. package/starter/roles/scout.md +0 -1
  104. package/subagents/README.md +8 -14
  105. package/subagents/src/contracts.ts +3 -4
  106. package/subagents/src/index.ts +2 -3
  107. package/subagents/src/manager.ts +45 -125
  108. package/subagents/src/navigator.ts +4 -13
  109. package/subagents/src/view.ts +4 -14
  110. package/subagents/src/worktree.ts +1 -1
  111. package/trajectory/src/assets/index.html +8 -8
  112. package/trajectory/src/index.ts +106 -82
  113. package/trajectory/src/server.ts +44 -8
  114. package/trajectory/src/startup-mutex.ts +127 -0
  115. package/dist/src/roles.d.ts +0 -70
  116. package/dist/src/roles.js +0 -326
  117. package/dist/starter/roles/developer.md +0 -6
  118. package/dist/starter/roles/oracle.md +0 -18
  119. package/dist/starter/roles/researcher.md +0 -20
  120. package/dist/starter/roles/reviewer.md +0 -19
  121. package/dist/starter/roles/scout.md +0 -18
  122. package/examples/workflow-extension-template/roles/reviewer.md +0 -4
  123. package/src/roles.ts +0 -327
package/src/types.ts CHANGED
@@ -27,29 +27,30 @@ export const WORKFLOW_BUDGET_EVENT = "workflow:budget-event";
27
27
  export const WORKFLOW_WORKTREE_CREATED_EVENT = "workflow:worktree-created";
28
28
  export const WORKFLOW_BLOCKED_EVENT = "workflow:blocked";
29
29
  export const ERROR_CODES = [
30
- "CONFIG_ERROR", "INVALID_SETTINGS", "INVALID_SYNTAX", "INVALID_METADATA", "DUPLICATE_NAME", "INVALID_SCHEMA", "UNKNOWN_MODEL", "UNKNOWN_TOOL", "UNKNOWN_AGENT_TYPE",
30
+ "CONFIG_ERROR", "INVALID_SETTINGS", "INVALID_SYNTAX", "INVALID_METADATA", "DUPLICATE_NAME", "INVALID_SCHEMA", "UNKNOWN_MODEL", "UNKNOWN_TOOL", "UNKNOWN_AGENT",
31
31
  "RUN_OWNED", "RUN_NOT_FOUND", "REGISTRY_FROZEN", "GLOBAL_COLLISION", "MISSING_WORKFLOW", "RPC_LIMIT_EXCEEDED", "SHELL_FAILED", "AGENT_TIMEOUT", "AGENT_FAILED", "AGENT_RESULT_COLLECTED", "RESULT_INVALID",
32
32
  "TOOL_FAILED", "CANCELLED", "WORKER_UNRESPONSIVE", "WORKTREE_FAILED", "RESUME_INCOMPATIBLE", "BUDGET_EXHAUSTED", "INTERNAL_ERROR",
33
33
  ] as const;
34
34
 
35
35
  export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
36
36
  export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
37
+ export const CONTEXT_FILE_SCOPES = ["global", "project", "cwd"] as const;
38
+ export type ContextFileScope = (typeof CONTEXT_FILE_SCOPES)[number];
39
+ export function isContextFileScope(value: unknown): value is ContextFileScope { return CONTEXT_FILE_SCOPES.some((scope) => scope === value); }
37
40
  export type WorkflowErrorCode = (typeof ERROR_CODES)[number];
38
41
  export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
39
42
  export type JsonSchema = { [key: string]: JsonValue };
40
43
  type WorkflowSchema = JsonSchema | TSchema;
41
- export function roleNameOf(value: unknown): string | undefined {
42
- return typeof value === "string" ? value : undefined;
43
- }
44
44
  // Extra keys are reserved for extensions and forwarded as JSON to agentOptions; core options remain typed.
45
45
  export interface AgentOptions<Schema extends TSchema = never> {
46
46
  label?: string;
47
47
  model?: string;
48
48
  tools?: string[];
49
+ /** Exact tool names removed after preparation hooks; never widens the parent ceiling. */
50
+ excludeTools?: string[];
49
51
  skills?: string[];
50
52
  extensions?: string[];
51
53
  contextFiles?: ContextFileScope[];
52
- role?: string;
53
54
  outputSchema?: JsonSchema | Schema;
54
55
  retries?: number;
55
56
  timeoutMs?: number | null;
@@ -80,7 +81,7 @@ export type WorkflowRunResumedEvent = WorkflowEventBase;
80
81
  export interface WorkflowRunStateChangedEvent extends WorkflowEventBase { previousState: RunState; state: RunState; reason?: string; errorCode?: WorkflowErrorCode }
81
82
  export interface WorkflowRunCompletedEvent extends WorkflowEventBase { resultPath: string }
82
83
  export interface WorkflowRunFailedEvent extends WorkflowEventBase { error: WorkflowErrorShape }
83
- export interface WorkflowAgentStateChangedEvent extends WorkflowEventBase { agentId: string; displayLabel: string; role?: string; structuralPath: readonly string[]; parentId?: string; parentBreadcrumb?: string; worktreeOwner?: string; previousState?: AgentState; state: AgentState; attempt: number }
84
+ export interface WorkflowAgentStateChangedEvent extends WorkflowEventBase { agentId: string; displayLabel: string; structuralPath: readonly string[]; parentId?: string; parentBreadcrumb?: string; worktreeOwner?: string; previousState?: AgentState; state: AgentState; attempt: number }
84
85
  export interface WorkflowPhaseChangedEvent extends WorkflowEventBase { previousPhase?: string; phase: string }
85
86
  export type WorkflowCheckpointState = "awaiting" | "approved" | "rejected";
86
87
  export interface WorkflowCheckpointStateChangedEvent extends WorkflowEventBase { name: string; state: WorkflowCheckpointState }
@@ -92,8 +93,8 @@ export interface WorkflowMetadata { name: string; description?: string }
92
93
  export interface HerdrExtensionSettings { enableFullyInspectableMode?: boolean }
93
94
  export interface TrajectoryExtensionSettings { port?: number }
94
95
  export interface WorkflowExtensionSettings { readonly [namespace: string]: JsonValue; readonly herdr?: Readonly<HerdrExtensionSettings>; readonly trajectory?: Readonly<TrajectoryExtensionSettings> }
95
- export type WorkflowExtensionSettingsSource = "global" | "project" | "role" | "effective";
96
- export interface WorkflowExtensionSettingsValidatorContext { source: WorkflowExtensionSettingsSource; cwd: string; projectTrusted: boolean; settingsPath?: string; role?: string }
96
+ export type WorkflowExtensionSettingsSource = "global" | "project" | "effective";
97
+ export interface WorkflowExtensionSettingsValidatorContext { source: WorkflowExtensionSettingsSource; cwd: string; projectTrusted: boolean; settingsPath?: string }
97
98
  export type WorkflowExtensionSettingsValidator = (settings: Readonly<WorkflowExtensionSettings>, context: Readonly<WorkflowExtensionSettingsValidatorContext>) => void;
98
99
  export interface WorkflowSessionStartEvent { type: "session_start"; reason: "startup" | "reload" | "new" | "resume" | "fork"; previousSessionFile?: string; settings: Readonly<WorkflowExtensionSettings> }
99
100
  export interface WorkflowRetentionSettings { olderThanDays?: number; maxTerminalRuns?: number }
@@ -101,14 +102,12 @@ export type CodemodeToolsSetting = "all" | "read-only" | "none";
101
102
  export interface WorkflowSettings { concurrency: number; backgroundWidget?: boolean; codemodeTools?: CodemodeToolsSetting; modelAliases?: Readonly<Record<string, string>>; skills?: readonly string[]; extensions?: readonly string[]; extensionSettings?: Readonly<WorkflowExtensionSettings>; tools?: readonly string[]; retention?: Readonly<WorkflowRetentionSettings>; worktreePostCreateCommand?: readonly string[] }
102
103
  export type WorkflowSettingsOverrides = Partial<Omit<WorkflowSettings, "backgroundWidget" | "codemodeTools">>;
103
104
  export interface WorkflowSettingsSources { concurrency: string; modelAliases: string; skills?: string; extensions?: string; tools?: string; extensionSettings?: string; retention?: string; worktreePostCreateCommand?: string }
104
- export interface WorkflowSettingsResolution { globalSettingsPath: string; projectSettingsPath: string; projectTrusted: boolean; global: Readonly<WorkflowSettings>; project: Readonly<WorkflowSettingsOverrides>; effective: Readonly<WorkflowSettings>; sources: Readonly<WorkflowSettingsSources> }
105
+ export interface WorkflowSettingsResolution { selectorSources?: AgentResourceSelectorSources; globalSettingsPath: string; projectSettingsPath: string; projectTrusted: boolean; global: Readonly<WorkflowSettings>; project: Readonly<WorkflowSettingsOverrides>; effective: Readonly<WorkflowSettings>; sources: Readonly<WorkflowSettingsSources> }
105
106
  export interface AgentResourceSelectors { skills?: readonly string[]; extensions?: readonly string[]; tools?: readonly string[] }
106
107
  export interface AgentResourceSelectorSet { skills: readonly string[]; extensions: readonly string[]; tools?: readonly string[] }
107
- export interface AgentResourceSelectorSources { global: AgentResourceSelectors; project: AgentResourceSelectors; role?: AgentResourceSelectors; call?: AgentResourceSelectors }
108
- export const CONTEXT_FILE_SCOPES = ["global", "project", "cwd"] as const;
109
- export type ContextFileScope = (typeof CONTEXT_FILE_SCOPES)[number];
110
- export function isContextFileScope(value: unknown): value is ContextFileScope { return CONTEXT_FILE_SCOPES.some((scope) => scope === value); }
108
+ export interface AgentResourceSelectorSources { global: AgentResourceSelectors; project: AgentResourceSelectors; call?: AgentResourceSelectors }
111
109
  export interface AgentResourcePolicy {
110
+ capabilities?: Readonly<{ tools: readonly string[]; skills: readonly string[]; extensions: readonly string[] }>;
112
111
  globalSettingsPath: string;
113
112
  projectSettingsPath: string;
114
113
  projectTrusted: boolean;
@@ -122,6 +121,8 @@ export interface AgentResourcePolicy {
122
121
  unmatchedExtensions: readonly string[];
123
122
  unmatchedTools?: readonly string[];
124
123
  selectorSources: AgentResourceSelectorSources;
124
+ /** Skills loaded by the parent session; ceiling for skills that extensions contribute through resources_discover. Absent for top-level agents. */
125
+ parentSkills?: readonly string[];
125
126
  }
126
127
  export interface AgentResourceInspection { selectors: AgentResourceSelectorSet; skills: readonly string[]; extensions: readonly string[]; tools: readonly string[]; unmatchedSkills: readonly string[]; unmatchedExtensions: readonly string[]; unmatchedTools: readonly string[]; selectorSources?: AgentResourceSelectorSources }
127
128
 
@@ -158,7 +159,6 @@ export interface AgentRecord {
158
159
  handle?: string;
159
160
  turn?: number;
160
161
  continuity?: AgentContinuity;
161
- role?: string;
162
162
  requestedModel?: string;
163
163
  model: ModelSpec;
164
164
  tools: readonly string[];
@@ -208,11 +208,10 @@ export interface RunRecord {
208
208
  events?: readonly WorkflowRunEvent[];
209
209
  delivery?: WorkflowRunDelivery;
210
210
  }
211
- export const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 5;
211
+ export const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 6;
212
212
  export type WorkflowLaunchMode = "foreground" | "background";
213
- export interface AgentDefinition { prompt?: string; description?: string; model?: string; thinking?: NonNullable<ModelSpec["thinking"]>; tools?: readonly string[]; skills?: readonly string[]; extensions?: readonly string[]; overrideSystemPrompt?: boolean; contextFiles?: readonly ContextFileScope[]; extensionSettings?: Readonly<WorkflowExtensionSettings> }
214
213
  export interface LaunchSnapshot {
215
- identityVersion?: number;
214
+ identityVersion: number;
216
215
  launchMode?: WorkflowLaunchMode;
217
216
  script: string;
218
217
  args: JsonValue;
@@ -225,14 +224,18 @@ export interface LaunchSnapshot {
225
224
  phases?: readonly string[];
226
225
  models: readonly string[];
227
226
  tools: readonly string[];
228
- agentTypes: readonly string[];
229
- roles?: Readonly<Record<string, AgentDefinition>>;
230
- projectRoles?: readonly string[];
227
+ agentConfigurations: Readonly<Record<string, PreparedAgentConfiguration>>;
231
228
  schemas: readonly JsonSchema[];
232
229
  }
233
- export interface PreflightCapabilities { models: ReadonlySet<string>; tools: ReadonlySet<string>; agentTypes: ReadonlySet<string>; modelAliases?: Readonly<Record<string, string>>; knownModels?: ReadonlySet<string>; settingsPath?: string; skipModelAvailability?: boolean }
234
- export interface PreflightResult { metadata: WorkflowMetadata; referenced: { phases: readonly string[]; models: readonly string[]; tools: readonly string[]; agentTypes: readonly string[] }; schemas: readonly JsonSchema[]; dynamicAgentRoles: boolean }
235
- export interface WorkflowOrchestrationContext { agent: { <Schema extends TSchema>(prompt: string, options: Readonly<AgentOptions<Schema> & { outputSchema: Schema }>): Promise<Static<Schema>>; (prompt: string, options?: Readonly<AgentOptions>): Promise<JsonValue> }; shell: (command: string, options?: ShellOptions) => Promise<ShellResult>; prompt: (template: string, values: Readonly<Record<string, JsonValue>>) => string; parallel: <Tasks extends ParallelTasks>(operationName: string, tasks: Tasks) => Promise<ParallelResult<Tasks>>; pipeline: <Items extends PipelineItems, Output extends JsonValue>(operationName: string, items: Items, stages: PipelineStages<Items[keyof Items], Output>) => Promise<PipelineResult<Items, Output>>; withWorktree: <Result extends JsonValue>(name: string, callback: WorkflowWorktreeCallback<Result>) => Promise<Result>; checkpoint: (input: CheckpointInput) => Promise<boolean>; phase: (name: string) => void; log: (message: string) => void }
230
+ export interface PreflightCapabilities { models: ReadonlySet<string>; tools: ReadonlySet<string>; modelAliases?: Readonly<Record<string, string>>; knownModels?: ReadonlySet<string>; settingsPath?: string; skipModelAvailability?: boolean }
231
+ export interface PreflightResult { metadata: WorkflowMetadata; referenced: { phases: readonly string[]; models: readonly string[]; tools: readonly string[]; }; schemas: readonly JsonSchema[] }
232
+ export interface AgentHandleSendOptions<Schema extends TSchema = never> { outputSchema?: JsonSchema | Schema; timeoutMs?: number | null }
233
+ /** A persistent agent: each send continues the transcript of the previous completed turn. Sends must be awaited one at a time, in the scope that created the handle. */
234
+ export interface AgentHandle {
235
+ readonly name: string;
236
+ send: { <Schema extends TSchema>(prompt: string, options: Readonly<AgentHandleSendOptions<Schema> & { outputSchema: Schema }>): Promise<Static<Schema>>; (prompt: string, options?: Readonly<AgentHandleSendOptions>): Promise<JsonValue> };
237
+ }
238
+ export interface WorkflowOrchestrationContext { agent: { <Schema extends TSchema>(prompt: string, options: Readonly<AgentOptions<Schema> & { outputSchema: Schema }>): Promise<Static<Schema>>; (prompt: string, options?: Readonly<AgentOptions>): Promise<JsonValue>; create: (options: Readonly<AgentOptions & { name: string }>) => AgentHandle }; shell: (command: string, options?: ShellOptions) => Promise<ShellResult>; prompt: (template: string, values: Readonly<Record<string, JsonValue>>) => string; parallel: <Tasks extends ParallelTasks>(operationName: string, tasks: Tasks) => Promise<ParallelResult<Tasks>>; pipeline: <Items extends PipelineItems, Output extends JsonValue>(operationName: string, items: Items, stages: PipelineStages<Items[keyof Items], Output>) => Promise<PipelineResult<Items, Output>>; withWorktree: <Result extends JsonValue>(name: string, callback: WorkflowWorktreeCallback<Result>) => Promise<Result>; checkpoint: (input: CheckpointInput) => Promise<boolean>; phase: (name: string) => void; log: (message: string) => void }
236
239
  export interface WorkflowRunContext { cwd: string; sessionId: string; runId: string; workflow: Readonly<WorkflowMetadata>; args: JsonValue; signal: AbortSignal }
237
240
  export interface WorkflowFunctionContext extends WorkflowOrchestrationContext { run: Readonly<WorkflowRunContext>; settings: Readonly<WorkflowExtensionSettings>; invoke: (name: string, input: Readonly<Record<string, JsonValue>>, label?: string) => Promise<JsonValue> }
238
241
  export type WorkflowWorktreeCallback<Result extends JsonValue = JsonValue> = (reference: Readonly<WorkflowWorktreeReference>) => Result | Promise<Result>;
@@ -267,6 +270,8 @@ export interface WorkflowAgentSession {
267
270
  getState(): Readonly<WorkflowAgentSessionState>;
268
271
  getSessionStats(): WorkflowAgentSessionStats;
269
272
  getLastAssistant(): WorkflowAgentMessage | undefined;
273
+ /** Set by a transport that already delivered `PreparedAgentSession.turnControl` itself; otherwise the runner steers it before the first prompt. */
274
+ readonly turnControlDelivered?: boolean;
270
275
  subscribe(listener: (event: WorkflowAgentSessionEvent) => void): () => void;
271
276
  subscribeAsync?(listener: (event: WorkflowAgentSessionEvent) => void | Promise<void>): () => void;
272
277
  prompt(text: string): Promise<WorkflowAgentTurnResult>;
@@ -280,7 +285,8 @@ export interface WorkflowAgentSession {
280
285
  type SessionTools = NonNullable<CreateAgentSessionOptions["tools"]>;
281
286
  type SessionCustomTools = NonNullable<CreateAgentSessionOptions["customTools"]>;
282
287
  export interface SessionInput {
283
- cwd: string;
288
+ /** Read-only: setup hooks cannot move an agent, because Pi derives trust, settings and `.pi` resources from cwd. */
289
+ readonly cwd: string;
284
290
  model: ModelSpec;
285
291
  tools: SessionTools;
286
292
  sessionLabel: string;
@@ -309,6 +315,8 @@ export interface PreparedAgentSession {
309
315
  readonly sessionLabel: string;
310
316
  readonly sessionPath?: string;
311
317
  readonly initialPrompt?: string;
318
+ /** Dynamic per-attempt control text (retry state, budget). The runner steers it before the first prompt unless the session reports `turnControlDelivered`. */
319
+ readonly turnControl?: string;
312
320
  readonly agentDir?: string;
313
321
  readonly customTools?: readonly ToolDefinition[];
314
322
  readonly resultTool?: ToolDefinition;
@@ -330,17 +338,50 @@ export interface AgentTransport { readonly id: string; createSession(prepared: R
330
338
  export interface AgentSetup { prompt: string; options: AgentOptions; sessionInput: SessionInput; prepared: Readonly<PreparedAgentSession>; transport: AgentTransport }
331
339
  export interface AgentSetupContext { readonly run: Readonly<WorkflowRunContext>; readonly identity: Readonly<AgentIdentity>; readonly attempt: number; readonly signal: AbortSignal; readonly tuiIndex?: number; readonly tuiLabel?: string; readonly mode?: AgentInspectionMode; readonly settings: Readonly<WorkflowExtensionSettings> }
332
340
 
341
+ export interface AgentPreparation {
342
+ label?: string;
343
+ model?: string;
344
+ tools: string[];
345
+ skills: string[];
346
+ extensions: string[];
347
+ contextFiles?: ContextFileScope[];
348
+ systemPrompt?: string;
349
+ systemPromptAppend: string;
350
+ settings: WorkflowExtensionSettings;
351
+ }
352
+ export interface AgentPreparationContext {
353
+ readonly options: Readonly<AgentOptions>;
354
+ readonly cwd: string;
355
+ /** Launch project root that owns project configuration; differs from `cwd` inside worktree scopes. Optional for older hosts. */
356
+ readonly projectCwd?: string;
357
+ readonly agentDir: string;
358
+ readonly projectTrusted: boolean;
359
+ /** `dynamicModelAliasNames` lists the `modelAliases` entries resolved by extensions rather than read from static settings. */
360
+ readonly defaults: Readonly<{ model: ModelSpec; modelAliases: Readonly<Record<string, string>>; dynamicModelAliasNames?: readonly string[]; selectorSources: AgentResourceSelectorSources; settings: Readonly<WorkflowExtensionSettings> }>;
361
+ readonly capabilities: Readonly<{ tools: readonly string[]; skills: readonly string[]; extensions: readonly string[] }>;
362
+ readonly knownModels: ReadonlySet<string>;
363
+ readonly availableModels: ReadonlySet<string>;
364
+ readonly signal: AbortSignal;
365
+ readonly mode: AgentInspectionMode;
366
+ }
367
+ export interface AgentPreparationHook { priority?: number; optionsSchema?: JsonSchema; prepare: (configuration: AgentPreparation, context: Readonly<AgentPreparationContext>) => void | Promise<void> }
368
+ /** `source` is the registering extension's module provenance, when it declared one. */
369
+ export interface RegisteredAgentPreparationHook { name: string; priority: number; optionsSchema?: JsonSchema; source?: string; prepare: AgentPreparationHook["prepare"] }
370
+ /**
371
+ * `excludeTools` holds the exact tool names the agent and its ancestors excluded, frozen with the configuration so setup and
372
+ * children apply them even when re-evaluated options change. It is absent only in configurations prepared before it existed, which cannot resume.
373
+ */
374
+ export interface PreparedAgentConfiguration extends Omit<AgentPreparation, "model"> { model: ModelSpec; projectTrusted: boolean; excludeTools?: string[] }
333
375
  export interface AgentSetupHook { priority?: number; setup: (agent: AgentSetup, context: Readonly<AgentSetupContext>) => void | Promise<void> }
334
376
  export interface RegisteredAgentSetupHook { name: string; priority: number; setup: AgentSetupHook["setup"] }
335
377
  export interface WorkflowExtensionMetadata { version: string; headline: string }
336
378
  export interface WorkflowFunctionSource { readonly module: string; readonly export: string; readonly dependencies: readonly string[] }
337
- export interface WorkflowRoleDirectoryRegistration { path: string; extension: WorkflowExtensionMetadata; readonly builtin?: true }
338
379
  export interface AgentAttemptActionUi { notify(message: string, level?: "info" | "warning" | "error"): void; confirm(title: string, message: string): Promise<boolean>; select(title: string, options: readonly string[]): Promise<string | undefined>; input(title: string, placeholder?: string): Promise<string | undefined>; setWorkingMessage?(message?: string): void }
339
380
  export interface StandaloneAgentRecord { readonly id: string; readonly name: string; readonly label?: string; readonly state: "running" | "completed" | "failed" | "stopped"; readonly structuralPath?: readonly string[] }
340
381
  export interface StandaloneAgentAttemptActionContext { readonly agent: Readonly<StandaloneAgentRecord>; readonly attempt: Readonly<AgentAttemptSummary>; readonly session?: WorkflowAgentSessionReference; readonly liveSession?: WorkflowAgentSession; readonly prepared?: Readonly<PreparedAgentSession>; readonly handoff?: LiveSessionHandoff; readonly signal: AbortSignal; readonly ui: Readonly<AgentAttemptActionUi> }
341
382
  export interface AgentAttemptActionContext { readonly run: Readonly<RunRecord>; readonly agent: Readonly<AgentRecord>; readonly attempt: Readonly<AgentAttemptSummary>; readonly session?: WorkflowAgentSessionReference; readonly liveSession?: WorkflowAgentSession; readonly prepared?: Readonly<PreparedAgentSession>; readonly handoff?: LiveSessionHandoff; readonly signal: AbortSignal; readonly ui: Readonly<AgentAttemptActionUi> }
342
383
  export interface AgentAttemptAction { readonly label: string; visible(context: Readonly<AgentAttemptActionContext>): boolean; run(context: Readonly<AgentAttemptActionContext>): void | Promise<void>; visibleStandalone?(context: Readonly<StandaloneAgentAttemptActionContext>): boolean; runStandalone?(context: Readonly<StandaloneAgentAttemptActionContext>): void | Promise<void> }
343
- export interface WorkflowExtension extends WorkflowExtensionMetadata { description?: string; source?: string; dependencies?: readonly string[]; validateSettings?: WorkflowExtensionSettingsValidator; functions?: Readonly<Record<string, WorkflowFunction>>; modelAliases?: Readonly<Record<string, WorkflowModelAlias>>; agentSetupHooks?: Readonly<Record<string, AgentSetupHook>>; agentAttemptActions?: Readonly<Record<string, AgentAttemptAction>>; roleDirectories?: readonly (string | URL)[] }
384
+ export interface WorkflowExtension extends WorkflowExtensionMetadata { description?: string; source?: string; dependencies?: readonly string[]; validateSettings?: WorkflowExtensionSettingsValidator; functions?: Readonly<Record<string, WorkflowFunction>>; modelAliases?: Readonly<Record<string, WorkflowModelAlias>>; agentPreparationHooks?: Readonly<Record<string, AgentPreparationHook>>; agentSetupHooks?: Readonly<Record<string, AgentSetupHook>>; agentAttemptActions?: Readonly<Record<string, AgentAttemptAction>> }
344
385
  export interface WorkflowJournal { get(path: string): JsonValue | undefined; put(path: string, value: JsonValue): void }
345
386
  // The brand keeps instanceof working across the bundled extension entries, which each inline their own copy of this class.
346
387
  const WORKFLOW_ERROR_BRAND = Symbol.for("pi-extensible-workflows.workflow-error");
@@ -353,8 +394,8 @@ export class WorkflowError extends Error {
353
394
  }
354
395
  }
355
396
  Object.defineProperty(WorkflowError, WORKFLOW_ERROR_BRAND, { value: true });
356
- export interface WorkflowFailureAgent { id: string; label?: string; role?: string; structuralPath: readonly string[]; attempt: number; transport?: string; session?: WorkflowAgentSessionReference }
357
- export interface WorkflowSiblingAgent { id: string; label?: string; role?: string; structuralPath: readonly string[] }
397
+ export interface WorkflowFailureAgent { id: string; label?: string; structuralPath: readonly string[]; attempt: number; transport?: string; session?: WorkflowAgentSessionReference }
398
+ export interface WorkflowSiblingAgent { id: string; label?: string; structuralPath: readonly string[] }
358
399
  export interface WorkflowFailureDiagnostics { runId: string; workflowName: string; state: RunState; failedAt: string | null; error: WorkflowErrorShape; failedAgent?: WorkflowFailureAgent; completedSiblingAgents?: readonly WorkflowSiblingAgent[]; completedSiblingPaths: readonly (readonly string[])[]; retry?: { sourceRunId: string; action: string; completedPaths: readonly string[]; incompletePaths: readonly string[]; namedWorktrees: readonly string[]; warning: string }; artifacts: { runDirectory: string; statePath: string; journalPath: string } }
359
400
  export interface CheckpointInput { name: string; prompt: string; context: JsonValue }
360
401
  export interface FunctionIdentity { path: string; structuralPath: readonly string[]; occurrence: number; worktreeOwner?: string }
@@ -367,7 +408,7 @@ export interface WorkflowBridge { agent?: (prompt: string, options: Readonly<Rec
367
408
  export interface WorkflowExecution { result: Promise<JsonValue>; cancel: () => void }
368
409
  export interface StaticWorkflowScope { kind: "parallel" | "pipeline"; name: string | null; key: string | null }
369
410
  export type StaticWorkflowExecution = "parallel" | "sequential";
370
- export interface StaticWorkflowCall { kind: WorkflowCallKind; start: number; end: number; name: string | null; prompt: string | null; model: string | null; label?: string | null; role: string | null; retries?: number | null; outputSchema?: JsonSchema | null; options?: Readonly<Record<string, JsonValue>> | null; optionKeys?: readonly string[]; execution?: StaticWorkflowExecution; structure?: readonly StaticWorkflowScope[] }
411
+ export interface StaticWorkflowCall { kind: WorkflowCallKind; start: number; end: number; name: string | null; prompt: string | null; model: string | null; label?: string | null; retries?: number | null; outputSchema?: JsonSchema | null; options?: Readonly<Record<string, JsonValue>> | null; optionKeys?: readonly string[]; execution?: StaticWorkflowExecution; structure?: readonly StaticWorkflowScope[] }
371
412
  export interface WorkflowCatalogFunction { name: string; version: string; headline: string; description: string; input: JsonSchema; output: JsonSchema }
372
413
  export interface WorkflowCatalogModelAlias { name: string; kind: "static" | "dynamic"; provenance: string; version?: string; headline?: string }
373
414
  export interface WorkflowCatalogSettings { concurrency: number; backgroundWidget: boolean; modelAliases: Readonly<Record<string, string>>; skills: readonly string[]; extensions: readonly string[]; tools: readonly string[]; extensionSettings?: Readonly<WorkflowExtensionSettings>; worktreePostCreateCommand?: readonly string[]; globalSettingsPath: string; projectSettingsPath: string; projectTrusted: boolean; sources: WorkflowSettingsSources }
@@ -378,4 +419,4 @@ export interface WorkflowCatalogIndex { functions: readonly WorkflowCatalogIndex
378
419
  export interface WorkflowCatalogError { error: { code: "NOT_FOUND"; name: string; message: string } }
379
420
  export interface WorkflowValidationParameters { name: string; description?: string; script?: string; scriptPath?: string; args?: unknown }
380
421
  export interface WorkflowValidationContext { cwd: string; projectTrusted: boolean; availableModels: ReadonlySet<string>; rootTools: ReadonlySet<string>; modelAliases?: Readonly<Record<string, string>>; knownModels?: ReadonlySet<string>; settingsPath?: string; agentDir?: string; extensionSettings?: Readonly<WorkflowExtensionSettings> }
381
- export interface ValidatedWorkflowLaunch { script: string; checked: PreflightResult; agentDefinitions: Readonly<Record<string, AgentDefinition>>; projectAgentDefinitions: Readonly<Record<string, AgentDefinition>>; roleNames: readonly string[] }
422
+ export interface ValidatedWorkflowLaunch { script: string; checked: PreflightResult }
package/src/utils.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
- import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, THINKING_LEVELS, WorkflowError, type JsonSchema, type JsonValue, type ModelSpec, type ThinkingLevel, type WorkflowErrorCode, type WorkflowExtensionSettings } from "./types.js";
2
+ import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WorkflowError, type JsonSchema, type JsonValue, type ModelSpec, type WorkflowErrorCode, type WorkflowExtensionSettings } from "./types.js";
3
3
  import { Minimatch } from "minimatch";
4
+ import { THINKING_LEVELS, type ThinkingLevel } from "./types.js";
4
5
  export class SerialLane {
5
6
  #tail: Promise<void> = Promise.resolve();
6
7
  run<T>(task: () => Promise<T>): Promise<T> {
@@ -92,11 +93,19 @@ export const WORKFLOW_EXTENSION_NAMESPACE = /^[A-Za-z][A-Za-z0-9_-]*$/;
92
93
  export function validWorkflowExtensionNamespace(value: string): boolean { return WORKFLOW_EXTENSION_NAMESPACE.test(value) && value !== "__proto__" && value !== "constructor" && value !== "prototype"; }
93
94
  export function parseThinking(value: unknown): ModelSpec["thinking"] | undefined { return isThinkingLevel(value) ? value : undefined; }
94
95
  export function parseModelReference(value: string): ModelSpec {
95
- const match = /^([^/:\s]+)\/([^:\s]+)(?::([^:\s]+))?$/.exec(value);
96
+ const match = /^([^/:\s]+)\/([^\s]+)$/.exec(value);
96
97
  if (!match?.[1] || !match[2]) fail("UNKNOWN_MODEL", `Invalid model spec: ${value}`);
97
- const thinking = match[3];
98
- if (thinking !== undefined && !isThinkingLevel(thinking)) fail("UNKNOWN_MODEL", `Invalid thinking level: ${thinking}`);
99
- return { provider: match[1], model: match[2], ...(thinking !== undefined ? { thinking } : {}) };
98
+ // The model id is everything after the first slash, so namespaced ids such as
99
+ // `deepseek/deepseek-v4.1-flash:nitro` survive intact. Only a trailing segment
100
+ // that names a thinking level is one.
101
+ const reference = match[2];
102
+ const separator = reference.lastIndexOf(":");
103
+ const suffix = separator === -1 ? undefined : reference.slice(separator + 1);
104
+ if (suffix === "") fail("UNKNOWN_MODEL", `Invalid model spec: ${value}`);
105
+ const thinking = suffix !== undefined && isThinkingLevel(suffix) ? suffix : undefined;
106
+ const model = thinking === undefined ? reference : reference.slice(0, separator);
107
+ if (model === "") fail("UNKNOWN_MODEL", `Invalid model spec: ${value}`);
108
+ return { provider: match[1], model, ...(thinking === undefined ? {} : { thinking }) };
100
109
  }
101
110
  export function assertModelThinking(value: string, path = "model"): void {
102
111
  if (value.includes("/") && parseModelReference(value).thinking === undefined) fail("INVALID_METADATA", `${path} must be provider/model:thinking`);
@@ -163,6 +172,10 @@ export function resolveModelReference(value: string, aliases: Readonly<Record<st
163
172
  };
164
173
  return resolveReference(value, []);
165
174
  }
175
+ export function modelCapability(value: string | ModelSpec, aliases?: Readonly<Record<string, string>>, knownModels?: ReadonlySet<string>, settingsPath?: string): string {
176
+ const parsed = typeof value === "string" ? resolveModelReference(value, aliases, knownModels, settingsPath) : value;
177
+ return `${parsed.provider}/${parsed.model}`;
178
+ }
166
179
  type ToolSource = { getActiveTools(): string[]; getAllTools?(): readonly { name: string; exposure?: string }[] };
167
180
  /**
168
181
  * Tools a session can reach, and so the ceiling for its agents: the declared tools plus the ones
@@ -181,10 +194,6 @@ export function physicalModel(spec: ModelSpec, aliases: Readonly<Record<string,
181
194
  const target = resolveModelReference(spec.model, aliases, knownModels, settingsPath);
182
195
  return spec.thinking === undefined ? target : { ...target, thinking: spec.thinking };
183
196
  }
184
- export function modelCapability(value: string | ModelSpec, aliases?: Readonly<Record<string, string>>, knownModels?: ReadonlySet<string>, settingsPath?: string): string {
185
- const parsed = typeof value === "string" ? resolveModelReference(value, aliases, knownModels, settingsPath) : value;
186
- return `${parsed.provider}/${parsed.model}`;
187
- }
188
197
  export function aliasDrift(previous: Readonly<Record<string, string>>, current: Readonly<Record<string, string>>): string[] {
189
198
  return [...new Set([...Object.keys(previous), ...Object.keys(current)])].sort().flatMap((name) => previous[name] === current[name] ? [] : [`${name}: ${previous[name] ?? "(missing)"} -> ${current[name] ?? "(missing)"}`]);
190
199
  }
package/src/validation.ts CHANGED
@@ -2,14 +2,11 @@ import { readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import * as acorn from "acorn";
4
4
  import { Script } from "node:vm";
5
- import type { AgentDefinition, CheckpointInput, JsonSchema, JsonValue, PreflightCapabilities, PreflightResult, ShellOptions, StaticWorkflowCall, StaticWorkflowExecution, StaticWorkflowScope, ValidatedWorkflowLaunch, WorkflowCallKind, WorkflowErrorCode, WorkflowMetadata, WorkflowValidationContext, WorkflowValidationParameters } from "./types.js";
5
+ import type { CheckpointInput, JsonSchema, JsonValue, PreflightCapabilities, PreflightResult, ShellOptions, StaticWorkflowCall, StaticWorkflowExecution, StaticWorkflowScope, ValidatedWorkflowLaunch, WorkflowCallKind, WorkflowErrorCode, WorkflowMetadata, WorkflowValidationContext, WorkflowValidationParameters } from "./types.js";
6
6
  import type { WorkflowRegistryApi } from "./registry.js";
7
- import { assertModelThinking, validateSchema, deepFreeze, errorText, fail, jsonObject, jsonValue, mergeWorkflowExtensionSettings, modelAliasName, modelCapability, object, positiveInteger, resolveModelReference, resourcePatternHasMagic, unknownModel } from "./utils.js";
7
+ import { assertModelThinking, validateSchema, deepFreeze, errorText, fail, jsonObject, jsonValue, object, positiveInteger, resourcePatternHasMagic } from "./utils.js";
8
8
  import { WORKFLOW_CALL_KINDS } from "./types.js";
9
9
  export { validateSchema } from "./utils.js";
10
- import { loadAgentDefinitions, loadProjectAgentDefinitions } from "./roles.js";
11
- export { loadAgentDefinitions, loadProjectAgentDefinitions, parseRoleMarkdown, workflowRoleDirectories } from "./roles.js";
12
- export type { WorkflowRoleDirectoryInput } from "./roles.js";
13
10
 
14
11
  import { validateContextFileScopes, validateSelectorList } from "./settings.js";
15
12
  export { DEFAULT_SETTINGS, loadCodemodeToolsSetting, loadSettings, loadSettingsOverrides, resolveAgentResourcePolicy, resolveWorkflowSettings, saveModelAliases, validateContextFileScopes, validateModelAliasAvailability, validateSelectorList, validateWorkflowExtensionSettings, workflowProjectSettingsPath, workflowSettingsPath, workflowToolExposure } from "./settings.js";
@@ -21,22 +18,6 @@ export function validateCheckpoint(value: unknown): CheckpointInput {
21
18
  return { name: value.name, prompt: value.prompt, context: value.context };
22
19
  }
23
20
 
24
- function validateRolePolicies(definitions: Readonly<Record<string, AgentDefinition>>, roles: readonly string[], availableModels: ReadonlySet<string>, aliases: Readonly<Record<string, string>> = {}, knownModels = availableModels, settingsPath?: string): void {
25
- for (const role of roles) {
26
- const definition = definitions[role];
27
- if (!definition) continue;
28
- if (definition.model !== undefined) {
29
- const resolved = modelCapability(definition.model, aliases, knownModels, settingsPath);
30
- if (!availableModels.has(resolved)) {
31
- if (modelAliasName(definition.model, aliases)) unknownModel(definition.model, resolved, settingsPath);
32
- fail("UNKNOWN_MODEL", `Unknown model for role ${role}: ${resolved}`);
33
- }
34
- }
35
- // Role tools absent from the session are tolerated at launch: the runtime resolve emits a
36
- // warning and the agent runs without them. Doctor still reports unmatched patterns.
37
- }
38
- }
39
-
40
21
  function validateWorkflowMetadata(value: unknown): WorkflowMetadata {
41
22
  if (!object(value) || typeof value.name !== "string" || value.name.trim() === "") fail("INVALID_METADATA", "Workflow metadata requires a non-empty name");
42
23
  if (value.description !== undefined && (typeof value.description !== "string" || value.description.trim() === "")) fail("INVALID_METADATA", "Workflow description must be a non-empty string when provided");
@@ -309,8 +290,9 @@ export function workflowPrompt(template: string, values: Readonly<Record<string,
309
290
  });
310
291
  }
311
292
 
312
- const AGENT_OPTION_KEYS = new Set(["label", "model", "tools", "skills", "extensions", "contextFiles", "role", "outputSchema", "retries", "timeoutMs"]);
313
- function validateAgentOption(key: string, value: unknown, aliases?: Readonly<Record<string, string>>, knownModels?: ReadonlySet<string>, settingsPath?: string): void {
293
+ const AGENT_OPTION_KEYS = new Set(["label", "model", "tools", "excludeTools", "skills", "extensions", "contextFiles", "outputSchema", "retries", "timeoutMs"]);
294
+ export function isCoreAgentOption(key: string): boolean { return AGENT_OPTION_KEYS.has(key); }
295
+ function validateAgentOption(key: string, value: unknown): void {
314
296
  switch (key) {
315
297
  case "label":
316
298
  if (typeof value !== "string" || !value.trim()) fail("INVALID_METADATA", "agent label must be a non-empty string");
@@ -318,19 +300,19 @@ function validateAgentOption(key: string, value: unknown, aliases?: Readonly<Rec
318
300
  case "model":
319
301
  if (typeof value !== "string" || !value.trim()) fail("INVALID_METADATA", "agent model must be a non-empty string");
320
302
  assertModelThinking(value, "agent model");
321
- if (aliases !== undefined) resolveModelReference(value, aliases, knownModels, settingsPath);
322
303
  break;
323
304
  case "tools":
324
305
  case "skills":
325
306
  case "extensions":
326
307
  validateSelectorList(value, "agent options", key, "INVALID_METADATA", key !== "extensions");
327
308
  break;
309
+ case "excludeTools":
310
+ if (!Array.isArray(value) || value.some((tool) => typeof tool !== "string" || !tool.trim() || tool !== tool.trim() || tool.startsWith("!") || resourcePatternHasMagic(tool))) fail("INVALID_METADATA", "agent excludeTools must be an array of exact tool names");
311
+ if (value.includes("workflow_result")) fail("INVALID_METADATA", "agent excludeTools cannot remove workflow_result");
312
+ break;
328
313
  case "contextFiles":
329
314
  validateContextFileScopes(value, "agent options");
330
315
  break;
331
- case "role":
332
- if (typeof value !== "string" || !value.trim()) fail("INVALID_METADATA", "agent role must be a non-empty string");
333
- break;
334
316
  case "outputSchema":
335
317
  validateSchema(value, "agent outputSchema");
336
318
  break;
@@ -429,36 +411,101 @@ export function inspectWorkflowScript(script: string): StaticWorkflowCall[] {
429
411
  if (value.known && jsonValue(value.value)) knownOptionEntries.push([key, value.value]);
430
412
  }
431
413
  const knownOptions: Record<string, JsonValue> = Object.fromEntries(knownOptionEntries);
432
- const base = { ...placement, kind, start: call.start, end: call.end, name: null, prompt: staticString(first), model: staticString(propertyNode(options, "model")), label: staticString(propertyNode(options, "label")), role: staticString(propertyNode(options, "role")) };
414
+ const base = { ...placement, kind, start: call.start, end: call.end, name: null, prompt: staticString(first), model: staticString(propertyNode(options, "model")), label: staticString(propertyNode(options, "label")) };
433
415
  return { ...base, ...(retries.known && typeof retries.value === "number" ? { retries: retries.value } : {}), ...(staticOutputSchema === undefined ? {} : { outputSchema: staticOutputSchema }), ...(optionKeys.length ? { options: knownOptions, optionKeys } : {}) };
434
416
  }
435
- if (kind === "checkpoint") return { ...placement, kind, start: call.start, end: call.end, name: staticString(propertyNode(first, "name")), prompt: staticString(propertyNode(first, "prompt")), model: null, role: null };
436
- if (kind === "shell") return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null, role: null };
437
- return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null, role: null };
417
+ if (kind === "checkpoint") return { ...placement, kind, start: call.start, end: call.end, name: staticString(propertyNode(first, "name")), prompt: staticString(propertyNode(first, "prompt")), model: null };
418
+ if (kind === "shell") return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null };
419
+ return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null };
438
420
  });
439
421
  }
440
422
 
441
- function validateStaticAgentOptions(node: acorn.AnyNode | undefined, aliases: Readonly<Record<string, string>> = {}, knownModels?: ReadonlySet<string>, settingsPath?: string): void {
442
- if (node?.type !== "ObjectExpression") return;
443
- for (const key of AGENT_OPTION_KEYS) {
444
- const value = staticValue(propertyNode(node, key));
445
- if (value.known) validateAgentOption(key, value.value, aliases, knownModels, settingsPath);
423
+ /** Every option is a preparation input, because any hook may read it; one dynamic value leaves the whole call to runtime. */
424
+ function staticPreparationOptions(node: acorn.AnyNode | undefined, omitted?: string): Record<string, JsonValue> | undefined {
425
+ if (node?.type !== "ObjectExpression") return undefined;
426
+ const options = new Map<string, JsonValue>();
427
+ for (const property of node.properties) {
428
+ if (property.type === "SpreadElement" || property.computed) return undefined;
429
+ const key = propertyKeyName(property);
430
+ if (key === undefined) return undefined;
431
+ if (key === omitted) continue;
432
+ const value = staticValue(property.value);
433
+ // NOTE: an output schema only shapes the result, so a computed one (a const declared elsewhere) does not hide the call.
434
+ if (key === "outputSchema" && !value.known) continue;
435
+ if (!value.known || !jsonValue(value.value)) return undefined;
436
+ options.set(key, value.value);
446
437
  }
438
+ return Object.fromEntries(options);
447
439
  }
448
- function hasDynamicAgentRole(node: acorn.AnyNode | undefined): boolean {
449
- if (!node) return false;
450
- if (node.type !== "ObjectExpression") return true;
451
- for (let index = node.properties.length - 1; index >= 0; index -= 1) {
452
- const property = node.properties[index];
453
- if (!property || property.type === "SpreadElement" || property.computed) return true;
454
- const key = propertyKeyName(property);
455
- if (key === "role") {
456
- const roleValue = staticValue(property.value);
457
- if (roleValue.known && typeof roleValue.value === "string") return false;
458
- return true;
440
+ /**
441
+ * A handle prepares its first turn with the create options merged with that send's options, so create options are complete
442
+ * only when every use of the handle provably passes no send options: `agent.create(...).send(prompt)` directly, or a
443
+ * `const` handle whose every reference is `handle.send(prompt)`. Computed access, destructuring, aliases or any other
444
+ * use leave the handle to runtime.
445
+ */
446
+ function handlesWithCompleteOptions(program: acorn.Program): acorn.CallExpression[] {
447
+ const parents = new Map<acorn.AnyNode, acorn.AnyNode>();
448
+ const references = new Map<string, acorn.AnyNode[]>();
449
+ const visit = (node: acorn.AnyNode): void => {
450
+ if (node.type === "Identifier") references.set(node.name, [...(references.get(node.name) ?? []), node]);
451
+ for (const child of astChildren(node)) { parents.set(child, node); visit(child); }
452
+ };
453
+ visit(program);
454
+ const plainSend = (member: acorn.AnyNode | undefined): boolean => {
455
+ const call = member ? parents.get(member) : undefined;
456
+ return member?.type === "MemberExpression" && !member.computed && member.property.type === "Identifier" && member.property.name === "send"
457
+ && call?.type === "CallExpression" && call.callee === member && call.arguments.length <= 1 && call.arguments.every((argument) => argument.type !== "SpreadElement");
458
+ };
459
+ return agentCreateCalls(program).filter((create) => {
460
+ const parent = parents.get(create);
461
+ if (parent?.type === "MemberExpression" && parent.object === create) return plainSend(parent);
462
+ const declaration = parent ? parents.get(parent) : undefined;
463
+ if (parent?.type !== "VariableDeclarator" || parent.init !== create || parent.id.type !== "Identifier" || declaration?.type !== "VariableDeclaration" || declaration.kind !== "const") return false;
464
+ const name = parent.id.name;
465
+ return (references.get(name) ?? []).every((reference) => reference === parent.id || (parents.get(reference)?.type === "MemberExpression" && (parents.get(reference) as acorn.MemberExpression).object === reference && plainSend(parents.get(reference))));
466
+ });
467
+ }
468
+ /**
469
+ * Distinct, completely static option objects of agent(...) and agent.create(...) calls, so launch can inspect them
470
+ * before any effect. Calls with any dynamic option stay runtime-checked, and so do handles whose sends may add options.
471
+ */
472
+ export function staticAgentPreparationOptions(script: string, registered: Readonly<Record<string, { run: (...args: never[]) => unknown }>> = {}): Array<Readonly<Record<string, JsonValue>>> {
473
+ const program = parseWorkflow(script);
474
+ const sources = [program, ...registeredFunctionPrograms(program, registered)];
475
+ const distinct = new Map<string, Readonly<Record<string, JsonValue>>>();
476
+ for (const source of sources) for (const options of programAgentOptions(source)) distinct.set(JSON.stringify(options), deepFreeze(options));
477
+ return [...distinct.values()];
478
+ }
479
+ /** Programs of the registered functions the script calls by name, so their agents are inspected before launch like inline ones. */
480
+ function registeredFunctionPrograms(program: acorn.Program, registered: Readonly<Record<string, { run: (...args: never[]) => unknown }>>): acorn.Program[] {
481
+ const called = new Set<string>();
482
+ const visit = (node: acorn.AnyNode): void => {
483
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && Object.prototype.hasOwnProperty.call(registered, node.callee.name)) called.add(node.callee.name);
484
+ for (const child of astChildren(node)) visit(child);
485
+ };
486
+ visit(program);
487
+ const programs: acorn.Program[] = [];
488
+ for (const name of called) {
489
+ const source = registered[name]?.run.toString() ?? "";
490
+ // A method prints as `async run(...) {}`, which only parses inside an object literal.
491
+ for (const wrapped of [`(${source})`, `({${source}})`]) {
492
+ try { programs.push(acorn.parse(wrapped, { ecmaVersion: "latest", sourceType: "module" })); break; } catch { /* try the next form; an unparsable function stays runtime-checked */ }
459
493
  }
460
494
  }
461
- return false;
495
+ return programs;
496
+ }
497
+ function programAgentOptions(program: acorn.Program): Array<Readonly<Record<string, JsonValue>>> {
498
+ const calls = workflowCalls(program).filter((call) => call.callee.name === "agent").map((call) => call.arguments.some((argument) => argument.type === "SpreadElement") ? undefined : call.arguments.length < 2 ? {} : staticPreparationOptions(callArgument(call, 1)));
499
+ const handles = handlesWithCompleteOptions(program).map((call) => staticPreparationOptions(callArgument(call, 0), "name"));
500
+ return [...calls, ...handles].filter((options): options is Record<string, JsonValue> => options !== undefined);
501
+ }
502
+
503
+ function validateStaticAgentOptions(node: acorn.AnyNode | undefined): void {
504
+ if (node?.type !== "ObjectExpression") return;
505
+ for (const key of AGENT_OPTION_KEYS) {
506
+ const value = staticValue(propertyNode(node, key));
507
+ if (value.known) validateAgentOption(key, value.value);
508
+ }
462
509
  }
463
510
  function validateStaticShellOptions(call: WorkflowCall): void {
464
511
  if (call.arguments.some((argument) => argument.type === "SpreadElement")) return;
@@ -495,7 +542,7 @@ export function preflight(script: string, capabilities: PreflightCapabilities, s
495
542
  const phases = calls.filter((call) => call.callee.name === "phase").map((call) => literalString(call.arguments[0])).filter((phase): phase is string => phase !== undefined);
496
543
  for (const call of calls) {
497
544
  const operation = call.callee.name;
498
- if (operation === "agent") validateStaticAgentOptions(call.arguments[1], capabilities.modelAliases ?? {}, capabilities.knownModels ?? capabilities.models, capabilities.settingsPath);
545
+ if (operation === "agent") validateStaticAgentOptions(call.arguments[1]);
499
546
  if (operation === "withWorktree") validateStaticWithWorktree(call, compatibility);
500
547
  if (operation === "shell") validateStaticShellOptions(call);
501
548
  if ((operation === "parallel" || operation === "pipeline") && call.arguments.some((argument) => argument.type === "SpreadElement")) continue;
@@ -506,11 +553,10 @@ export function preflight(script: string, capabilities: PreflightCapabilities, s
506
553
  const handleOptions = agentCreateCalls(program).map((call) => {
507
554
  const options = call.arguments.length === 1 ? callArgument(call, 0) : undefined;
508
555
  if (options?.type !== "ObjectExpression" || !literalString(propertyNode(options, "name"))?.trim()) fail("INVALID_METADATA", "agent.create requires an options object with a stable explicit name");
509
- validateStaticAgentOptions(options, capabilities.modelAliases ?? {}, capabilities.knownModels ?? capabilities.models, capabilities.settingsPath);
556
+ validateStaticAgentOptions(options);
510
557
  return options;
511
558
  });
512
559
  const agentOptions = [...calls.filter((call) => call.callee.name === "agent").map((call) => callArgument(call, 1)), ...handleOptions];
513
- const dynamicAgentRoles = agentOptions.some((options) => hasDynamicAgentRole(options));
514
560
  const staticSchemas: JsonSchema[] = [];
515
561
  for (const options of agentOptions) {
516
562
  const value = staticValue(propertyNode(options, "outputSchema"));
@@ -520,25 +566,16 @@ export function preflight(script: string, capabilities: PreflightCapabilities, s
520
566
  staticSchemas.push(schema);
521
567
  }
522
568
  checkedSchemas.push(...staticSchemas);
523
- const modelRefs = agentOptions.flatMap((options) => { const requested = literalString(propertyNode(options, "model")); return requested === undefined ? [] : [{ requested, resolved: modelCapability(requested, capabilities.modelAliases, capabilities.knownModels ?? capabilities.models, capabilities.settingsPath) }]; });
524
- const models = modelRefs.map(({ resolved }) => resolved);
569
+ const models: string[] = [];
525
570
  const tools = agentOptions.flatMap((options) => {
526
571
  const value = propertyNode(options, "tools");
527
572
  return value?.type === "ArrayExpression" ? value.elements.flatMap((element) => { const tool = element && element.type !== "SpreadElement" ? literalString(element) : undefined; return tool === undefined ? [] : [tool]; }) : [];
528
573
  });
529
- const agentTypes = agentOptions.flatMap((options) => { const value = staticString(propertyNode(options, "role")); return value === null ? [] : [value]; });
530
574
  for (const pattern of tools) {
531
575
  const body = pattern.startsWith("!") ? pattern.slice(1) : pattern;
532
576
  if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !capabilities.tools.has(body)) fail("UNKNOWN_TOOL", `Unknown tool: ${body}`);
533
577
  }
534
- const missingModel = capabilities.skipModelAvailability ? undefined : modelRefs.find(({ resolved }) => !capabilities.models.has(resolved));
535
- if (missingModel) {
536
- if (modelAliasName(missingModel.requested, capabilities.modelAliases ?? {})) unknownModel(missingModel.requested, missingModel.resolved, capabilities.settingsPath);
537
- fail("UNKNOWN_MODEL", `Unknown model: ${missingModel.resolved}`);
538
- }
539
- const missingType = agentTypes.find((type) => !capabilities.agentTypes.has(type));
540
- if (missingType) fail("UNKNOWN_AGENT_TYPE", `Unknown agent type: ${missingType}`);
541
- return Object.freeze({ metadata: deepFreeze(checkedMetadata), referenced: deepFreeze({ phases, models, tools, agentTypes }), schemas: deepFreeze(checkedSchemas), dynamicAgentRoles });
578
+ return Object.freeze({ metadata: deepFreeze(checkedMetadata), referenced: deepFreeze({ phases, models, tools }), schemas: deepFreeze(checkedSchemas) });
542
579
  }
543
580
 
544
581
  export function validateWorkflowLaunch(params: WorkflowValidationParameters, context: WorkflowValidationContext, registry?: WorkflowRegistryApi): ValidatedWorkflowLaunch {
@@ -562,23 +599,9 @@ export function validateWorkflowLaunchWithRegistry(params: WorkflowValidationPar
562
599
  const script = typeof params.script === "string" && params.script.trim() ? params.script : fileScript ?? "";
563
600
  if (!script) fail("INVALID_SYNTAX", "Provide script or scriptPath");
564
601
  const metadata = validateWorkflowMetadata({ name: explicitName, ...(typeof params.description === "string" ? { description: params.description } : {}) });
565
- const globalAgentDefinitions = loadAgentDefinitions(context.cwd, context.agentDir, false, registry && typeof registry.roleDirectoryRegistrations === "function" ? registry.roleDirectoryRegistrations() : registry && typeof registry.roleDirectories === "function" ? registry.roleDirectories() : undefined);
566
- const projectAgentDefinitions = context.projectTrusted ? loadProjectAgentDefinitions(context.cwd) : {};
567
- const agentDefinitions = deepFreeze({ ...globalAgentDefinitions, ...projectAgentDefinitions });
568
- const aliases = context.modelAliases ?? {};
569
- const knownModels = context.knownModels ?? context.availableModels;
570
- const checked = preflight(script, { models: context.availableModels, tools: context.rootTools, agentTypes: new Set(Object.keys(agentDefinitions)), modelAliases: aliases, knownModels, ...(context.settingsPath ? { settingsPath: context.settingsPath } : {}) }, [], metadata);
571
- const roleNames = checked.dynamicAgentRoles ? Object.keys(agentDefinitions) : checked.referenced.agentTypes;
572
- validateRolePolicies(agentDefinitions, roleNames, context.availableModels, aliases, knownModels, context.settingsPath);
573
- if (registry?.validateExtensionSettings) {
574
- const validate = registry.validateExtensionSettings.bind(registry);
575
- const validatorContext = (source: "effective" | "role", role?: string) => ({ source, cwd: context.cwd, projectTrusted: context.projectTrusted, ...(context.settingsPath ? { settingsPath: context.settingsPath } : {}), ...(role === undefined ? {} : { role }) });
576
- validate(context.extensionSettings, validatorContext("effective"));
577
- for (const role of roleNames) {
578
- validate(mergeWorkflowExtensionSettings(context.extensionSettings, agentDefinitions[role]?.extensionSettings), validatorContext("role", role));
579
- }
580
- }
581
- return { script, checked, agentDefinitions, projectAgentDefinitions, roleNames };
602
+ const checked = preflight(script, { models: context.availableModels, tools: context.rootTools, modelAliases: context.modelAliases ?? {}, knownModels: context.knownModels ?? context.availableModels, ...(context.settingsPath ? { settingsPath: context.settingsPath } : {}) }, [], metadata);
603
+ registry?.validateExtensionSettings(context.extensionSettings, { source: "effective", cwd: context.cwd, projectTrusted: context.projectTrusted, ...(context.settingsPath ? { settingsPath: context.settingsPath } : {}) });
604
+ return { script, checked };
582
605
  }
583
606
 
584
607
  export { createLaunchSnapshot, loadLaunchSnapshot } from "./utils.js";
package/starter/index.ts CHANGED
@@ -27,7 +27,6 @@ const extension: WorkflowExtension = {
27
27
  resolve: ({ rootModel }) => `${rootModel.provider}/${rootModel.model}`,
28
28
  },
29
29
  },
30
- roleDirectories: [new URL("./roles/", import.meta.url)],
31
30
  };
32
31
 
33
32
  export default function (): void {