@wichayutdew/pi-workflows 0.1.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 (45) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +752 -0
  3. package/agents/step.md +17 -0
  4. package/dist/index.js +4576 -0
  5. package/examples/mr-comments.workflow.yaml +115 -0
  6. package/examples/prompts/mr-comments/implement.md +8 -0
  7. package/examples/prompts/mr-comments/inspect.md +5 -0
  8. package/examples/prompts/mr-comments/plan.md +13 -0
  9. package/examples/prompts/mr-comments/verify.md +7 -0
  10. package/examples/settings.yaml +19 -0
  11. package/package.json +81 -0
  12. package/schemas/settings.schema.json +22 -0
  13. package/schemas/workflow.schema.json +585 -0
  14. package/src/command-names.ts +46 -0
  15. package/src/commands.ts +80 -0
  16. package/src/config/ceiling.ts +153 -0
  17. package/src/config/command-conflicts.ts +31 -0
  18. package/src/config/load.ts +327 -0
  19. package/src/config/types.ts +187 -0
  20. package/src/config/validate.ts +1145 -0
  21. package/src/digest.ts +23 -0
  22. package/src/engine/checkpoint.ts +30 -0
  23. package/src/engine/resume.ts +44 -0
  24. package/src/engine/state.ts +186 -0
  25. package/src/engine/transitions.ts +426 -0
  26. package/src/harness.ts +1676 -0
  27. package/src/index.ts +15 -0
  28. package/src/integrations/plannotator.ts +235 -0
  29. package/src/integrations/prompt-gate.ts +54 -0
  30. package/src/integrations/subagents/child-runtime.ts +306 -0
  31. package/src/integrations/subagents/client.ts +239 -0
  32. package/src/integrations/subagents/protocol.ts +304 -0
  33. package/src/policy/approved-commands.ts +225 -0
  34. package/src/policy/bash.ts +355 -0
  35. package/src/policy/completion-batch.ts +36 -0
  36. package/src/policy/immutable-input.ts +18 -0
  37. package/src/policy/tools.ts +150 -0
  38. package/src/preflight.ts +76 -0
  39. package/src/prompt.ts +146 -0
  40. package/src/runtime/completion-tool.ts +22 -0
  41. package/src/runtime/main-step-runtime.ts +227 -0
  42. package/src/runtime/serial-task-queue.ts +17 -0
  43. package/src/runtime/step-result.ts +85 -0
  44. package/src/workflow-list.ts +25 -0
  45. package/src/workflow-status.ts +611 -0
