pi-extensible-workflows 3.0.0 → 3.2.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 (46) hide show
  1. package/README.md +15 -17
  2. package/dist/src/agent-execution.d.ts +4 -80
  3. package/dist/src/agent-execution.js +20 -10
  4. package/dist/src/cli.d.ts +2 -0
  5. package/dist/src/cli.js +48 -2
  6. package/dist/src/doctor-cleanup.d.ts +41 -0
  7. package/dist/src/doctor-cleanup.js +597 -0
  8. package/dist/src/doctor.d.ts +7 -2
  9. package/dist/src/doctor.js +127 -22
  10. package/dist/src/execution.js +13 -4
  11. package/dist/src/host.d.ts +83 -4
  12. package/dist/src/host.js +1246 -410
  13. package/dist/src/index.d.ts +5 -2
  14. package/dist/src/index.js +3 -1
  15. package/dist/src/persistence.d.ts +3 -0
  16. package/dist/src/persistence.js +49 -1
  17. package/dist/src/registry.d.ts +21 -9
  18. package/dist/src/registry.js +131 -21
  19. package/dist/src/session-inspector.js +4 -2
  20. package/dist/src/types.d.ts +135 -7
  21. package/dist/src/utils.d.ts +6 -0
  22. package/dist/src/utils.js +45 -5
  23. package/dist/src/validation.d.ts +6 -2
  24. package/dist/src/validation.js +157 -31
  25. package/dist/src/workflow-artifacts.d.ts +13 -0
  26. package/dist/src/workflow-artifacts.js +39 -0
  27. package/examples/workflow-extension-template/README.md +37 -0
  28. package/examples/workflow-extension-template/extension.test.mjs +59 -0
  29. package/examples/workflow-extension-template/index.js +51 -0
  30. package/examples/workflow-extension-template/roles/reviewer.md +4 -0
  31. package/package.json +5 -3
  32. package/skills/pi-extensible-workflows/SKILL.md +45 -18
  33. package/src/agent-execution.ts +23 -37
  34. package/src/cli.ts +33 -2
  35. package/src/doctor-cleanup.ts +337 -0
  36. package/src/doctor.ts +117 -24
  37. package/src/execution.ts +13 -4
  38. package/src/host.ts +1039 -366
  39. package/src/index.ts +5 -2
  40. package/src/persistence.ts +35 -1
  41. package/src/registry.ts +108 -25
  42. package/src/session-inspector.ts +4 -2
  43. package/src/types.ts +53 -8
  44. package/src/utils.ts +39 -5
  45. package/src/validation.ts +130 -31
  46. package/src/workflow-artifacts.ts +34 -0
@@ -1,3 +1,4 @@
1
+ import type { AgentSessionEvent, CreateAgentSessionOptions, InlineExtension, SessionStats, ToolDefinition } from "@earendil-works/pi-coding-agent";
1
2
  export declare const RUN_STATES: readonly ["queued", "running", "pausing", "paused", "awaiting_input", "completed", "failed", "stopped", "interrupted", "budget_exhausted"];
2
3
  export declare const AGENT_STATES: readonly ["queued", "running", "waiting_for_child", "paused", "retrying", "completed", "failed", "cancelled"];
3
4
  export declare const WORKFLOW_CALL_KINDS: readonly ["agent", "parallel", "pipeline", "checkpoint", "phase", "withWorktree", "shell"];
@@ -141,6 +142,17 @@ export interface ModelSpec {
141
142
  model: string;
142
143
  thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
143
144
  }
