pi-extensible-workflows 3.0.0 → 3.1.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.
@@ -6,9 +6,11 @@ export * from "./registry.js";
6
6
  export * from "./execution.js";
7
7
  export * from "./host.js";
8
8
  export { default } from "./host.js";
9
- export { acquireSessionLease, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
9
+ export { acquireSessionLease, hasLiveSessionLease, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
10
10
  export type { AwaitingCheckpoint, CompletedOperation, NativeSessionReference, PendingWorkflowDecision, PersistedOwnershipNode, PersistedRun, WorktreeReference } from "./persistence.js";
11
11
  export { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
12
- export type { AgentAttempt, AgentBudgetHooks, AgentExecutionOptions, AgentExecutionResult, AgentExecutionRoot, AgentProgress, AgentProviderFailure, AgentProviderRecovery, AgentToolCallProgress, SessionInput } from "./agent-execution.js";
12
+ export type { AgentAttempt, AgentBudgetHooks, AgentExecutionOptions, AgentExecutionResult, AgentExecutionRoot, AgentProgress, AgentProviderFailure, AgentProviderRecovery, AgentToolCallProgress, AgentSetup, AgentSetupContext, AgentSetupHook, NativeSession, RegisteredAgentSetupHook, SessionFactory, SessionInput } from "./agent-execution.js";
13
13
  export { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
14
14
  export type { DoctorDiagnostic, DoctorFunction, DoctorOptions, DoctorPiState, DoctorReport, DoctorRole, DoctorSeverity, DoctorTrust } from "./doctor.js";
15
+ export { doctorCleanup, doctorCleanupExitCode, formatDoctorCleanupReport } from "./doctor-cleanup.js";
16
+ export type { CleanupFailure, CleanupRunResult, CleanupSessionReport, DoctorCleanupOptions, DoctorCleanupReport } from "./doctor-cleanup.js";
package/dist/src/index.js CHANGED
@@ -6,6 +6,7 @@ export * from "./registry.js";
6
6
  export * from "./execution.js";
7
7
  export * from "./host.js";
8
8
  export { default } from "./host.js";
9
- export { acquireSessionLease, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
9
+ export { acquireSessionLease, hasLiveSessionLease, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
10
10
  export { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
11
11
  export { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
12
+ export { doctorCleanup, doctorCleanupExitCode, formatDoctorCleanupReport } from "./doctor-cleanup.js";
@@ -39,7 +39,9 @@ export interface BorrowedWorktreeBinding {
39
39
  owner: string;
40
40
  }
41
41
  export declare function projectStorageKey(cwd: string): string;
42
+ export declare function projectSessionsDirectory(cwd: string, home?: string): string;
42
43
  export declare function runsDirectory(cwd: string, sessionId: string, home?: string): string;
44
+ export declare function hasLiveSessionLease(cwd: string, sessionId: string, home?: string): Promise<boolean>;
43
45
  export declare class SessionLease {
44
46
  #private;
45
47
  readonly path: string;
@@ -112,6 +114,7 @@ export declare class RunStore {
112
114
  sourceRunId: string;
113
115
  owner: string;
114
116
  }>;
117
+ validateDeletionWorktrees(): Promise<void>;
115
118
  validateBorrowedWorktrees(): Promise<void>;
116
119
  validateNamedWorktrees(): Promise<void>;
117
120
  ownsWorktree(owner: string): Promise<boolean>;
@@ -18,8 +18,11 @@ export function projectStorageKey(cwd) {
18
18
  const slug = safePart(basename(exact)) || "root";
19
19
  return `${slug}-${createHash("sha256").update(exact).digest("hex").slice(0, 12)}`;
20
20
  }
21
+ export function projectSessionsDirectory(cwd, home = homedir()) {
22
+ return join(home, ".pi", "workflows", "projects", projectStorageKey(cwd), "sessions");
23
+ }
21
24
  export function runsDirectory(cwd, sessionId, home = homedir()) {
22
- return join(home, ".pi", "workflows", "projects", projectStorageKey(cwd), "sessions", safePart(sessionId), "runs");
25
+ return join(projectSessionsDirectory(cwd, home), safePart(sessionId), "runs");
23
26
  }
24
27
  const SESSION_OWNER_FILE = "owner.json";
25
28
  const SESSION_OWNER_WRITE_GRACE_MS = 30_000;
@@ -43,6 +46,24 @@ async function processAlive(pid, startedAt) {
43
46
  }
44
47
  return true;
45
48
  }
49
+ export async function hasLiveSessionLease(cwd, sessionId, home = homedir()) {
50
+ const path = join(runsDirectory(cwd, sessionId, home), SESSION_OWNER_FILE);
51
+ let owner;
52
+ try {
53
+ owner = JSON.parse(await readFile(path, "utf8"));
54
+ }
55
+ catch (error) {
56
+ if (error.code === "ENOENT")
57
+ return false;
58
+ throw error;
59
+ }
60
+ if (!owner || typeof owner !== "object" || Array.isArray(owner))
61
+ throw new WorkflowError("RUN_OWNED", `Pi session ${sessionId} has an invalid ownership lease`);
62
+ const candidate = owner;
63
+ if (typeof candidate.pid !== "number" || !Number.isInteger(candidate.pid) || candidate.pid < 1 || typeof candidate.token !== "string" || !candidate.token || typeof candidate.startedAt !== "number" || !Number.isFinite(candidate.startedAt))
64
+ throw new WorkflowError("RUN_OWNED", `Pi session ${sessionId} has an invalid ownership lease`);
65
+ return processAlive(candidate.pid, candidate.startedAt);
66
+ }
46
67
  function sameOwner(left, right) {
47
68
  if (!left || typeof left !== "object" || !right || typeof right !== "object")
48
69
  return false;
@@ -587,6 +608,33 @@ export class RunStore {
587
608
  throw new WorkflowError("WORKTREE_FAILED", `Missing named worktree ${name}`);
588
609
  return resolved;
589
610
  }
611
+ async validateDeletionWorktrees() {
612
+ try {
613
+ const records = await json(join(this.directory, "worktrees.json"));
614
+ if (!Array.isArray(records))
615
+ throw new Error("Worktree records are invalid");
616
+ const owners = new Set();
617
+ const paths = new Set();
618
+ records.forEach((record) => {
619
+ if (!record || typeof record !== "object" || typeof record.owner !== "string")
620
+ throw new Error("Invalid worktree record");
621
+ const owner = record.owner;
622
+ if (owners.has(owner))
623
+ throw new Error(`Duplicate worktree record for ${owner}`);
624
+ owners.add(owner);
625
+ const reference = this.structuralWorktree(owner, record);
626
+ paths.add(resolve(reference.path));
627
+ });
628
+ const entries = await readdir(join(this.directory, "worktrees"), { withFileTypes: true }).catch((error) => { if (error.code === "ENOENT")
629
+ return []; throw error; });
630
+ for (const entry of entries)
631
+ if (!entry.isDirectory() || entry.isSymbolicLink() || !paths.has(resolve(join(this.directory, "worktrees", entry.name))))
632
+ throw new Error(`Unrecorded worktree artifact: ${join(this.directory, "worktrees", entry.name)}`);
633
+ }
634
+ catch (error) {
635
+ throw new WorkflowError("WORKTREE_FAILED", error instanceof Error ? error.message : String(error));
636
+ }
637
+ }
590
638
  async validateBorrowedWorktrees() {
591
639
  try {
592
640
  const loaded = await this.load();
@@ -1,4 +1,4 @@
1
- import type { JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogVariable, WorkflowExtension, WorkflowFunction, WorkflowFunctionContext, WorkflowJournal, WorkflowVariable } from "./types.js";
1
+ import type { JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowCatalogVariable, WorkflowExtension, WorkflowFunction, WorkflowFunctionContext, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext, WorkflowRoleDirectoryRegistration, WorkflowVariable } from "./types.js";
2
2
  export declare class WorkflowRegistry {
3
3
  #private;
4
4
  get frozen(): boolean;
@@ -6,9 +6,9 @@ export declare class WorkflowRegistry {
6
6
  register(extension: WorkflowExtension): void;
7
7
  function(name: string): WorkflowFunction;
8
8
  functions(): Readonly<Record<string, WorkflowFunction>>;
9
- catalog(): WorkflowCatalog;
10
- catalogIndex(): WorkflowCatalogIndex;
11
- catalogDetail(name: string): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogError;
9
+ catalog(context?: WorkflowCatalogContext): WorkflowCatalog;
10
+ catalogIndex(context?: WorkflowCatalogContext): WorkflowCatalogIndex;
11
+ catalogDetail(name: string, context?: WorkflowCatalogContext): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogModelAlias | WorkflowCatalogError;
12
12
  globals(): Readonly<Record<string, {
13
13
  name: string;
14
14
  }>>;
@@ -18,14 +18,26 @@ export declare class WorkflowRegistry {
18
18
  variable: WorkflowVariable;
19
19
  }[];
20
20
  agentSetupHooks(): readonly RegisteredAgentSetupHook[];
21
+ roleDirectories(): readonly string[];
22
+ roleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[];
23
+ modelAliases(): readonly {
24
+ name: string;
25
+ version: string;
26
+ headline: string;
27
+ extensionDescription: string;
28
+ resolve: WorkflowModelAlias["resolve"];
29
+ }[];
30
+ resolveModelAliases(context: Readonly<WorkflowModelAliasResolverContext>, shadowed?: ReadonlySet<string>): Promise<Readonly<Record<string, string>>>;
21
31
  }
22
- export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "agentSetupHooks">;
32
+ export type WorkflowRegistryApi = Pick<WorkflowRegistry, "frozen" | "freeze" | "register" | "function" | "functions" | "catalog" | "catalogIndex" | "catalogDetail" | "globals" | "invokeFunction" | "variables" | "modelAliases" | "resolveModelAliases" | "agentSetupHooks" | "roleDirectories" | "roleDirectoryRegistrations">;
23
33
  export declare function resetWorkflowRegistry(): void;
24
34
  export declare function beginWorkflowExtensionLoading(): void;
25
35
  export declare function loadingRegistry(): WorkflowRegistryApi;
26
36
  export declare function registerWorkflowExtension(extension: WorkflowExtension): void;
27
- export declare function workflowCatalog(): WorkflowCatalog;
28
- export declare function workflowCatalogIndex(): WorkflowCatalogIndex;
29
- export declare function workflowCatalogDetail(name: string): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogError;
37
+ export declare function workflowCatalog(context?: WorkflowCatalogContext): WorkflowCatalog;
38
+ export declare function workflowCatalogIndex(context?: WorkflowCatalogContext): WorkflowCatalogIndex;
39
+ export declare function workflowCatalogDetail(name: string, context?: WorkflowCatalogContext): WorkflowCatalogFunction | WorkflowCatalogVariable | WorkflowCatalogModelAlias | WorkflowCatalogError;
30
40
  export declare function registeredWorkflowFunctions(): Readonly<Record<string, WorkflowFunction>>;
31
- export type { WorkflowCatalog, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogIndexFunction, WorkflowCatalogIndexVariable, WorkflowCatalogVariable } from "./types.js";
41
+ export declare function registeredWorkflowRoleDirectories(): readonly string[];
42
+ export declare function registeredWorkflowRoleDirectoryRegistrations(): readonly WorkflowRoleDirectoryRegistration[];
43
+ export type { WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogIndexFunction, WorkflowCatalogIndexVariable, WorkflowCatalogModelAlias, WorkflowCatalogSettings, WorkflowCatalogVariable, WorkflowRoleDirectoryRegistration } from "./types.js";
@@ -1,13 +1,36 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { fileURLToPath } from "node:url";
1
3
  import { Value } from "typebox/value";
2
4
  import { deepFreeze, fail, jsonValue, object } from "./utils.js";
3
- import { loadSettings, validateSchema } from "./validation.js";
5
+ import { loadSettings, resolveWorkflowSettings, validateSchema } from "./validation.js";
4
6
  const RESERVED_GLOBALS = new Set(["agent", "shell", "prompt", "checkpoint", "parallel", "pipeline", "phase", "withWorktree", "log", "args", "Promise", "JSON", "Math", "Date", "eval", "Function", "WebAssembly", "process", "require", "module", "exports", "console", "fetch", "XMLHttpRequest", "WebSocket", "performance", "crypto", "setTimeout", "setInterval", "setImmediate", "queueMicrotask", "Intl", "SharedArrayBuffer", "Atomics", "globalThis", "global", "undefined", "NaN", "Infinity", "extensions", "workflow_catalog"]);
5
7
  const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
6
8
  const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
9
+ function normalizeRoleDirectory(value) {
10
+ try {
11
+ if (value instanceof URL) {
12
+ if (value.protocol !== "file:")
13
+ fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
14
+ return fileURLToPath(value);
15
+ }
16
+ if (typeof value === "string") {
17
+ if (value.startsWith("file:"))
18
+ return fileURLToPath(value);
19
+ if (isAbsolute(value))
20
+ return value;
21
+ }
22
+ }
23
+ catch (error) {
24
+ fail("INVALID_METADATA", `Invalid workflow role directory: ${error instanceof Error ? error.message : String(error)}`);
25
+ }
26
+ fail("INVALID_METADATA", "Workflow role directories require file URLs or absolute filesystem paths");
27
+ }
7
28
  export class WorkflowRegistry {
8
29
  #extensions = new Set();
9
30
  #globals = new Map();
10
31
  #hooks = new Map();
32
+ #roleDirectories = new Map();
33
+ #modelAliases = new Map();
11
34
  #frozen = false;
12
35
  get frozen() { return this.#frozen; }
13
36
  freeze() { this.#frozen = true; }
@@ -16,13 +39,18 @@ export class WorkflowRegistry {
16
39
  fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
17
40
  if (object(extension) && Object.prototype.hasOwnProperty.call(extension, "workflows"))
18
41
  fail("INVALID_METADATA", "Separate registered workflow definitions were removed; register a function with input and output schemas instead");
19
- if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "functions", "variables", "agentSetupHooks"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim() || typeof extension.description !== "string" || !extension.description.trim())
42
+ if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "functions", "variables", "modelAliases", "agentSetupHooks", "roleDirectories"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim() || typeof extension.description !== "string" || !extension.description.trim())
20
43
  fail("INVALID_METADATA", "Workflow extensions require a semantic version, headline, and description");
21
44
  const functions = extension.functions ?? {};
22
45
  const variables = extension.variables ?? {};
46
+ const modelAliases = extension.modelAliases ?? {};
23
47
  const agentSetupHooks = extension.agentSetupHooks ?? {};
24
- if (!object(functions) || !object(variables) || !object(agentSetupHooks) || (Object.keys(functions).length === 0 && Object.keys(variables).length === 0 && Object.keys(agentSetupHooks).length === 0))
25
- fail("INVALID_METADATA", "Workflow extensions require functions, variables, or agent setup hooks");
48
+ const roleDirectoryValues = extension.roleDirectories === undefined ? [] : extension.roleDirectories;
49
+ if (!Array.isArray(roleDirectoryValues))
50
+ fail("INVALID_METADATA", "Workflow extension roleDirectories must be an array");
51
+ const roleDirectories = [...new Set(Array.from(roleDirectoryValues, (value) => normalizeRoleDirectory(value)))];
52
+ if (!object(functions) || !object(variables) || !object(modelAliases) || !object(agentSetupHooks) || (Object.keys(functions).length === 0 && Object.keys(variables).length === 0 && Object.keys(modelAliases).length === 0 && Object.keys(agentSetupHooks).length === 0 && roleDirectories.length === 0))
53
+ fail("INVALID_METADATA", "Workflow extensions require functions, variables, model aliases, agent setup hooks, or role directories");
26
54
  const names = [...Object.keys(functions), ...Object.keys(variables)];
27
55
  if (new Set(names).size !== names.length)
28
56
  fail("GLOBAL_COLLISION", "Global name collision inside extension");
@@ -47,16 +75,29 @@ export class WorkflowRegistry {
47
75
  fail("INVALID_METADATA", `Invalid workflow variable: ${name}`);
48
76
  validateSchema(variable.schema, `${name} schema`);
49
77
  }
78
+ for (const [name, alias] of Object.entries(modelAliases)) {
79
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name))
80
+ fail("INVALID_METADATA", `Invalid model alias name: ${name}`);
81
+ if (!object(alias) || Object.keys(alias).some((key) => key !== "resolve") || typeof alias.resolve !== "function")
82
+ fail("INVALID_METADATA", `Invalid model alias resolver: ${name}`);
83
+ if (this.#modelAliases.has(name))
84
+ fail("DUPLICATE_NAME", `Model alias already registered: ${name}`);
85
+ }
50
86
  for (const [name, hook] of Object.entries(agentSetupHooks)) {
51
87
  if (!IDENTIFIER.test(name) || !object(hook) || Object.keys(hook).some((key) => !["priority", "setup"].includes(key)) || typeof hook.setup !== "function" || hook.priority !== undefined && (typeof hook.priority !== "number" || !Number.isFinite(hook.priority)))
52
88
  fail("INVALID_METADATA", `Invalid agent setup hook: ${name}`);
53
89
  if (this.#hooks.has(name))
54
90
  fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
55
91
  }
56
- const stored = deepFreeze({ ...extension, functions, variables, agentSetupHooks });
92
+ const stored = deepFreeze({ ...extension, functions, variables, modelAliases, agentSetupHooks, ...(roleDirectories.length ? { roleDirectories } : {}) });
57
93
  this.#extensions.add(stored);
94
+ for (const directory of roleDirectories)
95
+ if (!this.#roleDirectories.has(directory))
96
+ this.#roleDirectories.set(directory, deepFreeze({ path: directory, extension: { version: extension.version, headline: extension.headline, description: extension.description } }));
58
97
  for (const name of names)
59
98
  this.#globals.set(name, name);
99
+ for (const [name, alias] of Object.entries(modelAliases))
100
+ this.#modelAliases.set(name, { name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, resolve: alias.resolve });
60
101
  for (const [name, hook] of Object.entries(agentSetupHooks))
61
102
  this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
62
103
  }
@@ -71,7 +112,7 @@ export class WorkflowRegistry {
71
112
  functions() {
72
113
  return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.entries(extension.functions ?? {}))));
73
114
  }
74
- catalog() {
115
+ catalog(context) {
75
116
  const functions = [];
76
117
  const variables = [];
77
118
  for (const extension of this.#extensions) {
@@ -81,28 +122,53 @@ export class WorkflowRegistry {
81
122
  variables.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: variable.description, schema: structuredClone(variable.schema) });
82
123
  }
83
124
  let aliases;
125
+ let settings;
126
+ let source = "global settings";
84
127
  try {
85
- aliases = loadSettings().modelAliases;
128
+ const resolved = context ? resolveWorkflowSettings(context.cwd, context.projectTrusted, context.globalSettingsPath) : undefined;
129
+ aliases = resolved?.effective.modelAliases ?? loadSettings().modelAliases;
130
+ if (resolved) {
131
+ source = resolved.projectTrusted && resolved.sources.modelAliases === resolved.projectSettingsPath ? "trusted project settings" : "global settings";
132
+ settings = { concurrency: resolved.effective.concurrency, modelAliases: resolved.effective.modelAliases ?? {}, disabledAgentResources: resolved.effective.disabledAgentResources ?? { skills: [], extensions: [] }, globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted: resolved.projectTrusted, sources: resolved.sources };
133
+ }
86
134
  }
87
135
  catch {
88
136
  aliases = undefined;
89
137
  }
138
+ const staticEntries = Object.keys(aliases ?? {}).map((name) => ({ name, kind: "static", provenance: source }));
139
+ const dynamicEntries = [...this.#modelAliases.values()].map(({ name, version, headline, extensionDescription }) => ({ name, kind: "dynamic", provenance: `extension: ${headline}`, version, headline, extensionDescription }));
140
+ const modelAliasEntries = [...staticEntries, ...dynamicEntries].sort((left, right) => left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind) || left.provenance.localeCompare(right.provenance));
90
141
  const sort = (left, right) => left.name.localeCompare(right.name);
91
- return deepFreeze({ functions: functions.sort(sort), variables: variables.sort(sort), ...(aliases ? { modelAliases: structuredClone(aliases) } : {}) });
142
+ const catalog = { functions: functions.sort(sort), variables: variables.sort(sort), ...(modelAliasEntries.length ? { modelAliasEntries } : {}) };
143
+ if (aliases && Object.keys(aliases).length)
144
+ Object.defineProperty(catalog, "modelAliases", { value: Object.freeze(structuredClone(aliases)), enumerable: false });
145
+ if (settings) {
146
+ const publicSettings = { ...settings, modelAliases: Object.keys(aliases ?? {}).length ? {} : settings.modelAliases };
147
+ Object.defineProperty(catalog, "settings", { value: deepFreeze(publicSettings), enumerable: true });
148
+ }
149
+ return deepFreeze(catalog);
92
150
  }
93
- catalogIndex() {
94
- const catalog = this.catalog();
95
- return deepFreeze({
151
+ catalogIndex(context) {
152
+ const catalog = this.catalog(context);
153
+ const index = {
96
154
  functions: catalog.functions.map(({ name, description, input }) => ({ name, description, input: structuredClone(input) })),
97
155
  variables: catalog.variables.map(({ name, description, schema }) => ({ name, description, schema: structuredClone(schema) })),
98
- ...(catalog.modelAliases ? { modelAliases: structuredClone(catalog.modelAliases) } : {}),
99
- });
156
+ ...(catalog.modelAliasEntries ? { modelAliasEntries: structuredClone(catalog.modelAliasEntries) } : {}),
157
+ };
158
+ if (catalog.modelAliases)
159
+ Object.defineProperty(index, "modelAliases", { value: Object.freeze(structuredClone(catalog.modelAliases)), enumerable: false });
160
+ if (catalog.settings)
161
+ Object.defineProperty(index, "settings", { value: deepFreeze(structuredClone(catalog.settings)), enumerable: true });
162
+ return deepFreeze(index);
100
163
  }
101
- catalogDetail(name) {
102
- const catalog = this.catalog();
164
+ catalogDetail(name, context) {
165
+ const catalog = this.catalog(context);
103
166
  const entry = catalog.functions.find((candidate) => candidate.name === name) ?? catalog.variables.find((candidate) => candidate.name === name);
104
167
  if (entry)
105
168
  return entry;
169
+ const alias = catalog.modelAliasEntries?.find((candidate) => candidate.name === name);
170
+ if (alias)
171
+ return alias;
106
172
  return deepFreeze({ error: { code: "NOT_FOUND", name, message: `No registered workflow function or variable is available: ${name}` } });
107
173
  }
108
174
  globals() {
@@ -131,6 +197,38 @@ export class WorkflowRegistry {
131
197
  agentSetupHooks() {
132
198
  return [...this.#hooks.values()].sort((left, right) => left.priority - right.priority || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
133
199
  }
200
+ roleDirectories() {
201
+ return [...this.#roleDirectories.keys()];
202
+ }
203
+ roleDirectoryRegistrations() {
204
+ return [...this.#roleDirectories.values()];
205
+ }
206
+ modelAliases() { return [...this.#modelAliases.values()].sort((left, right) => left.name.localeCompare(right.name)); }
207
+ async resolveModelAliases(context, shadowed = new Set()) {
208
+ const resolved = {};
209
+ const isAborted = () => context.signal.aborted;
210
+ for (const alias of this.modelAliases()) {
211
+ if (shadowed.has(alias.name))
212
+ continue;
213
+ if (isAborted())
214
+ fail("CANCELLED", `Model alias resolver cancelled: ${alias.name} (${alias.headline})`);
215
+ let target;
216
+ try {
217
+ target = await alias.resolve(Object.freeze({ cwd: context.cwd, projectTrusted: context.projectTrusted, rootModel: Object.freeze({ ...context.rootModel }), knownModels: new Set(context.knownModels), availableModels: new Set(context.availableModels), signal: context.signal }));
218
+ }
219
+ catch (error) {
220
+ if (isAborted() || error instanceof Error && error.name === "AbortError" || typeof error === "object" && error !== null && !Array.isArray(error) && error.code === "CANCELLED")
221
+ fail("CANCELLED", `Model alias resolver cancelled: ${alias.name} (${alias.headline})`);
222
+ fail("CONFIG_ERROR", `Model alias resolver failed for ${alias.name} (${alias.headline}): ${error instanceof Error ? error.message : String(error)}`);
223
+ }
224
+ if (isAborted())
225
+ fail("CANCELLED", `Model alias resolver cancelled: ${alias.name} (${alias.headline})`);
226
+ if (typeof target !== "string" || !target.trim())
227
+ fail("CONFIG_ERROR", `Model alias resolver returned an invalid target for ${alias.name} (${alias.headline})`);
228
+ resolved[alias.name] = target.trim();
229
+ }
230
+ return Object.freeze(resolved);
231
+ }
134
232
  }
135
233
  const WORKFLOW_REGISTRY_KEY = Symbol.for("pi-extensible-workflows.workflow-registry");
136
234
  const globalRegistry = globalThis;
@@ -141,12 +239,16 @@ function createWorkflowRegistryApi(registry) {
141
239
  register: (extension) => { registry.register(extension); },
142
240
  function: (name) => registry.function(name),
143
241
  functions: () => registry.functions(),
144
- catalog: () => registry.catalog(),
145
- catalogIndex: () => registry.catalogIndex(),
146
- catalogDetail: (name) => registry.catalogDetail(name),
242
+ catalog: (context) => registry.catalog(context),
243
+ catalogIndex: (context) => registry.catalogIndex(context),
244
+ catalogDetail: (name, context) => registry.catalogDetail(name, context),
147
245
  globals: () => registry.globals(),
148
246
  invokeFunction: (...args) => registry.invokeFunction(...args),
149
247
  variables: () => registry.variables(),
248
+ modelAliases: () => registry.modelAliases(),
249
+ resolveModelAliases: (...args) => registry.resolveModelAliases(...args),
250
+ roleDirectories: () => registry.roleDirectories(),
251
+ roleDirectoryRegistrations: () => registry.roleDirectoryRegistrations(),
150
252
  agentSetupHooks: () => registry.agentSetupHooks(),
151
253
  };
152
254
  }
@@ -163,7 +265,15 @@ export function beginWorkflowExtensionLoading() {
163
265
  export function loadingRegistry() { return workflowRegistryHost().api; }
164
266
  beginWorkflowExtensionLoading();
165
267
  export function registerWorkflowExtension(extension) { loadingRegistry().register(extension); }
166
- export function workflowCatalog() { return loadingRegistry().catalog(); }
167
- export function workflowCatalogIndex() { return loadingRegistry().catalogIndex(); }
168
- export function workflowCatalogDetail(name) { return loadingRegistry().catalogDetail(name); }
268
+ export function workflowCatalog(context) { return loadingRegistry().catalog(context); }
269
+ export function workflowCatalogIndex(context) { return loadingRegistry().catalogIndex(context); }
270
+ export function workflowCatalogDetail(name, context) { return loadingRegistry().catalogDetail(name, context); }
169
271
  export function registeredWorkflowFunctions() { return loadingRegistry().functions(); }
272
+ export function registeredWorkflowRoleDirectories() {
273
+ const directories = loadingRegistry().roleDirectories;
274
+ return typeof directories === "function" ? directories() : [];
275
+ }
276
+ export function registeredWorkflowRoleDirectoryRegistrations() {
277
+ const registrations = loadingRegistry().roleDirectoryRegistrations;
278
+ return typeof registrations === "function" ? registrations() : [];
279
+ }
@@ -296,8 +296,10 @@ function detailLines(workflow) {
296
296
  `Hooks: ${attempt.setup.hookNames.join(", ") || "(none)"}`,
297
297
  `Effective: model=${attempt.setup.model.provider}/${attempt.setup.model.model}${attempt.setup.model.thinking ? `:${attempt.setup.model.thinking}` : ""} tools=${attempt.setup.tools.join(",") || "(none)"} cwd=${attempt.setup.cwd}`,
298
298
  ...(attempt.setup.disabledAgentResources ? [
299
- `Disabled skills: ${attempt.setup.disabledAgentResources.skills.join(", ") || "(none)"}`,
300
- `Disabled extensions: ${attempt.setup.disabledAgentResources.extensions.join(", ") || "(none)"}`,
299
+ `Configured skill patterns: ${attempt.setup.disabledAgentResources.skills.join(", ") || "(none)"}`,
300
+ `Excluded skills: ${attempt.setup.disabledAgentResources.excludedSkills?.join(", ") || "(none)"}`,
301
+ `Configured extension patterns: ${attempt.setup.disabledAgentResources.extensions.join(", ") || "(none)"}`,
302
+ `Excluded extensions: ${attempt.setup.disabledAgentResources.excludedExtensions?.join(", ") || "(none)"}`,
301
303
  `Unmatched skills: ${attempt.setup.disabledAgentResources.unmatchedSkills.join(", ") || "(none)"}`,
302
304
  `Unmatched extensions: ${attempt.setup.disabledAgentResources.unmatchedExtensions.join(", ") || "(none)"}`,
303
305
  ] : []),
@@ -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
  };
@@ -294,9 +329,11 @@ export interface LaunchSnapshot {
294
329
  args: JsonValue;
295
330
  metadata: WorkflowMetadata;
296
331
  settings: WorkflowSettings;
332
+ settingsSources?: WorkflowSettingsSources;
297
333
  budget?: WorkflowBudget;
298
334
  settingsPath?: string;
299
335
  modelAliases?: Readonly<Record<string, string>>;
336
+ phases?: readonly string[];
300
337
  models: readonly string[];
301
338
  tools: readonly string[];
302
339
  agentTypes: readonly string[];
@@ -359,14 +396,69 @@ export interface WorkflowVariable {
359
396
  schema: JsonSchema;
360
397
  resolve: (run: Readonly<WorkflowRunContext>) => Promise<JsonValue> | JsonValue;
361
398
  }
399
+ type NativeAgentMessage = {
400
+ role: string;
401
+ content?: unknown;
402
+ stopReason?: string;
403
+ errorMessage?: string;
404
+ usage?: {
405
+ input: number;
406
+ output: number;
407
+ cacheRead: number;
408
+ cacheWrite: number;
409
+ cost: {
410
+ total: number;
411
+ };
412
+ };
413
+ };
414
+ type NativeSessionStats = Pick<SessionStats, "tokens" | "cost">;
415
+ export interface NativeSession {
416
+ readonly sessionId: string;
417
+ readonly sessionFile: string | undefined;
418
+ readonly messages: readonly NativeAgentMessage[];
419
+ getSessionStats(): NativeSessionStats;
420
+ readonly systemPrompt?: string;
421
+ readonly model?: {
422
+ provider: string;
423
+ model?: string;
424
+ id?: string;
425
+ };
426
+ readonly agent?: {
427
+ state: {
428
+ tools: readonly {
429
+ name: string;
430
+ }[];
431
+ };
432
+ };
433
+ getLeafId?: () => string | null;
434
+ getToolDefinitions?: () => unknown;
435
+ subscribe?(listener: (event: AgentSessionEvent) => void): () => void;
436
+ prompt(text: string): Promise<void>;
437
+ steer?(text: string): Promise<void>;
438
+ abort?(): Promise<void>;
439
+ dispose(): void;
440
+ }
441
+ type SessionTools = NonNullable<CreateAgentSessionOptions["tools"]>;
442
+ type SessionCustomTools = NonNullable<CreateAgentSessionOptions["customTools"]>;
443
+ export interface SessionInput {
444
+ cwd: string;
445
+ model: ModelSpec;
446
+ tools: SessionTools;
447
+ sessionLabel: string;
448
+ agentDir?: string;
449
+ customTools?: SessionCustomTools;
450
+ resultTool?: ToolDefinition;
451
+ systemPromptAppend?: string;
452
+ extensionFactories?: InlineExtension[];
453
+ resourcePolicy?: AgentResourcePolicy;
454
+ options?: AgentOptions;
455
+ }
456
+ export type SessionFactory = (input: SessionInput) => Promise<NativeSession>;
362
457
  export interface AgentSetup {
363
458
  prompt: string;
364
- options: Record<string, JsonValue>;
365
- sessionInput: {
366
- extensionFactories?: unknown[];
367
- [key: string]: unknown;
368
- };
369
- createSession: unknown;
459
+ options: AgentOptions;
460
+ sessionInput: SessionInput;
461
+ createSession: SessionFactory;
370
462
  }
371
463
  export interface AgentSetupContext {
372
464
  readonly run: Readonly<WorkflowRunContext>;
@@ -383,13 +475,21 @@ export interface RegisteredAgentSetupHook {
383
475
  priority: number;
384
476
  setup: AgentSetupHook["setup"];
385
477
  }
386
- export interface WorkflowExtension {
478
+ export interface WorkflowExtensionMetadata {
387
479
  version: string;
388
480
  headline: string;
389
481
  description: string;
482
+ }
483
+ export interface WorkflowRoleDirectoryRegistration {
484
+ path: string;
485
+ extension: WorkflowExtensionMetadata;
486
+ }
487
+ export interface WorkflowExtension extends WorkflowExtensionMetadata {
390
488
  functions?: Readonly<Record<string, WorkflowFunction>>;
391
489
  variables?: Readonly<Record<string, WorkflowVariable>>;
490
+ modelAliases?: Readonly<Record<string, WorkflowModelAlias>>;
392
491
  agentSetupHooks?: Readonly<Record<string, AgentSetupHook>>;
492
+ roleDirectories?: readonly (string | URL)[];
393
493
  }
394
494
  export interface WorkflowJournal {
395
495
  get(path: string): JsonValue | undefined;
@@ -511,10 +611,34 @@ export interface WorkflowCatalogVariable {
511
611
  description: string;
512
612
  schema: JsonSchema;
513
613
  }
614
+ export interface WorkflowCatalogModelAlias {
615
+ name: string;
616
+ kind: "static" | "dynamic";
617
+ provenance: string;
618
+ version?: string;
619
+ headline?: string;
620
+ extensionDescription?: string;
621
+ }
622
+ export interface WorkflowCatalogSettings {
623
+ concurrency: number;
624
+ modelAliases: Readonly<Record<string, string>>;
625
+ disabledAgentResources: AgentResourceExclusions;
626
+ globalSettingsPath: string;
627
+ projectSettingsPath: string;
628
+ projectTrusted: boolean;
629
+ sources: WorkflowSettingsSources;
630
+ }
631
+ export interface WorkflowCatalogContext {
632
+ cwd: string;
633
+ projectTrusted: boolean;
634
+ globalSettingsPath?: string;
635
+ }
514
636
  export interface WorkflowCatalog {
515
637
  functions: readonly WorkflowCatalogFunction[];
516
638
  variables: readonly WorkflowCatalogVariable[];
517
639
  modelAliases?: Readonly<Record<string, string>>;
640
+ modelAliasEntries?: readonly WorkflowCatalogModelAlias[];
641
+ settings?: WorkflowCatalogSettings;
518
642
  }
519
643
  export interface WorkflowCatalogIndexFunction {
520
644
  name: string;
@@ -530,6 +654,8 @@ export interface WorkflowCatalogIndex {
530
654
  functions: readonly WorkflowCatalogIndexFunction[];
531
655
  variables: readonly WorkflowCatalogIndexVariable[];
532
656
  modelAliases?: Readonly<Record<string, string>>;
657
+ modelAliasEntries?: readonly WorkflowCatalogModelAlias[];
658
+ settings?: WorkflowCatalogSettings;
533
659
  }
534
660
  export interface WorkflowCatalogError {
535
661
  error: {
@@ -563,3 +689,4 @@ export interface ValidatedWorkflowLaunch {
563
689
  roleNames: readonly string[];
564
690
  functionName?: string;
565
691
  }
692
+ export {};