@@ -0,0 +1,153 @@
1
+ import type {
2
+ BashPermission,
3
+ BashRule,
4
+ PermissionCeiling,
5
+ WorkflowDefinition,
6
+ } from './types.ts';
7
+
8
+ function selectorAllowed(
9
+ requested: string,
10
+ ceiling: readonly string[],
11
+ ): boolean {
12
+ const separator = requested.indexOf('/');
13
+ if (separator === -1) return ceiling.includes(requested);
14
+ const server = requested.slice(0, separator);
15
+ return ceiling.includes(server) || ceiling.includes(requested);
16
+ }
17
+
18
+ function ruleKey(rule: BashRule): string {
19
+ return JSON.stringify([rule.executable, rule.argsPrefix]);
20
+ }
21
+
22
+ function bashWithinCeiling(
23
+ requested: BashPermission,
24
+ ceiling: BashPermission,
25
+ ): boolean {
26
+ if (ceiling.mode === 'unrestricted') return true;
27
+ if (requested.mode === 'deny') return true;
28
+ if (ceiling.mode === 'deny') return false;
29
+ if (ceiling.mode === 'read-only') return requested.mode === 'read-only';
30
+ if (requested.mode !== 'allow-list') return false;
31
+
32
+ const allowedRules = new Set(ceiling.allow.map(ruleKey));
33
+ const allowedSources = new Set(ceiling.approvedSources ?? []);
34
+ return (
35
+ requested.allow.every((rule) => allowedRules.has(ruleKey(rule))) &&
36
+ (requested.approvedSources ?? []).every((source) =>
37
+ allowedSources.has(source),
38
+ )
39
+ );
40
+ }
41
+
42
+ export function checkWorkflowAgainstCeiling(
43
+ workflow: WorkflowDefinition,
44
+ ceiling: PermissionCeiling,
45
+ ): string[] {
46
+ const errors: string[] = [];
47
+ for (const [stepId, step] of Object.entries(workflow.steps)) {
48
+ const path = `workflow.steps.${stepId}.permissions`;
49
+ const subagentPath = `workflow.steps.${stepId}.subagent`;
50
+ for (const tool of step.permissions.tools) {
51
+ if (!ceiling.tools.includes(tool)) {
52
+ errors.push(
53
+ `${path}.tools: "${tool}" exceeds the user permission ceiling`,
54
+ );
55
+ }
56
+ }
57
+ for (const selector of step.permissions.mcp) {
58
+ if (!selectorAllowed(selector, ceiling.mcp)) {
59
+ errors.push(
60
+ `${path}.mcp: "${selector}" exceeds the user permission ceiling`,
61
+ );
62
+ }
63
+ }
64
+ for (const extension of step.permissions.extensions) {
65
+ if (!ceiling.extensions.includes(extension)) {
66
+ errors.push(
67
+ `${path}.extensions: "${extension}" exceeds the user permission ceiling`,
68
+ );
69
+ }
70
+ }
71
+ for (const skill of step.permissions.skills) {
72
+ if (!ceiling.skills.includes(skill)) {
73
+ errors.push(
74
+ `${path}.skills: "${skill}" exceeds the user permission ceiling`,
75
+ );
76
+ }
77
+ }
78
+ if (!bashWithinCeiling(step.permissions.bash, ceiling.bash)) {
79
+ errors.push(`${path}.bash: exceeds the user permission ceiling`);
80
+ }
81
+ if (!step.subagent) continue;
82
+ if (!ceiling.subagent) {
83
+ errors.push(
84
+ `${subagentPath}: subagent execution exceeds the user permission ceiling`,
85
+ );
86
+ continue;
87
+ }
88
+ if (!ceiling.subagent.agents.includes(step.subagent.agent)) {
89
+ errors.push(
90
+ `${subagentPath}.agent: "${step.subagent.agent}" exceeds the user permission ceiling`,
91
+ );
92
+ }
93
+ if (!ceiling.subagent.contexts.includes(step.subagent.context)) {
94
+ errors.push(
95
+ `${subagentPath}.context: "${step.subagent.context}" exceeds the user permission ceiling`,
96
+ );
97
+ }
98
+ if (
99
+ step.subagent.model &&
100
+ !ceiling.subagent.models.includes(step.subagent.model)
101
+ ) {
102
+ errors.push(
103
+ `${subagentPath}.model: "${step.subagent.model}" exceeds the user permission ceiling`,
104
+ );
105
+ }
106
+ if (step.subagent.timeoutMs > ceiling.subagent.maxTimeoutMs) {
107
+ errors.push(
108
+ `${subagentPath}.timeoutMs: exceeds the user permission ceiling`,
109
+ );
110
+ }
111
+ if (step.subagent.artifacts && !ceiling.subagent.artifacts) {
112
+ errors.push(
113
+ `${subagentPath}.artifacts: exceeds the user permission ceiling`,
114
+ );
115
+ }
116
+ if (!step.subagent.turnBudget) {
117
+ errors.push(
118
+ `${subagentPath}.turnBudget: required for a project workflow`,
119
+ );
120
+ } else {
121
+ if (step.subagent.turnBudget.maxTurns > ceiling.subagent.maxTurns) {
122
+ errors.push(
123
+ `${subagentPath}.turnBudget.maxTurns: exceeds the user permission ceiling`,
124
+ );
125
+ }
126
+ if (
127
+ (step.subagent.turnBudget.graceTurns ?? 0) >
128
+ ceiling.subagent.maxGraceTurns
129
+ ) {
130
+ errors.push(
131
+ `${subagentPath}.turnBudget.graceTurns: exceeds the user permission ceiling`,
132
+ );
133
+ }
134
+ }
135
+ if (!step.subagent.toolBudget) {
136
+ errors.push(
137
+ `${subagentPath}.toolBudget: required for a project workflow`,
138
+ );
139
+ } else {
140
+ if (step.subagent.toolBudget.hard > ceiling.subagent.maxToolCalls) {
141
+ errors.push(
142
+ `${subagentPath}.toolBudget.hard: exceeds the user permission ceiling`,
143
+ );
144
+ }
145
+ if (step.subagent.toolBudget.block !== '*') {
146
+ errors.push(
147
+ `${subagentPath}.toolBudget.block: must be "*" for a project workflow`,
148
+ );
149
+ }
150
+ }
151
+ }
152
+ return errors;
153
+ }
@@ -0,0 +1,31 @@
1
+ interface CommandLike {
2
+ name: string;
3
+ }
4
+
5
+ function isSuffixedInvocation(name: string, command: string): boolean {
6
+ if (!name.startsWith(`${command}:`)) return false;
7
+ return /^\d+$/.test(name.slice(command.length + 1));
8
+ }
9
+
10
+ /**
11
+ * Pi adds numeric suffixes when extensions register the same command. Existing
12
+ * aliases owned by this harness are safe to replace on reload; a suffixed name
13
+ * proves at least one other command owns the same base invocation.
14
+ */
15
+ export function hasRuntimeCommandConflict(
16
+ command: string,
17
+ availableCommands: readonly CommandLike[],
18
+ ownedAliases: ReadonlySet<string>,
19
+ ): boolean {
20
+ if (
21
+ availableCommands.some((candidate) =>
22
+ isSuffixedInvocation(candidate.name, command),
23
+ )
24
+ ) {
25
+ return true;
26
+ }
27
+ return (
28
+ !ownedAliases.has(command) &&
29
+ availableCommands.some((candidate) => candidate.name === command)
30
+ );
31
+ }
@@ -0,0 +1,327 @@
1
+ import { readdir, readFile, realpath } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { parseDocument } from 'yaml';
5
+ import { digest } from '../digest.ts';
6
+ import { checkWorkflowAgainstCeiling } from './ceiling.ts';
7
+ import {
8
+ DEFAULT_SETTINGS,
9
+ type ConfigDiagnostic,
10
+ type LoadedWorkflow,
11
+ type WorkflowCatalog,
12
+ type WorkflowDefinition,
13
+ type WorkflowSettings,
14
+ type WorkflowSourceKind,
15
+ } from './types.ts';
16
+ import {
17
+ validatePromptText,
18
+ validateSettings,
19
+ validateWorkflow,
20
+ } from './validate.ts';
21
+
22
+ export interface LoadCatalogOptions {
23
+ cwd: string;
24
+ projectTrusted: boolean;
25
+ userDirectory?: string;
26
+ }
27
+
28
+ interface LoadedDirectory {
29
+ workflows: LoadedWorkflow[];
30
+ diagnostics: ConfigDiagnostic[];
31
+ }
32
+
33
+ function diagnostic(
34
+ path: string,
35
+ message: string,
36
+ level: ConfigDiagnostic['level'] = 'error',
37
+ ): ConfigDiagnostic {
38
+ return { path, message, level };
39
+ }
40
+
41
+ async function readYaml(
42
+ path: string,
43
+ kind: 'settings' | 'workflow',
44
+ ): Promise<unknown> {
45
+ const text = await readFile(path, 'utf8');
46
+ const document = parseDocument(text, {
47
+ customTags: [],
48
+ merge: false,
49
+ prettyErrors: true,
50
+ resolveKnownTags: false,
51
+ schema: 'core',
52
+ strict: true,
53
+ stringKeys: true,
54
+ uniqueKeys: true,
55
+ version: '1.2',
56
+ });
57
+ const issues = [...document.errors, ...document.warnings];
58
+ if (issues.length > 0) {
59
+ throw new Error(issues.map((issue) => issue.message).join('\n'));
60
+ }
61
+ if (
62
+ document.directives.yaml.explicit &&
63
+ document.directives.yaml.version !== '1.2'
64
+ ) {
65
+ throw new Error(`${kind} YAML must use version 1.2`);
66
+ }
67
+ return document.toJS({ maxAliasCount: 100 }) as unknown;
68
+ }
69
+
70
+ function isInside(root: string, candidate: string): boolean {
71
+ const pathFromRoot = relative(root, candidate);
72
+ return (
73
+ pathFromRoot === '' ||
74
+ (pathFromRoot !== '..' &&
75
+ !pathFromRoot.startsWith(`..${sep}`) &&
76
+ !isAbsolute(pathFromRoot))
77
+ );
78
+ }
79
+
80
+ async function loadPrompt(
81
+ sourcePath: string,
82
+ definition: WorkflowDefinition,
83
+ stepId: string,
84
+ ): Promise<string> {
85
+ const prompt = definition.steps[stepId]?.prompt;
86
+ if (!prompt) throw new Error(`unknown step "${stepId}"`);
87
+ if ('inline' in prompt) return prompt.inline;
88
+
89
+ const sourceDirectory = await realpath(dirname(sourcePath));
90
+ const requestedPath = resolve(sourceDirectory, prompt.file);
91
+ if (!isInside(sourceDirectory, requestedPath)) {
92
+ throw new Error(`prompt file escapes workflow directory: ${prompt.file}`);
93
+ }
94
+
95
+ const actualPath = await realpath(requestedPath);
96
+ if (!isInside(sourceDirectory, actualPath)) {
97
+ throw new Error(
98
+ `prompt file symlink escapes workflow directory: ${prompt.file}`,
99
+ );
100
+ }
101
+ return readFile(actualPath, 'utf8');
102
+ }
103
+
104
+ async function loadWorkflowFile(
105
+ sourcePath: string,
106
+ sourceKind: WorkflowSourceKind,
107
+ ): Promise<LoadedWorkflow> {
108
+ const raw = await readYaml(sourcePath, 'workflow');
109
+ const validation = validateWorkflow(raw);
110
+ if (!validation.value) {
111
+ throw new Error(validation.errors.join('\n'));
112
+ }
113
+
114
+ const definition = validation.value;
115
+ const prompts: Record<string, string> = {};
116
+ for (const stepId of Object.keys(definition.steps)) {
117
+ const text = await loadPrompt(sourcePath, definition, stepId);
118
+ const promptErrors = validatePromptText(
119
+ text,
120
+ `workflow.steps.${stepId}.prompt`,
121
+ );
122
+ if (promptErrors.length > 0) {
123
+ throw new Error(promptErrors.join('\n'));
124
+ }
125
+ prompts[stepId] = text;
126
+ }
127
+
128
+ const stepDigests = Object.fromEntries(
129
+ Object.entries(definition.steps).map(([stepId, step]) => [
130
+ stepId,
131
+ digest({ step, prompt: prompts[stepId] }),
132
+ ]),
133
+ );
134
+ return {
135
+ definition,
136
+ prompts,
137
+ digest: digest({ definition, prompts }),
138
+ stepDigests,
139
+ sourcePath,
140
+ sourceKind,
141
+ };
142
+ }
143
+
144
+ async function loadWorkflowDirectory(
145
+ directory: string,
146
+ sourceKind: WorkflowSourceKind,
147
+ ): Promise<LoadedDirectory> {
148
+ let entries;
149
+ try {
150
+ entries = await readdir(directory, { withFileTypes: true });
151
+ } catch (error) {
152
+ const code = (error as NodeJS.ErrnoException).code;
153
+ if (code === 'ENOENT') return { workflows: [], diagnostics: [] };
154
+ return {
155
+ workflows: [],
156
+ diagnostics: [
157
+ diagnostic(
158
+ directory,
159
+ `cannot read workflow directory: ${String(error)}`,
160
+ ),
161
+ ],
162
+ };
163
+ }
164
+
165
+ const workflows: LoadedWorkflow[] = [];
166
+ const diagnostics: ConfigDiagnostic[] = [];
167
+ const files = entries
168
+ .filter((entry) => entry.isFile() && /\.workflow\.ya?ml$/i.test(entry.name))
169
+ .map((entry) => join(directory, entry.name))
170
+ .sort();
171
+
172
+ for (const path of files) {
173
+ try {
174
+ workflows.push(await loadWorkflowFile(path, sourceKind));
175
+ } catch (error) {
176
+ diagnostics.push(
177
+ diagnostic(
178
+ path,
179
+ error instanceof Error ? error.message : String(error),
180
+ ),
181
+ );
182
+ }
183
+ }
184
+ return { workflows, diagnostics };
185
+ }
186
+
187
+ async function loadSettings(userDirectory: string): Promise<{
188
+ settings: WorkflowSettings;
189
+ diagnostics: ConfigDiagnostic[];
190
+ }> {
191
+ const path = join(userDirectory, 'settings.yaml');
192
+ try {
193
+ const validation = validateSettings(await readYaml(path, 'settings'));
194
+ if (!validation.value) {
195
+ return {
196
+ settings: DEFAULT_SETTINGS,
197
+ diagnostics: validation.errors.map((message) =>
198
+ diagnostic(path, message),
199
+ ),
200
+ };
201
+ }
202
+ return { settings: validation.value, diagnostics: [] };
203
+ } catch (error) {
204
+ const code = (error as NodeJS.ErrnoException).code;
205
+ if (code === 'ENOENT')
206
+ return { settings: DEFAULT_SETTINGS, diagnostics: [] };
207
+ return {
208
+ settings: DEFAULT_SETTINGS,
209
+ diagnostics: [
210
+ diagnostic(
211
+ path,
212
+ `cannot read settings: ${error instanceof Error ? error.message : String(error)}`,
213
+ ),
214
+ ],
215
+ };
216
+ }
217
+ }
218
+
219
+ function addWorkflow(
220
+ catalog: Map<string, LoadedWorkflow>,
221
+ commands: Map<string, string>,
222
+ workflow: LoadedWorkflow,
223
+ diagnostics: ConfigDiagnostic[],
224
+ ): void {
225
+ const id = workflow.definition.id;
226
+ const command = workflow.definition.command;
227
+ const existing = catalog.get(id);
228
+ if (existing) {
229
+ diagnostics.push(
230
+ diagnostic(
231
+ workflow.sourcePath,
232
+ `workflow id "${id}" already belongs to ${existing.sourcePath}; overrides are not allowed`,
233
+ ),
234
+ );
235
+ return;
236
+ }
237
+ const existingCommand = commands.get(command);
238
+ if (existingCommand) {
239
+ diagnostics.push(
240
+ diagnostic(
241
+ workflow.sourcePath,
242
+ `command "/${command}" already belongs to workflow "${existingCommand}"`,
243
+ ),
244
+ );
245
+ return;
246
+ }
247
+ catalog.set(id, workflow);
248
+ commands.set(command, id);
249
+ }
250
+
251
+ export function defaultUserWorkflowDirectory(): string {
252
+ const explicit = process.env.PI_WORKFLOWS_DIR?.trim();
253
+ if (explicit) return resolve(explicit);
254
+ const agentDirectory =
255
+ process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), '.pi', 'agent');
256
+ return join(agentDirectory, 'workflows');
257
+ }
258
+
259
+ export async function loadCatalog(
260
+ options: LoadCatalogOptions,
261
+ ): Promise<WorkflowCatalog> {
262
+ const userDirectory = resolve(
263
+ options.userDirectory ?? defaultUserWorkflowDirectory(),
264
+ );
265
+ const diagnostics: ConfigDiagnostic[] = [];
266
+ const settingsResult = await loadSettings(userDirectory);
267
+ diagnostics.push(...settingsResult.diagnostics);
268
+
269
+ const catalog = new Map<string, LoadedWorkflow>();
270
+ const commands = new Map<string, string>();
271
+ const userResult = await loadWorkflowDirectory(userDirectory, 'user');
272
+ diagnostics.push(...userResult.diagnostics);
273
+ for (const workflow of userResult.workflows) {
274
+ addWorkflow(catalog, commands, workflow, diagnostics);
275
+ }
276
+
277
+ const projectDirectory = join(resolve(options.cwd), '.pi', 'workflows');
278
+ if (settingsResult.settings.allowProjectWorkflows) {
279
+ if (!options.projectTrusted) {
280
+ diagnostics.push(
281
+ diagnostic(
282
+ projectDirectory,
283
+ 'project workflows were skipped because the project is not trusted',
284
+ 'warning',
285
+ ),
286
+ );
287
+ } else if (!settingsResult.settings.permissionCeiling) {
288
+ diagnostics.push(
289
+ diagnostic(
290
+ projectDirectory,
291
+ 'project workflows were skipped because no user permission ceiling is configured',
292
+ ),
293
+ );
294
+ } else {
295
+ const projectResult = await loadWorkflowDirectory(
296
+ projectDirectory,
297
+ 'project',
298
+ );
299
+ diagnostics.push(...projectResult.diagnostics);
300
+ for (const workflow of projectResult.workflows) {
301
+ const ceilingErrors = checkWorkflowAgainstCeiling(
302
+ workflow.definition,
303
+ settingsResult.settings.permissionCeiling,
304
+ );
305
+ if (ceilingErrors.length > 0) {
306
+ diagnostics.push(
307
+ ...ceilingErrors.map((message) =>
308
+ diagnostic(workflow.sourcePath, message),
309
+ ),
310
+ );
311
+ continue;
312
+ }
313
+ addWorkflow(catalog, commands, workflow, diagnostics);
314
+ }
315
+ }
316
+ }
317
+
318
+ return {
319
+ workflows: catalog,
320
+ settings: settingsResult.settings,
321
+ diagnostics,
322
+ userDirectory,
323
+ ...(settingsResult.settings.allowProjectWorkflows
324
+ ? { projectDirectory }
325
+ : {}),
326
+ };
327
+ }
@@ -0,0 +1,187 @@
1
+ export const WORKFLOW_SCHEMA_VERSION = 1 as const;
2
+ export const SUBAGENT_RUNTIME_NAME_PATTERN =
3
+ /^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)*$/;
4
+
5
+ export const TERMINAL_TARGETS = ['$done', '$pause'] as const;
6
+ export type TerminalTarget = (typeof TERMINAL_TARGETS)[number];
7
+ export type StepTarget = string | TerminalTarget;
8
+
9
+ export type BashMode = 'deny' | 'read-only' | 'allow-list' | 'unrestricted';
10
+ export type BashApprovalSource =
11
+ 'verification-worker' | 'verification-reviewer' | 'remote-actions';
12
+
13
+ export interface BashRule {
14
+ executable: string;
15
+ argsPrefix: string[];
16
+ }
17
+
18
+ export interface BashPermission {
19
+ mode: BashMode;
20
+ allow: BashRule[];
21
+ /**
22
+ * Exact commands extracted from the most recent human-approved artifact.
23
+ * They supplement `allow` only inside the correlated step execution.
24
+ */
25
+ approvedSources?: BashApprovalSource[];
26
+ }
27
+
28
+ export interface StepPermissions {
29
+ /** Exact Pi tool names, including direct MCP tools. */
30
+ tools: string[];
31
+ /** MCP proxy selectors in `server` or `server/tool` form. */
32
+ mcp: string[];
33
+ /** Source-name/path fragments whose registered tools may be used. */
34
+ extensions: string[];
35
+ /** Skills the step prompt is allowed to use. */
36
+ skills: string[];
37
+ bash: BashPermission;
38
+ }
39
+
40
+ export interface StepRequirements {
41
+ tools: string[];
42
+ extensions: string[];
43
+ skills: string[];
44
+ }
45
+
46
+ export type SubagentContext = 'fresh' | 'fork';
47
+
48
+ export interface SubagentTurnBudget {
49
+ maxTurns: number;
50
+ graceTurns?: number;
51
+ }
52
+
53
+ export interface SubagentToolBudget {
54
+ hard: number;
55
+ soft?: number;
56
+ block?: string[] | '*';
57
+ }
58
+
59
+ export interface StepSubagent {
60
+ /** Configured pi-subagents agent name. */
61
+ agent: string;
62
+ /** Fresh keeps workflow work out of the parent transcript. */
63
+ context: SubagentContext;
64
+ model?: string;
65
+ timeoutMs: number;
66
+ turnBudget?: SubagentTurnBudget;
67
+ toolBudget?: SubagentToolBudget;
68
+ artifacts: boolean;
69
+ }
70
+
71
+ export type PromptSpec = { inline: string } | { file: string };
72
+
73
+ interface GateDefinition {
74
+ submitOutcome: string;
75
+ approvedOutcome: string;
76
+ rejectedOutcome: string;
77
+ }
78
+
79
+ export interface PromptGate extends GateDefinition {
80
+ provider: 'prompt';
81
+ }
82
+
83
+ export interface PlannotatorGate extends GateDefinition {
84
+ provider: 'plannotator';
85
+ timeoutMs: number;
86
+ }
87
+
88
+ export type WorkflowGate = PromptGate | PlannotatorGate;
89
+
90
+ export interface WorkflowStep {
91
+ title: string;
92
+ prompt: PromptSpec;
93
+ /** Omit to execute this step in the main Pi agent. */
94
+ subagent?: StepSubagent;
95
+ permissions: StepPermissions;
96
+ requires: StepRequirements;
97
+ transitions: Record<string, StepTarget>;
98
+ gate?: WorkflowGate;
99
+ }
100
+
101
+ export interface WorkflowDefinition {
102
+ version: typeof WORKFLOW_SCHEMA_VERSION;
103
+ id: string;
104
+ command: string;
105
+ description: string;
106
+ start: string;
107
+ maxStepVisits: number;
108
+ summaryMaxChars: number;
109
+ steps: Record<string, WorkflowStep>;
110
+ }
111
+
112
+ export type WorkflowSourceKind = 'user' | 'project';
113
+
114
+ export interface LoadedWorkflow {
115
+ definition: WorkflowDefinition;
116
+ prompts: Record<string, string>;
117
+ digest: string;
118
+ stepDigests: Record<string, string>;
119
+ sourcePath: string;
120
+ sourceKind: WorkflowSourceKind;
121
+ }
122
+
123
+ export interface PermissionCeiling {
124
+ tools: string[];
125
+ mcp: string[];
126
+ extensions: string[];
127
+ skills: string[];
128
+ bash: BashPermission;
129
+ subagent?: SubagentPermissionCeiling;
130
+ }
131
+
132
+ export interface SubagentPermissionCeiling {
133
+ agents: string[];
134
+ contexts: SubagentContext[];
135
+ models: string[];
136
+ maxTimeoutMs: number;
137
+ maxTurns: number;
138
+ maxGraceTurns: number;
139
+ maxToolCalls: number;
140
+ artifacts: boolean;
141
+ }
142
+
143
+ export interface WorkflowSettings {
144
+ version: typeof WORKFLOW_SCHEMA_VERSION;
145
+ allowProjectWorkflows: boolean;
146
+ permissionCeiling?: PermissionCeiling;
147
+ }
148
+
149
+ export interface ConfigDiagnostic {
150
+ level: 'warning' | 'error';
151
+ path: string;
152
+ message: string;
153
+ }
154
+
155
+ export interface WorkflowCatalog {
156
+ workflows: Map<string, LoadedWorkflow>;
157
+ settings: WorkflowSettings;
158
+ diagnostics: ConfigDiagnostic[];
159
+ userDirectory: string;
160
+ projectDirectory?: string;
161
+ }
162
+
163
+ export const EMPTY_PERMISSIONS: StepPermissions = {
164
+ tools: [],
165
+ mcp: [],
166
+ extensions: [],
167
+ skills: [],
168
+ bash: { mode: 'deny', allow: [] },
169
+ };
170
+
171
+ export const EMPTY_REQUIREMENTS: StepRequirements = {
172
+ tools: [],
173
+ extensions: [],
174
+ skills: [],
175
+ };
176
+
177
+ export const DEFAULT_STEP_SUBAGENT: StepSubagent = {
178
+ agent: 'pi-workflows.step',
179
+ context: 'fresh',
180
+ timeoutMs: 900_000,
181
+ artifacts: false,
182
+ };
183
+
184
+ export const DEFAULT_SETTINGS: WorkflowSettings = {
185
+ version: WORKFLOW_SCHEMA_VERSION,
186
+ allowProjectWorkflows: false,
187
+ };