145
+ export interface WorkflowModelAliasResolverContext {
146
+ cwd: string;
147
+ projectTrusted: boolean;
148
+ rootModel: ModelSpec;
149
+ knownModels: ReadonlySet<string>;
150
+ availableModels: ReadonlySet<string>;
151
+ signal: AbortSignal;
152
+ }
153
+ export interface WorkflowModelAlias {
154
+ resolve: (context: Readonly<WorkflowModelAliasResolverContext>) => string | Promise<string>;
155
+ }
144
156
  export interface WorkflowMetadata {
145
157
  name: string;
146
158
  description?: string;
@@ -150,6 +162,25 @@ export interface WorkflowSettings {
150
162
  modelAliases?: Readonly<Record<string, string>>;
151
163
  disabledAgentResources?: Readonly<AgentResourceExclusions>;
152
164
  }
165
+ export interface WorkflowSettingsOverrides {
166
+ concurrency?: number;
167
+ modelAliases?: Readonly<Record<string, string>>;
168
+ disabledAgentResources?: Readonly<AgentResourceExclusions>;
169
+ }
170
+ export interface WorkflowSettingsSources {
171
+ concurrency: string;
172
+ modelAliases: string;
173
+ disabledAgentResources: string;
174
+ }
175
+ export interface WorkflowSettingsResolution {
176
+ globalSettingsPath: string;
177
+ projectSettingsPath: string;
178
+ projectTrusted: boolean;
179
+ global: Readonly<WorkflowSettings>;
180
+ project: Readonly<WorkflowSettingsOverrides>;
181
+ effective: Readonly<WorkflowSettings>;
182
+ sources: Readonly<WorkflowSettingsSources>;
183
+ }
153
184
  export interface AgentResourceExclusions {
154
185
  skills: readonly string[];
155
186
  extensions: readonly string[];
@@ -163,6 +194,8 @@ export interface AgentResourcePolicy {
163
194
  effective: AgentResourceExclusions;
164
195
  unmatchedSkills: string[];
165
196
  unmatchedExtensions: string[];
197
+ excludedSkills?: string[];
198
+ excludedExtensions?: string[];
166
199
  }
167
200
  export interface AgentActivity {
168
201
  kind: "reasoning" | "tool" | "text";
@@ -183,6 +216,8 @@ export interface AgentSetupSummary {
183
216
  disabledAgentResources?: {
184
217
  skills: readonly string[];
185
218
  extensions: readonly string[];
219
+ excludedSkills?: readonly string[];
220
+ excludedExtensions?: readonly string[];
186
221
  unmatchedSkills: readonly string[];
187
222
  unmatchedExtensions: readonly string[];
188
223
  };
@@ -222,6 +257,7 @@ export interface AgentRecord {
222
257
  state: AgentState;
223
258
  parentId?: string;
224
259
  structuralPath?: readonly string[];
260
+ resultPath?: string;
225
261
  parentBreadcrumb?: string;
226
262
  worktreeOwner?: string;
227
263
  role?: string;
@@ -294,9 +330,11 @@ export interface LaunchSnapshot {
294
330
  args: JsonValue;
295
331
  metadata: WorkflowMetadata;
296
332
  settings: WorkflowSettings;
333
+ settingsSources?: WorkflowSettingsSources;
297
334
  budget?: WorkflowBudget;
298
335
  settingsPath?: string;
299
336
  modelAliases?: Readonly<Record<string, string>>;
337
+ phases?: readonly string[];
300
338
  models: readonly string[];
301
339
  tools: readonly string[];
302
340
  agentTypes: readonly string[];
@@ -359,14 +397,69 @@ export interface WorkflowVariable {
359
397
  schema: JsonSchema;
360
398
  resolve: (run: Readonly<WorkflowRunContext>) => Promise<JsonValue> | JsonValue;
361
399
  }
400
+ type NativeAgentMessage = {
401
+ role: string;
402
+ content?: unknown;
403
+ stopReason?: string;
404
+ errorMessage?: string;
405
+ usage?: {
406
+ input: number;
407
+ output: number;
408
+ cacheRead: number;
409
+ cacheWrite: number;
410
+ cost: {
411
+ total: number;
412
+ };
413
+ };
414
+ };
415
+ type NativeSessionStats = Pick<SessionStats, "tokens" | "cost">;
416
+ export interface NativeSession {
417
+ readonly sessionId: string;
418
+ readonly sessionFile: string | undefined;
419
+ readonly messages: readonly NativeAgentMessage[];
420
+ getSessionStats(): NativeSessionStats;
421
+ readonly systemPrompt?: string;
422
+ readonly model?: {
423
+ provider: string;
424
+ model?: string;
425
+ id?: string;
426
+ };
427
+ readonly agent?: {
428
+ state: {
429
+ tools: readonly {
430
+ name: string;
431
+ }[];
432
+ };
433
+ };
434
+ getLeafId?: () => string | null;
435
+ getToolDefinitions?: () => unknown;
436
+ subscribe?(listener: (event: AgentSessionEvent) => void): () => void;
437
+ prompt(text: string): Promise<void>;
438
+ steer?(text: string): Promise<void>;
439
+ abort?(): Promise<void>;
440
+ dispose(): void;
441
+ }
442
+ type SessionTools = NonNullable<CreateAgentSessionOptions["tools"]>;
443
+ type SessionCustomTools = NonNullable<CreateAgentSessionOptions["customTools"]>;
444
+ export interface SessionInput {
445
+ cwd: string;
446
+ model: ModelSpec;
447
+ tools: SessionTools;
448
+ sessionLabel: string;
449
+ agentDir?: string;
450
+ customTools?: SessionCustomTools;
451
+ resultTool?: ToolDefinition;
452
+ systemPromptAppend?: string;
453
+ extensionFactories?: InlineExtension[];
454
+ resourcePolicy?: AgentResourcePolicy;
455
+ options?: AgentOptions;
456
+ }
457
+ export type SessionFactory = (input: SessionInput) => Promise<NativeSession>;
362
458
  export interface AgentSetup {
363
459
  prompt: string;
364
- options: Record<string, JsonValue>;
365
- sessionInput: {
366
- extensionFactories?: unknown[];
367
- [key: string]: unknown;
368
- };
369
- createSession: unknown;
460
+ options: AgentOptions;
461
+ sessionInput: SessionInput;
462
+ createSession: SessionFactory;
370
463
  }
371
464
  export interface AgentSetupContext {
372
465
  readonly run: Readonly<WorkflowRunContext>;
@@ -383,13 +476,21 @@ export interface RegisteredAgentSetupHook {
383
476
  priority: number;
384
477
  setup: AgentSetupHook["setup"];
385
478
  }
386
- export interface WorkflowExtension {
479
+ export interface WorkflowExtensionMetadata {
387
480
  version: string;
388
481
  headline: string;
389
482
  description: string;
483
+ }
484
+ export interface WorkflowRoleDirectoryRegistration {
485
+ path: string;
486
+ extension: WorkflowExtensionMetadata;
487
+ }
488
+ export interface WorkflowExtension extends WorkflowExtensionMetadata {
390
489
  functions?: Readonly<Record<string, WorkflowFunction>>;
391
490
  variables?: Readonly<Record<string, WorkflowVariable>>;
491
+ modelAliases?: Readonly<Record<string, WorkflowModelAlias>>;
392
492
  agentSetupHooks?: Readonly<Record<string, AgentSetupHook>>;
493
+ roleDirectories?: readonly (string | URL)[];
393
494
  }
394
495
  export interface WorkflowJournal {
395
496
  get(path: string): JsonValue | undefined;
@@ -511,10 +612,34 @@ export interface WorkflowCatalogVariable {
511
612
  description: string;
512
613
  schema: JsonSchema;
513
614
  }
615
+ export interface WorkflowCatalogModelAlias {
616
+ name: string;
617
+ kind: "static" | "dynamic";
618
+ provenance: string;
619
+ version?: string;
620
+ headline?: string;
621
+ extensionDescription?: string;
622
+ }
623
+ export interface WorkflowCatalogSettings {
624
+ concurrency: number;
625
+ modelAliases: Readonly<Record<string, string>>;
626
+ disabledAgentResources: AgentResourceExclusions;
627
+ globalSettingsPath: string;
628
+ projectSettingsPath: string;
629
+ projectTrusted: boolean;
630
+ sources: WorkflowSettingsSources;
631
+ }
632
+ export interface WorkflowCatalogContext {
633
+ cwd: string;
634
+ projectTrusted: boolean;
635
+ globalSettingsPath?: string;
636
+ }
514
637
  export interface WorkflowCatalog {
515
638
  functions: readonly WorkflowCatalogFunction[];
516
639
  variables: readonly WorkflowCatalogVariable[];
517
640
  modelAliases?: Readonly<Record<string, string>>;
641
+ modelAliasEntries?: readonly WorkflowCatalogModelAlias[];
642
+ settings?: WorkflowCatalogSettings;
518
643
  }
519
644
  export interface WorkflowCatalogIndexFunction {
520
645
  name: string;
@@ -530,6 +655,8 @@ export interface WorkflowCatalogIndex {
530
655
  functions: readonly WorkflowCatalogIndexFunction[];
531
656
  variables: readonly WorkflowCatalogIndexVariable[];
532
657
  modelAliases?: Readonly<Record<string, string>>;
658
+ modelAliasEntries?: readonly WorkflowCatalogModelAlias[];
659
+ settings?: WorkflowCatalogSettings;
533
660
  }
534
661
  export interface WorkflowCatalogError {
535
662
  error: {
@@ -563,3 +690,4 @@ export interface ValidatedWorkflowLaunch {
563
690
  roleNames: readonly string[];
564
691
  functionName?: string;
565
692
  }
693
+ export {};
@@ -13,12 +13,18 @@ export declare function asWorkflowError(error: unknown): WorkflowError;
13
13
  export declare function fail(code: WorkflowErrorCode, message: string): never;
14
14
  export declare function parseThinking(value: unknown): ModelSpec["thinking"] | undefined;
15
15
  export declare function parseModelReference(value: string): ModelSpec;
16
+ export declare function annotateModelAliasError(error: unknown, name: string): unknown;
17
+ export declare function modelAliasErrorName(error: unknown): string | undefined;
16
18
  export declare function modelAliasName(value: string, aliases: Readonly<Record<string, string>>): string | undefined;
17
19
  export declare function validateModelAliases(value: unknown, settingsPath?: string): Readonly<Record<string, string>>;
18
20
  export declare function unknownModel(value: string, target: string | undefined, settingsPath?: string): never;
19
21
  export declare function resolveModelReference(value: string, aliases?: Readonly<Record<string, string>>, knownModels?: ReadonlySet<string>, settingsPath?: string): ModelSpec;
20
22
  export declare function modelCapability(value: string | ModelSpec, aliases?: Readonly<Record<string, string>>, knownModels?: ReadonlySet<string>, settingsPath?: string): string;
21
23
  export declare function aliasDrift(previous: Readonly<Record<string, string>>, current: Readonly<Record<string, string>>): string[];
24
+ export declare function validateResourcePattern(pattern: string): void;
25
+ export declare function resourcePatternMatches(resource: string, pattern: string): boolean;
26
+ export declare function disabledResources(patterns: readonly string[], resources: readonly string[]): string[];
27
+ export declare function unmatchedResourcePatterns(patterns: readonly string[], resources: readonly string[]): string[];
22
28
  export declare function mergeAgentResourceExclusions(...values: (AgentResourceExclusions | undefined)[]): AgentResourceExclusions;
23
29
  export declare function createLaunchSnapshot(input: Omit<import("./types.js").LaunchSnapshot, "identityVersion"> & {
24
30
  identityVersion?: number;
package/dist/src/utils.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WorkflowError } from "./types.js";
2
+ import { Minimatch } from "minimatch";
2
3
  export function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
3
4
  export { object as isObject };
4
5
  export function jsonValue(value, seen = new Set()) {
@@ -61,7 +62,21 @@ export function parseModelReference(value) {
61
62
  fail("UNKNOWN_MODEL", `Invalid thinking level: ${thinking}`);
62
63
  return { provider: match[1], model: match[2], ...(thinking ? { thinking: thinking } : {}) };
63
64
  }
64
- function aliasError(message, settingsPath) { fail("CONFIG_ERROR", `${message} (settings: ${settingsPath})`); }
65
+ const MODEL_ALIAS_ERROR_NAME = Symbol("modelAliasErrorName");
66
+ function aliasError(message, settingsPath, name) {
67
+ const error = new WorkflowError("CONFIG_ERROR", `${message} (settings: ${settingsPath})`);
68
+ if (name)
69
+ Object.defineProperty(error, MODEL_ALIAS_ERROR_NAME, { value: name, configurable: true });
70
+ throw error;
71
+ }
72
+ export function annotateModelAliasError(error, name) {
73
+ if (error instanceof WorkflowError)
74
+ Object.defineProperty(error, MODEL_ALIAS_ERROR_NAME, { value: name, configurable: true });
75
+ return error;
76
+ }
77
+ export function modelAliasErrorName(error) {
78
+ return error instanceof WorkflowError ? error[MODEL_ALIAS_ERROR_NAME] : undefined;
79
+ }
65
80
  export function modelAliasName(value, aliases) {
66
81
  const name = /^([^/:\s]+)(?::[^:\s]+)?$/.exec(value)?.[1];
67
82
  return name && Object.prototype.hasOwnProperty.call(aliases, name) ? name : undefined;
@@ -72,9 +87,9 @@ export function validateModelAliases(value, settingsPath = "workflow settings")
72
87
  const aliases = {};
73
88
  for (const [name, target] of Object.entries(value)) {
74
89
  if (!MODEL_ALIAS_NAME.test(name))
75
- aliasError(`Invalid model alias name: ${name}`, settingsPath);
90
+ aliasError(`Invalid model alias name: ${name}`, settingsPath, name);
76
91
  if (typeof target !== "string" || !target.trim())
77
- aliasError(`Invalid model alias target for ${name}`, settingsPath);
92
+ aliasError(`Invalid model alias target for ${name}`, settingsPath, name);
78
93
  aliases[name] = target;
79
94
  }
80
95
  for (const name of Object.keys(aliases)) {
@@ -82,7 +97,7 @@ export function validateModelAliases(value, settingsPath = "workflow settings")
82
97
  resolveModelReference(name, aliases);
83
98
  }
84
99
  catch (error) {
85
- aliasError(`Invalid model alias target for ${name}: ${errorText(error)}`, settingsPath);
100
+ aliasError(`Invalid model alias target for ${name}: ${errorText(error)}`, settingsPath, name);
86
101
  }
87
102
  }
88
103
  return Object.freeze(aliases);
@@ -123,6 +138,31 @@ export function modelCapability(value, aliases, knownModels, settingsPath) {
123
138
  export function aliasDrift(previous, current) {
124
139
  return [...new Set([...Object.keys(previous), ...Object.keys(current)])].sort().flatMap((name) => previous[name] === current[name] ? [] : [`${name}: ${previous[name] ?? "(missing)"} -> ${current[name] ?? "(missing)"}`]);
125
140
  }
126
- export function mergeAgentResourceExclusions(...values) { return { skills: [...new Set(values.flatMap((value) => value?.skills ?? []))], extensions: [...new Set(values.flatMap((value) => value?.extensions ?? []))] }; }
141
+ const RESOURCE_PATTERN_OPTIONS = { dot: true, nonegate: true, nocomment: true };
142
+ function resourcePatternBody(pattern) { return pattern.startsWith("!") ? pattern.slice(1) : pattern; }
143
+ function resourcePatternPath(value) { return value.replaceAll("\\", "/"); }
144
+ export function validateResourcePattern(pattern) {
145
+ const body = resourcePatternBody(pattern);
146
+ if (!body)
147
+ throw new Error(`Empty minimatch pattern ${JSON.stringify(pattern)}`);
148
+ const matcher = new Minimatch(resourcePatternPath(body), RESOURCE_PATTERN_OPTIONS);
149
+ if (matcher.makeRe() === false)
150
+ throw new Error(`Invalid minimatch pattern ${JSON.stringify(pattern)}`);
151
+ }
152
+ export function resourcePatternMatches(resource, pattern) { return new Minimatch(resourcePatternPath(resourcePatternBody(pattern)), RESOURCE_PATTERN_OPTIONS).match(resourcePatternPath(resource)); }
153
+ export function disabledResources(patterns, resources) {
154
+ const disabled = new Set();
155
+ for (const resource of resources) {
156
+ let excluded = false;
157
+ for (const pattern of patterns)
158
+ if (resourcePatternMatches(resource, pattern))
159
+ excluded = !pattern.startsWith("!");
160
+ if (excluded)
161
+ disabled.add(resource);
162
+ }
163
+ return [...disabled];
164
+ }
165
+ export function unmatchedResourcePatterns(patterns, resources) { return patterns.filter((pattern) => !resources.some((resource) => resourcePatternMatches(resource, pattern))); }
166
+ export function mergeAgentResourceExclusions(...values) { return { skills: values.flatMap((value) => value?.skills ?? []), extensions: values.flatMap((value) => value?.extensions ?? []) }; }
127
167
  export function createLaunchSnapshot(input) { return deepFreeze(structuredClone({ ...input, launchKind: input.launchKind ?? (input.functionName ? "function" : "inline"), identityVersion: input.identityVersion ?? LAUNCH_SNAPSHOT_IDENTITY_VERSION })); }
128
168
  export function loadLaunchSnapshot(input) { return deepFreeze(structuredClone(input)); }
@@ -1,15 +1,19 @@
1
- import type { AgentDefinition, AgentResourcePolicy, CheckpointInput, JsonSchema, JsonValue, LaunchSnapshot, PreflightCapabilities, PreflightResult, ShellOptions, StaticWorkflowCall, ValidatedWorkflowLaunch, WorkflowMetadata, WorkflowSettings, WorkflowValidationContext, WorkflowValidationParameters } from "./types.js";
1
+ import type { AgentDefinition, AgentResourcePolicy, CheckpointInput, JsonSchema, JsonValue, LaunchSnapshot, PreflightCapabilities, PreflightResult, ShellOptions, StaticWorkflowCall, ValidatedWorkflowLaunch, WorkflowMetadata, WorkflowRoleDirectoryRegistration, WorkflowSettings, WorkflowSettingsOverrides, WorkflowSettingsResolution, WorkflowValidationContext, WorkflowValidationParameters } from "./types.js";
2
2
  import type { WorkflowRegistryApi } from "./registry.js";
3
3
  export declare const DEFAULT_SETTINGS: Readonly<WorkflowSettings>;
4
4
  export declare function validateCheckpoint(value: unknown): CheckpointInput;
5
5
  export declare function workflowSettingsPath(agentDir?: string): string;
6
6
  export declare function workflowProjectSettingsPath(cwd: string): string;
7
7
  export declare function loadSettings(path?: string): Readonly<WorkflowSettings>;
8
+ export declare function loadSettingsOverrides(path: string): Readonly<WorkflowSettingsOverrides>;
9
+ export declare function resolveWorkflowSettings(cwd: string, projectTrusted: boolean, globalSettingsPath?: string): WorkflowSettingsResolution;
10
+ export declare function validateModelAliasAvailability(aliases: Readonly<Record<string, string>>, names: readonly string[], availableModels: ReadonlySet<string>, knownModels: ReadonlySet<string>, settingsPath?: string): void;
8
11
  export declare function resolveAgentResourcePolicy(cwd: string, projectTrusted: boolean, globalSettingsPath?: string): AgentResourcePolicy;
9
12
  export declare function saveModelAliases(path?: string, aliases?: Readonly<Record<string, string>>): void;
10
13
  export declare function parseRoleMarkdown(content: string, strict?: boolean, rolePath?: string): AgentDefinition;
11
14
  export declare function workflowRoleDirectories(agentDir?: string): readonly string[];
12
- export declare function loadAgentDefinitions(cwd: string, agentDir?: string, projectTrusted?: boolean): Readonly<Record<string, AgentDefinition>>;
15
+ export type WorkflowRoleDirectoryInput = string | WorkflowRoleDirectoryRegistration;
16
+ export declare function loadAgentDefinitions(cwd: string, agentDir?: string, projectTrusted?: boolean, extensionRoleDirectories?: readonly WorkflowRoleDirectoryInput[]): Readonly<Record<string, AgentDefinition>>;
13
17
  export declare function instrumentWorkflow(script: string): string;
14
18
  export declare function workflowPrompt(template: string, values: Readonly<Record<string, JsonValue>>): string;
15
19
  export declare function validateSchema(schema: unknown, at?: string): asserts schema is JsonSchema;
@@ -8,7 +8,8 @@ import { Script } from "node:vm";
8
8
  import { Value } from "typebox/value";
9
9
  import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
10
10
  import { WorkflowError } from "./types.js";
11
- import { deepFreeze, errorText, fail, jsonValue, mergeAgentResourceExclusions, modelAliasName, modelCapability, object, parseThinking, positiveInteger, resolveModelReference, unknownModel, validateModelAliases } from "./utils.js";
11
+ import { registeredWorkflowRoleDirectoryRegistrations } from "./registry.js";
12
+ import { annotateModelAliasError, deepFreeze, errorText, fail, jsonValue, modelAliasName, modelCapability, object, parseThinking, positiveInteger, resolveModelReference, unknownModel, validateModelAliases, validateResourcePattern } from "./utils.js";
12
13
  import { WORKFLOW_CALL_KINDS } from "./types.js";
13
14
  export const DEFAULT_SETTINGS = Object.freeze({ concurrency: 8 });
14
15
  export function validateCheckpoint(value) {
@@ -23,11 +24,27 @@ export function validateCheckpoint(value) {
23
24
  export function workflowSettingsPath(agentDir = getAgentDir()) { return join(agentDir, ROLE_DIRECTORY, "settings.json"); }
24
25
  export function workflowProjectSettingsPath(cwd) { return join(cwd, ".pi", ROLE_DIRECTORY, "settings.json"); }
25
26
  const EMPTY_AGENT_RESOURCE_EXCLUSIONS = Object.freeze({ skills: [], extensions: [] });
27
+ function resourcePatternHasMagic(value) { return /[*?\x5b\x5d{}()]/.test(value); }
26
28
  function normalizedResourcePath(value, settingsPath) {
27
29
  let expanded = value === "~" ? homedir() : value.startsWith("~/") || value.startsWith("~\\") ? join(homedir(), value.slice(2)) : value;
28
30
  if (expanded.startsWith("file://"))
29
31
  expanded = fileURLToPath(expanded);
30
32
  const resolved = resolve(dirname(settingsPath), expanded);
33
+ if (expanded === "**" || expanded.startsWith("**/") || expanded.startsWith("**\\"))
34
+ return expanded;
35
+ if (resourcePatternHasMagic(expanded)) {
36
+ const magicIndex = resolved.search(/[*?\x5b\x5d{}()]/);
37
+ const separatorIndex = Math.max(resolved.lastIndexOf("/", magicIndex), resolved.lastIndexOf("\\", magicIndex));
38
+ const rootBoundary = separatorIndex === 0 || (separatorIndex === 2 && /^[A-Za-z]:[\\/]/.test(resolved));
39
+ const prefix = rootBoundary ? resolved.slice(0, separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(0, separatorIndex) : resolved;
40
+ const suffix = rootBoundary ? resolved.slice(separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(separatorIndex) : "";
41
+ try {
42
+ return `${realpathSync(prefix)}${suffix}`;
43
+ }
44
+ catch {
45
+ return resolved;
46
+ }
47
+ }
31
48
  try {
32
49
  return realpathSync(resolved);
33
50
  }
@@ -51,35 +68,41 @@ function validateAgentResourceExclusions(value, settingsPath, errorCode = "INVAL
51
68
  continue;
52
69
  if (!Array.isArray(entries))
53
70
  fail(errorCode, `${base}.${kind} must be an array`);
54
- const seen = new Set();
55
71
  for (const [index, entry] of entries.entries()) {
56
72
  if (typeof entry !== "string" || !entry.trim())
57
73
  fail(errorCode, `${base}.${kind}[${String(index)}] must be a non-empty string`);
58
74
  let selector = entry.trim();
59
75
  if (kind === "extensions") {
76
+ const negated = selector.startsWith("!");
77
+ const body = negated ? selector.slice(1) : selector;
78
+ if (!body)
79
+ fail(errorCode, `${base}.${kind}[${String(index)}] must be a valid minimatch pattern: Empty minimatch pattern ${JSON.stringify(selector)}`);
60
80
  try {
61
- selector = normalizedResourcePath(selector, settingsPath);
81
+ selector = `${negated ? "!" : ""}${normalizedResourcePath(body, settingsPath)}`;
62
82
  }
63
83
  catch (error) {
64
84
  fail(errorCode, `${base}.${kind}[${String(index)}] must be a valid path: ${errorText(error)}`);
65
85
  }
66
86
  }
67
- if (!seen.has(selector)) {
68
- seen.add(selector);
69
- normalized[kind].push(selector);
87
+ try {
88
+ validateResourcePattern(selector);
89
+ }
90
+ catch (error) {
91
+ fail(errorCode, `${base}.${kind}[${String(index)}] must be a valid minimatch pattern: ${errorText(error)}`);
70
92
  }
93
+ normalized[kind].push(selector);
71
94
  }
72
95
  }
73
96
  return Object.freeze({ skills: Object.freeze(normalized.skills), extensions: Object.freeze(normalized.extensions) });
74
97
  }
75
- export function loadSettings(path = workflowSettingsPath()) {
98
+ function parseSettings(path, partial) {
76
99
  let parsed;
77
100
  try {
78
101
  parsed = JSON.parse(readFileSync(path, "utf8"));
79
102
  }
80
103
  catch (error) {
81
104
  if (error.code === "ENOENT")
82
- return DEFAULT_SETTINGS;
105
+ return partial ? Object.freeze({}) : DEFAULT_SETTINGS;
83
106
  fail("CONFIG_ERROR", `Invalid workflow settings JSON at ${path}: ${errorText(error)}`);
84
107
  }
85
108
  if (!object(parsed))
@@ -88,19 +111,51 @@ export function loadSettings(path = workflowSettingsPath()) {
88
111
  const unknown = Object.keys(parsed).find((key) => !allowed.has(key));
89
112
  if (unknown)
90
113
  fail("INVALID_SETTINGS", `Unknown workflow setting at ${path}: ${unknown}`);
91
- const concurrency = parsed.concurrency === undefined ? DEFAULT_SETTINGS.concurrency : parsed.concurrency;
92
- if (!positiveInteger(concurrency) || concurrency > 16)
114
+ const concurrency = parsed.concurrency === undefined ? (partial ? undefined : DEFAULT_SETTINGS.concurrency) : parsed.concurrency;
115
+ if (concurrency !== undefined && (!positiveInteger(concurrency) || concurrency > 16))
93
116
  fail("INVALID_SETTINGS", `${path}.concurrency must be an integer from 1 to 16`);
94
117
  const modelAliases = parsed.modelAliases === undefined ? undefined : validateModelAliases(parsed.modelAliases, path);
95
118
  const disabledAgentResources = validateAgentResourceExclusions(parsed.disabledAgentResources, path);
96
- return Object.freeze({ concurrency, ...(modelAliases ? { modelAliases } : {}), ...(disabledAgentResources ? { disabledAgentResources } : {}) });
119
+ return Object.freeze({ ...(concurrency === undefined ? {} : { concurrency }), ...(modelAliases === undefined ? {} : { modelAliases }), ...(disabledAgentResources === undefined ? {} : { disabledAgentResources }) });
97
120
  }
98
- export function resolveAgentResourcePolicy(cwd, projectTrusted, globalSettingsPath = workflowSettingsPath()) {
121
+ export function loadSettings(path = workflowSettingsPath()) { return parseSettings(path, false); }
122
+ export function loadSettingsOverrides(path) { return parseSettings(path, true); }
123
+ export function resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath = workflowSettingsPath()) {
99
124
  const projectSettingsPath = workflowProjectSettingsPath(cwd);
100
- const global = loadSettings(globalSettingsPath).disabledAgentResources ?? EMPTY_AGENT_RESOURCE_EXCLUSIONS;
101
- const project = projectTrusted ? loadSettings(projectSettingsPath).disabledAgentResources ?? EMPTY_AGENT_RESOURCE_EXCLUSIONS : EMPTY_AGENT_RESOURCE_EXCLUSIONS;
102
- const effective = mergeAgentResourceExclusions(global, project);
103
- return { globalSettingsPath, projectSettingsPath, projectTrusted, global, project, effective, unmatchedSkills: [], unmatchedExtensions: [] };
125
+ const global = loadSettings(globalSettingsPath);
126
+ const project = projectTrusted ? loadSettingsOverrides(projectSettingsPath) : Object.freeze({});
127
+ const projectHas = (key) => Object.prototype.hasOwnProperty.call(project, key);
128
+ const sources = {
129
+ concurrency: projectHas("concurrency") ? projectSettingsPath : globalSettingsPath,
130
+ modelAliases: projectHas("modelAliases") ? projectSettingsPath : globalSettingsPath,
131
+ disabledAgentResources: projectHas("disabledAgentResources") ? projectSettingsPath : globalSettingsPath,
132
+ };
133
+ const effective = Object.freeze({
134
+ concurrency: project.concurrency ?? global.concurrency,
135
+ ...(projectHas("modelAliases") ? { modelAliases: project.modelAliases } : global.modelAliases === undefined ? {} : { modelAliases: global.modelAliases }),
136
+ ...(projectHas("disabledAgentResources") ? { disabledAgentResources: project.disabledAgentResources } : global.disabledAgentResources === undefined ? {} : { disabledAgentResources: global.disabledAgentResources }),
137
+ });
138
+ return { globalSettingsPath, projectSettingsPath, projectTrusted, global, project, effective, sources };
139
+ }
140
+ export function validateModelAliasAvailability(aliases, names, availableModels, knownModels, settingsPath) {
141
+ for (const name of names) {
142
+ try {
143
+ const target = modelCapability(name, aliases, knownModels, settingsPath);
144
+ if (!availableModels.has(target))
145
+ unknownModel(name, target, settingsPath);
146
+ }
147
+ catch (error) {
148
+ throw annotateModelAliasError(error, name);
149
+ }
150
+ }
151
+ }
152
+ export function resolveAgentResourcePolicy(cwd, projectTrusted, globalSettingsPath = workflowSettingsPath()) {
153
+ const resolved = resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath);
154
+ const empty = EMPTY_AGENT_RESOURCE_EXCLUSIONS;
155
+ const global = resolved.global.disabledAgentResources ?? empty;
156
+ const project = resolved.project.disabledAgentResources ?? empty;
157
+ const effective = resolved.effective.disabledAgentResources ?? empty;
158
+ return { globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted, global, project, effective, unmatchedSkills: [], unmatchedExtensions: [] };
104
159
  }
105
160
  export function saveModelAliases(path = workflowSettingsPath(), aliases = {}) {
106
161
  const normalized = validateModelAliases(aliases, path);
@@ -175,23 +230,60 @@ export function workflowRoleDirectories(agentDir = getAgentDir()) {
175
230
  function projectRoleDirectories(root) {
176
231
  return [join(root, ROLE_DIRECTORY, "roles")];
177
232
  }
178
- function readAgentDefinitions(dir) {
179
- try {
180
- return Object.fromEntries(readdirSync(dir, { withFileTypes: true })
181
- .filter((entry) => entry.isFile() && extname(entry.name) === ".md")
182
- .map((entry) => { const path = join(dir, entry.name); return [basename(entry.name, ".md"), parseRoleMarkdown(readFileSync(path, "utf8"), true, path)]; }));
233
+ function roleDirectorySources(dirs) {
234
+ const seen = new Set();
235
+ return dirs.flatMap((value) => {
236
+ const source = typeof value === "string" ? { path: value } : { path: value.path, extension: value.extension };
237
+ if (seen.has(source.path))
238
+ return [];
239
+ seen.add(source.path);
240
+ return [source];
241
+ });
242
+ }
243
+ function roleDirectoryLabel(source, extension = true) {
244
+ return source.extension ? `Extension "${source.extension.headline}" (${source.extension.version})` : extension ? "Registered workflow extension" : "Standard workflow";
245
+ }
246
+ function scanRoleFiles(dirs, extension) {
247
+ const files = [];
248
+ for (const source of roleDirectorySources(dirs)) {
249
+ let entries;
250
+ try {
251
+ entries = readdirSync(source.path, { withFileTypes: true });
252
+ }
253
+ catch (error) {
254
+ if (!extension && error.code === "ENOENT")
255
+ continue;
256
+ fail("INVALID_METADATA", `${roleDirectoryLabel(source, extension)} role directory "${source.path}" could not be scanned: ${errorText(error)}`);
257
+ }
258
+ for (const entry of entries)
259
+ if (entry.isFile() && extname(entry.name) === ".md")
260
+ files.push({ name: basename(entry.name, ".md"), path: join(source.path, entry.name), source });
183
261
  }
184
- catch (error) {
185
- if (error.code === "ENOENT")
186
- return {};
187
- throw error;
262
+ return files.sort((left, right) => left.name.localeCompare(right.name) || left.path.localeCompare(right.path));
263
+ }
264
+ function readRoleDefinitions(dirs, extension = false) {
265
+ const files = scanRoleFiles(dirs, extension);
266
+ if (extension) {
267
+ const byName = new Map();
268
+ for (const file of files)
269
+ byName.set(file.name, [...(byName.get(file.name) ?? []), file]);
270
+ for (const [name, matches] of byName)
271
+ if (matches.length > 1)
272
+ fail("INVALID_METADATA", `Duplicate extension role "${name}": ${matches.map(({ path, source }) => `${roleDirectoryLabel(source)} role directory "${source.path}" (${path})`).join("; ")}`);
188
273
  }
274
+ return Object.fromEntries(files.map(({ name, path, source }) => {
275
+ try {
276
+ return [name, parseRoleMarkdown(readFileSync(path, "utf8"), true, path)];
277
+ }
278
+ catch (error) {
279
+ if (extension)
280
+ fail("INVALID_METADATA", `${roleDirectoryLabel(source)} role directory "${source.path}" contains invalid role "${name}" at "${path}": ${errorText(error)}`);
281
+ throw error;
282
+ }
283
+ }));
189
284
  }
190
- function readRoleDefinitions(dirs) {
191
- return Object.fromEntries(dirs.flatMap((dir) => Object.entries(readAgentDefinitions(dir))));
192
- }
193
- export function loadAgentDefinitions(cwd, agentDir = getAgentDir(), projectTrusted = true) {
194
- return deepFreeze({ ...readRoleDefinitions(workflowRoleDirectories(agentDir)), ...(projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(cwd, ".pi"))) : {}) });
285
+ export function loadAgentDefinitions(cwd, agentDir = getAgentDir(), projectTrusted = true, extensionRoleDirectories = registeredWorkflowRoleDirectoryRegistrations()) {
286
+ return deepFreeze({ ...readRoleDefinitions(extensionRoleDirectories, true), ...readRoleDefinitions(workflowRoleDirectories(agentDir)), ...(projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(cwd, ".pi"))) : {}) });
195
287
  }
196
288
  function validateRolePolicies(definitions, roles, availableModels, rootTools, aliases = {}, knownModels = availableModels, settingsPath) {
197
289
  for (const role of roles) {
@@ -309,6 +401,39 @@ function workflowCallsWithStructure(program) {
309
401
  visit(program, { execution: "sequential", structure: [] });
310
402
  return calls.sort((left, right) => left.call.start - right.call.start);
311
403
  }
404
+ function memberCall(node, objectName, propertyName) {
405
+ if (node?.type !== "CallExpression" || node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.object.type !== "Identifier" || node.callee.object.name !== objectName || node.callee.property.type !== "Identifier")
406
+ return false;
407
+ return node.callee.property.name === propertyName;
408
+ }
409
+ function mapCallback(node) {
410
+ if (!memberCall(node, "Promise", "all") && !memberCall(node, "Promise", "allSettled"))
411
+ return undefined;
412
+ if (node.type !== "CallExpression")
413
+ return undefined;
414
+ const source = node.arguments[0];
415
+ if (source?.type !== "CallExpression" || source.callee.type !== "MemberExpression" || source.callee.computed || source.callee.property.type !== "Identifier" || !["map", "flatMap"].includes(source.callee.property.name))
416
+ return undefined;
417
+ const callback = source.arguments[0];
418
+ return callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression" ? callback : undefined;
419
+ }
420
+ function hasUnscopedAgent(node, scoped = false) {
421
+ const operation = workflowCallKind(node);
422
+ if (operation === "agent")
423
+ return !scoped;
424
+ const nestedScope = scoped || operation === "parallel" || operation === "pipeline";
425
+ return astChildren(node).some((child) => hasUnscopedAgent(child, nestedScope));
426
+ }
427
+ function validateObviousConcurrentAgentCalls(program) {
428
+ const visit = (node) => {
429
+ const callback = mapCallback(node);
430
+ if (callback && hasUnscopedAgent(callback))
431
+ fail("INVALID_METADATA", "Promise.all/map agent fan-out cannot prove stable call-site identity; use parallel(...) or pipeline(...)");
432
+ for (const child of astChildren(node))
433
+ visit(child);
434
+ };
435
+ visit(program);
436
+ }
312
437
  function validateDirectPrimitiveReferences(program, name) {
313
438
  const visit = (node, parent) => {
314
439
  if (node.type === "Identifier" && node.name === name) {
@@ -639,6 +764,7 @@ export function preflight(script, capabilities, schemas = [], metadata = { name:
639
764
  for (const [index, schema] of schemas.entries())
640
765
  validateSchema(schema, `schema[${String(index)}]`);
641
766
  const calls = workflowCalls(program);
767
+ validateObviousConcurrentAgentCalls(program);
642
768
  const phases = calls.filter((call) => call.callee.name === "phase").map((call) => literalString(call.arguments[0])).filter((phase) => phase !== undefined);
643
769
  for (const call of calls) {
644
770
  const operation = call.callee.name;
@@ -709,7 +835,7 @@ export function validateWorkflowLaunchWithRegistry(params, context, registry) {
709
835
  if (!script)
710
836
  fail("INVALID_SYNTAX", "Provide script or registered function");
711
837
  const metadata = validateWorkflowMetadata({ name: workflowName, ...(typeof params.description === "string" ? { description: params.description } : fn?.description ? { description: fn.description } : {}) });
712
- const globalAgentDefinitions = loadAgentDefinitions(context.cwd, context.agentDir, false);
838
+ const globalAgentDefinitions = loadAgentDefinitions(context.cwd, context.agentDir, false, registry && typeof registry.roleDirectoryRegistrations === "function" ? registry.roleDirectoryRegistrations() : registry && typeof registry.roleDirectories === "function" ? registry.roleDirectories() : undefined);
713
839
  const projectAgentDefinitions = context.projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(context.cwd, ".pi"))) : {};
714
840
  const agentDefinitions = deepFreeze({ ...globalAgentDefinitions, ...projectAgentDefinitions });
715
841
  const aliases = context.modelAliases ?? {};
@@ -0,0 +1,13 @@
1
+ import type { JsonValue } from "./types.js";
2
+ export interface WorkflowArtifact {
3
+ extension: ".js" | ".json" | ".md";
4
+ content: string;
5
+ }
6
+ export type WorkflowTui = {
7
+ stop(): void;
8
+ start(): void;
9
+ requestRender(force?: boolean): void;
10
+ };
11
+ export declare function workflowScriptArtifact(script: string): WorkflowArtifact;
12
+ export declare function workflowResultArtifact(value: JsonValue): WorkflowArtifact;
13
+ export declare function openWorkflowArtifact(tui: WorkflowTui, command: string, artifact: WorkflowArtifact): Promise<number | null>;