@wichayutdew/pi-workflows 0.3.0 → 1.0.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.
- package/README.md +53 -39
- package/dist/index.js +5754 -4794
- package/package.json +1 -1
- package/src/command-names.ts +7 -1
- package/src/commands.ts +126 -52
- package/src/config/catalog.ts +162 -0
- package/src/config/ceiling.ts +116 -116
- package/src/config/command-conflicts.ts +4 -4
- package/src/config/diagnostics.ts +28 -0
- package/src/config/load-settings.ts +37 -0
- package/src/config/load-types.ts +53 -0
- package/src/config/load-workflows.ts +135 -0
- package/src/config/load.ts +43 -311
- package/src/config/types.ts +137 -135
- package/src/config/validate.ts +12 -1261
- package/src/config/validation/permissions.ts +291 -0
- package/src/config/validation/prompt.ts +19 -0
- package/src/config/validation/settings.ts +122 -0
- package/src/config/validation/shared.ts +129 -0
- package/src/config/validation/shortcut.ts +118 -0
- package/src/config/validation/step.ts +228 -0
- package/src/config/validation/subagent.ts +288 -0
- package/src/config/validation/workflow.ts +156 -0
- package/src/config/yaml.ts +29 -0
- package/src/digest.ts +38 -6
- package/src/engine/checkpoint.ts +13 -9
- package/src/engine/create-run.ts +42 -0
- package/src/engine/gate-transitions.ts +184 -0
- package/src/engine/reconciliation-history.ts +88 -0
- package/src/engine/resume.ts +19 -7
- package/src/engine/run-advance.ts +122 -0
- package/src/engine/run-lifecycle.ts +124 -0
- package/src/engine/run-reconciliation.ts +116 -0
- package/src/engine/run-validation.ts +128 -0
- package/src/engine/state-types.ts +64 -0
- package/src/engine/state.ts +10 -193
- package/src/engine/transition-helpers.ts +28 -0
- package/src/engine/transition-types.ts +8 -0
- package/src/engine/transitions.ts +17 -471
- package/src/harness/action-context.ts +181 -0
- package/src/harness/catalog.ts +45 -0
- package/src/harness/context-idle.ts +21 -0
- package/src/harness/core-actions.ts +299 -0
- package/src/harness/delegation-control-actions.ts +250 -0
- package/src/harness/delegation-failure.ts +215 -0
- package/src/harness/delegation-plan.ts +219 -0
- package/src/harness/delegation-recovery-validation.ts +159 -0
- package/src/harness/delegation-response-actions.ts +295 -0
- package/src/harness/delegation-retry-policy.ts +118 -0
- package/src/harness/dependencies.ts +165 -0
- package/src/harness/gate-submission-action.ts +154 -0
- package/src/harness/lifecycle-actions.ts +154 -0
- package/src/harness/pause-actions.ts +120 -0
- package/src/harness/plannotator-result-actions.ts +123 -0
- package/src/harness/prompt-gate-actions.ts +270 -0
- package/src/harness/resume-action.ts +261 -0
- package/src/harness/start-actions.ts +183 -0
- package/src/harness/status-actions.ts +191 -0
- package/src/harness/step-execution-actions.ts +283 -0
- package/src/harness/types.ts +76 -0
- package/src/harness.ts +236 -2303
- package/src/index.ts +73 -11
- package/src/integrations/plannotator-requests.ts +127 -0
- package/src/integrations/plannotator-responses.ts +141 -0
- package/src/integrations/plannotator-types.ts +44 -0
- package/src/integrations/plannotator.ts +16 -235
- package/src/integrations/prompt-gate.ts +17 -10
- package/src/integrations/subagents/child-policy-envelope.ts +119 -0
- package/src/integrations/subagents/child-policy-paths.ts +66 -0
- package/src/integrations/subagents/child-policy-sections.ts +174 -0
- package/src/integrations/subagents/child-policy-types.ts +32 -0
- package/src/integrations/subagents/child-policy-validation.ts +163 -0
- package/src/integrations/subagents/child-runtime-completion.ts +56 -0
- package/src/integrations/subagents/child-runtime-dependencies.ts +47 -0
- package/src/integrations/subagents/child-runtime-files.ts +156 -0
- package/src/integrations/subagents/child-runtime-policy.ts +83 -0
- package/src/integrations/subagents/child-runtime-types.ts +30 -0
- package/src/integrations/subagents/child-runtime.ts +140 -301
- package/src/integrations/subagents/client-delegation.ts +181 -0
- package/src/integrations/subagents/client-messages.ts +66 -0
- package/src/integrations/subagents/client-types.ts +36 -0
- package/src/integrations/subagents/client.ts +127 -219
- package/src/integrations/subagents/delegated-result.ts +31 -0
- package/src/integrations/subagents/diagnostic-format.ts +40 -0
- package/src/integrations/subagents/diagnostic-text.ts +114 -0
- package/src/integrations/subagents/diagnostic-types.ts +77 -0
- package/src/integrations/subagents/diagnostics.ts +24 -977
- package/src/integrations/subagents/failure-correlation.ts +235 -0
- package/src/integrations/subagents/failure-transcript.ts +192 -0
- package/src/integrations/subagents/hidden-bash-failure.ts +98 -0
- package/src/integrations/subagents/protocol-events.ts +27 -0
- package/src/integrations/subagents/protocol.ts +30 -375
- package/src/integrations/subagents/replay-audit.ts +147 -0
- package/src/integrations/subagents/replay-safety.ts +82 -0
- package/src/integrations/subagents/session-diagnostics.ts +258 -0
- package/src/policy/approved-command-extraction.ts +71 -0
- package/src/policy/approved-commands.ts +15 -432
- package/src/policy/bash-authorization.ts +86 -0
- package/src/policy/bash-read-only.ts +161 -0
- package/src/policy/bash-types.ts +20 -0
- package/src/policy/bash.ts +4 -346
- package/src/policy/completion-batch.ts +21 -16
- package/src/policy/immutable-input.ts +5 -2
- package/src/policy/mcp-authorization.ts +71 -0
- package/src/policy/restricted-command.ts +141 -0
- package/src/policy/restricted-git.ts +30 -0
- package/src/policy/reviewed-artifact.ts +97 -0
- package/src/policy/reviewed-command-safety.ts +150 -0
- package/src/policy/reviewed-command-shape.ts +49 -0
- package/src/policy/reviewed-repository-contract.ts +138 -0
- package/src/policy/reviewed-repository-cwd.ts +102 -0
- package/src/policy/tool-call-authorization.ts +58 -0
- package/src/policy/tool-selection.ts +72 -0
- package/src/policy/tool-types.ts +14 -0
- package/src/policy/tools.ts +11 -150
- package/src/preflight.ts +96 -61
- package/src/prompt/main-workflow-notice.ts +41 -0
- package/src/prompt/retry-task.ts +83 -0
- package/src/prompt/step-contract.ts +101 -0
- package/src/prompt/step-sections.ts +75 -0
- package/src/prompt/step-task.ts +156 -0
- package/src/prompt/template.ts +81 -0
- package/src/prompt.ts +10 -254
- package/src/runtime/completion-tool.ts +3 -0
- package/src/runtime/main-step-completion.ts +78 -0
- package/src/runtime/main-step-lifecycle.ts +37 -0
- package/src/runtime/main-step-policy.ts +84 -0
- package/src/runtime/main-step-runtime-types.ts +64 -0
- package/src/runtime/main-step-runtime.ts +129 -200
- package/src/runtime/main-step-state.ts +85 -0
- package/src/runtime/serial-task-queue.ts +48 -13
- package/src/runtime/step-result.ts +48 -27
- package/src/workflow-list.ts +15 -6
- package/src/workflow-status/format-status.ts +52 -0
- package/src/workflow-status/formatting.ts +178 -0
- package/src/workflow-status/layout.ts +109 -0
- package/src/workflow-status/render-board.ts +101 -0
- package/src/workflow-status/render-path.ts +123 -0
- package/src/workflow-status/render-summary.ts +144 -0
- package/src/workflow-status/types.ts +40 -0
- package/src/workflow-status/view.ts +273 -0
- package/src/workflow-status.ts +20 -820
package/package.json
CHANGED
package/src/command-names.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash commands owned by the workflow harness.
|
|
3
|
+
*/
|
|
1
4
|
export const HARNESS_COMMAND_NAMES = [
|
|
2
5
|
'workflow-abort',
|
|
3
6
|
'workflow-list',
|
|
@@ -39,7 +42,10 @@ export const PI_BUILTIN_COMMAND_NAMES = [
|
|
|
39
42
|
'trust',
|
|
40
43
|
] as const;
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Command names that workflow aliases may not shadow.
|
|
47
|
+
*/
|
|
48
|
+
export const RESERVED_COMMAND_NAMES: ReadonlySet<string> = new Set([
|
|
43
49
|
...HARNESS_COMMAND_NAMES,
|
|
44
50
|
...PI_BUILTIN_COMMAND_NAMES,
|
|
45
51
|
]);
|
package/src/commands.ts
CHANGED
|
@@ -1,74 +1,148 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
ExtensionCommandContext,
|
|
4
|
+
RegisteredCommand,
|
|
4
5
|
} from '@earendil-works/pi-coding-agent';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Workflow operations exposed to slash-command handlers.
|
|
9
|
+
*/
|
|
10
|
+
export type WorkflowCommandController = {
|
|
11
|
+
/** Returns loaded workflow IDs for command completion. */
|
|
12
|
+
readonly workflowIds: () => ReadonlyArray<string>;
|
|
13
|
+
/** Lists loaded workflows. */
|
|
14
|
+
readonly list: (context: ExtensionCommandContext) => Promise<void>;
|
|
15
|
+
/** Starts a workflow. */
|
|
16
|
+
readonly start: (
|
|
10
17
|
workflowId: string,
|
|
11
18
|
input: string,
|
|
12
|
-
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
context: ExtensionCommandContext,
|
|
20
|
+
) => Promise<void>;
|
|
21
|
+
/** Pauses the active workflow. */
|
|
22
|
+
readonly pause: (
|
|
23
|
+
reason: string,
|
|
24
|
+
context: ExtensionCommandContext,
|
|
25
|
+
) => Promise<void>;
|
|
26
|
+
/** Resumes the paused workflow. */
|
|
27
|
+
readonly resume: (context: ExtensionCommandContext) => Promise<void>;
|
|
28
|
+
/** Aborts the active workflow. */
|
|
29
|
+
readonly abort: (
|
|
30
|
+
reason: string,
|
|
31
|
+
context: ExtensionCommandContext,
|
|
32
|
+
) => Promise<void>;
|
|
33
|
+
/** Reloads workflow configuration. */
|
|
34
|
+
readonly reload: (context: ExtensionCommandContext) => Promise<void>;
|
|
35
|
+
};
|
|
19
36
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Declarative Pi command registration.
|
|
39
|
+
*/
|
|
40
|
+
export type HarnessCommand = {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly options: Omit<RegisteredCommand, 'name' | 'sourceInfo'>;
|
|
43
|
+
};
|
|
26
44
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
): void {
|
|
31
|
-
pi.registerCommand('workflow-list', {
|
|
32
|
-
description: 'List loaded declarative workflows',
|
|
33
|
-
handler: async (_args, ctx) => controller.list(ctx),
|
|
34
|
-
});
|
|
45
|
+
const splitFirst = (value: string): readonly [string, string] => {
|
|
46
|
+
const trimmedValue = value.trim();
|
|
47
|
+
const separatorIndex = trimmedValue.search(/\s/);
|
|
35
48
|
|
|
36
|
-
|
|
49
|
+
return separatorIndex === -1
|
|
50
|
+
? [trimmedValue, '']
|
|
51
|
+
: [
|
|
52
|
+
trimmedValue.slice(0, separatorIndex),
|
|
53
|
+
trimmedValue.slice(separatorIndex).trim(),
|
|
54
|
+
];
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const createStartCommand = (
|
|
58
|
+
controller: WorkflowCommandController,
|
|
59
|
+
): HarnessCommand => ({
|
|
60
|
+
name: 'workflow-start',
|
|
61
|
+
options: {
|
|
37
62
|
description: 'Start a workflow: /workflow-start <id> [input]',
|
|
38
63
|
getArgumentCompletions: (prefix) => {
|
|
39
|
-
const
|
|
64
|
+
const completions = controller
|
|
40
65
|
.workflowIds()
|
|
41
|
-
.filter((
|
|
42
|
-
.map((
|
|
43
|
-
|
|
66
|
+
.filter((workflowId) => workflowId.startsWith(prefix))
|
|
67
|
+
.map((workflowId) => ({
|
|
68
|
+
value: workflowId,
|
|
69
|
+
label: workflowId,
|
|
70
|
+
}));
|
|
71
|
+
return completions.length > 0 ? completions : null;
|
|
44
72
|
},
|
|
45
|
-
handler: async (args,
|
|
73
|
+
handler: async (args, context) => {
|
|
46
74
|
const [workflowId, input] = splitFirst(args);
|
|
47
75
|
if (!workflowId) {
|
|
48
|
-
|
|
76
|
+
context.ui.notify('Usage: /workflow-start <id> [input]', 'warning');
|
|
49
77
|
return;
|
|
50
78
|
}
|
|
51
|
-
await controller.start(workflowId, input,
|
|
79
|
+
await controller.start(workflowId, input, context);
|
|
52
80
|
},
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
pi.registerCommand('workflow-pause', {
|
|
56
|
-
description: 'Pause the active workflow without losing its checkpoint',
|
|
57
|
-
handler: async (reason, ctx) => controller.pause(reason.trim(), ctx),
|
|
58
|
-
});
|
|
81
|
+
},
|
|
82
|
+
});
|
|
59
83
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Creates the declarative command registrations for a workflow controller.
|
|
86
|
+
*
|
|
87
|
+
* @param controller - Injected workflow command operations.
|
|
88
|
+
* @returns Command names and Pi registration options.
|
|
89
|
+
*/
|
|
90
|
+
export function createHarnessCommands(
|
|
91
|
+
controller: WorkflowCommandController,
|
|
92
|
+
): ReadonlyArray<HarnessCommand> {
|
|
93
|
+
return [
|
|
94
|
+
{
|
|
95
|
+
name: 'workflow-list',
|
|
96
|
+
options: {
|
|
97
|
+
description: 'List loaded declarative workflows',
|
|
98
|
+
handler: async (_args, context) => controller.list(context),
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
createStartCommand(controller),
|
|
102
|
+
{
|
|
103
|
+
name: 'workflow-pause',
|
|
104
|
+
options: {
|
|
105
|
+
description: 'Pause the active workflow without losing its checkpoint',
|
|
106
|
+
handler: async (reason, context) =>
|
|
107
|
+
controller.pause(reason.trim(), context),
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'workflow-resume',
|
|
112
|
+
options: {
|
|
113
|
+
description: 'Reload configuration and resume the paused workflow',
|
|
114
|
+
handler: async (_args, context) => controller.resume(context),
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'workflow-abort',
|
|
119
|
+
options: {
|
|
120
|
+
description: 'Abort the active workflow',
|
|
121
|
+
handler: async (reason, context) =>
|
|
122
|
+
controller.abort(reason.trim(), context),
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'workflow-reload',
|
|
127
|
+
options: {
|
|
128
|
+
description: 'Reload workflow files while no workflow is running',
|
|
129
|
+
handler: async (_args, context) => controller.reload(context),
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
}
|
|
69
134
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Registers all workflow commands with Pi.
|
|
137
|
+
*
|
|
138
|
+
* @param pi - Pi extension API used for registration.
|
|
139
|
+
* @param controller - Injected workflow command operations.
|
|
140
|
+
*/
|
|
141
|
+
export function registerHarnessCommands(
|
|
142
|
+
pi: ExtensionAPI,
|
|
143
|
+
controller: WorkflowCommandController,
|
|
144
|
+
): void {
|
|
145
|
+
for (const command of createHarnessCommands(controller)) {
|
|
146
|
+
pi.registerCommand(command.name, command.options);
|
|
147
|
+
}
|
|
74
148
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { join, resolve } from 'node:path';
|
|
2
|
+
import { checkWorkflowAgainstCeiling } from './ceiling.ts';
|
|
3
|
+
import { createDiagnostic } from './diagnostics.ts';
|
|
4
|
+
import { loadSettings } from './load-settings.ts';
|
|
5
|
+
import type {
|
|
6
|
+
ConfigLoader,
|
|
7
|
+
ConfigLoaderDependencies,
|
|
8
|
+
LoadCatalogOptions,
|
|
9
|
+
} from './load-types.ts';
|
|
10
|
+
import { loadWorkflowDirectory } from './load-workflows.ts';
|
|
11
|
+
import type {
|
|
12
|
+
ConfigDiagnostic,
|
|
13
|
+
LoadedWorkflow,
|
|
14
|
+
PermissionCeiling,
|
|
15
|
+
WorkflowCatalog,
|
|
16
|
+
WorkflowSettings,
|
|
17
|
+
} from './types.ts';
|
|
18
|
+
|
|
19
|
+
type MutableCatalog = {
|
|
20
|
+
readonly workflows: Map<string, LoadedWorkflow>;
|
|
21
|
+
readonly commands: Map<string, string>;
|
|
22
|
+
readonly diagnostics: Array<ConfigDiagnostic>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function addWorkflow(catalog: MutableCatalog, workflow: LoadedWorkflow): void {
|
|
26
|
+
const { id, command } = workflow.definition;
|
|
27
|
+
const existing = catalog.workflows.get(id);
|
|
28
|
+
if (existing) {
|
|
29
|
+
catalog.diagnostics.push(
|
|
30
|
+
createDiagnostic(
|
|
31
|
+
workflow.sourcePath,
|
|
32
|
+
`workflow id "${id}" already belongs to ${existing.sourcePath}; overrides are not allowed`,
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const existingCommand = catalog.commands.get(command);
|
|
38
|
+
if (existingCommand) {
|
|
39
|
+
catalog.diagnostics.push(
|
|
40
|
+
createDiagnostic(
|
|
41
|
+
workflow.sourcePath,
|
|
42
|
+
`command "/${command}" already belongs to workflow "${existingCommand}"`,
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
catalog.workflows.set(id, workflow);
|
|
48
|
+
catalog.commands.set(command, id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function defaultUserWorkflowDirectory(
|
|
52
|
+
dependencies: ConfigLoaderDependencies,
|
|
53
|
+
): string {
|
|
54
|
+
const explicit = dependencies.environment
|
|
55
|
+
.getVariable('PI_WORKFLOWS_DIR')
|
|
56
|
+
?.trim();
|
|
57
|
+
if (explicit) return resolve(explicit);
|
|
58
|
+
const agentDirectory =
|
|
59
|
+
dependencies.environment.getVariable('PI_CODING_AGENT_DIR')?.trim() ||
|
|
60
|
+
join(dependencies.environment.homeDirectory(), '.pi', 'agent');
|
|
61
|
+
return join(agentDirectory, 'workflows');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function requiredPermissionCeiling(
|
|
65
|
+
settings: WorkflowSettings,
|
|
66
|
+
): PermissionCeiling {
|
|
67
|
+
if (!settings.permissionCeiling) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
'validated settings must define a ceiling when project workflows are enabled',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return settings.permissionCeiling;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function loadCatalog(
|
|
76
|
+
dependencies: ConfigLoaderDependencies,
|
|
77
|
+
options: LoadCatalogOptions,
|
|
78
|
+
): Promise<WorkflowCatalog> {
|
|
79
|
+
const userDirectory = resolve(
|
|
80
|
+
options.userDirectory ?? defaultUserWorkflowDirectory(dependencies),
|
|
81
|
+
);
|
|
82
|
+
const settingsResult = await loadSettings(
|
|
83
|
+
dependencies.fileSystem,
|
|
84
|
+
userDirectory,
|
|
85
|
+
);
|
|
86
|
+
const state: MutableCatalog = {
|
|
87
|
+
workflows: new Map(),
|
|
88
|
+
commands: new Map(),
|
|
89
|
+
diagnostics: [...settingsResult.diagnostics],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const userResult = await loadWorkflowDirectory(
|
|
93
|
+
dependencies.fileSystem,
|
|
94
|
+
userDirectory,
|
|
95
|
+
'user',
|
|
96
|
+
);
|
|
97
|
+
state.diagnostics.push(...userResult.diagnostics);
|
|
98
|
+
userResult.workflows.forEach((workflow) => {
|
|
99
|
+
addWorkflow(state, workflow);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const projectDirectory = join(resolve(options.cwd), '.pi', 'workflows');
|
|
103
|
+
if (settingsResult.settings.allowProjectWorkflows) {
|
|
104
|
+
if (!options.projectTrusted) {
|
|
105
|
+
state.diagnostics.push(
|
|
106
|
+
createDiagnostic(
|
|
107
|
+
projectDirectory,
|
|
108
|
+
'project workflows were skipped because the project is not trusted',
|
|
109
|
+
'warning',
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
} else {
|
|
113
|
+
const permissionCeiling = requiredPermissionCeiling(
|
|
114
|
+
settingsResult.settings,
|
|
115
|
+
);
|
|
116
|
+
const projectResult = await loadWorkflowDirectory(
|
|
117
|
+
dependencies.fileSystem,
|
|
118
|
+
projectDirectory,
|
|
119
|
+
'project',
|
|
120
|
+
);
|
|
121
|
+
state.diagnostics.push(...projectResult.diagnostics);
|
|
122
|
+
projectResult.workflows.forEach((workflow) => {
|
|
123
|
+
const ceilingErrors = checkWorkflowAgainstCeiling(
|
|
124
|
+
workflow.definition,
|
|
125
|
+
permissionCeiling,
|
|
126
|
+
);
|
|
127
|
+
if (ceilingErrors.length > 0) {
|
|
128
|
+
state.diagnostics.push(
|
|
129
|
+
...ceilingErrors.map((message) =>
|
|
130
|
+
createDiagnostic(workflow.sourcePath, message),
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
addWorkflow(state, workflow);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
workflows: state.workflows,
|
|
142
|
+
settings: settingsResult.settings,
|
|
143
|
+
diagnostics: state.diagnostics,
|
|
144
|
+
userDirectory,
|
|
145
|
+
...(settingsResult.settings.allowProjectWorkflows
|
|
146
|
+
? { projectDirectory }
|
|
147
|
+
: {}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Bind configuration loading to explicit filesystem and environment ports. */
|
|
152
|
+
export function createConfigLoader(
|
|
153
|
+
dependencies: ConfigLoaderDependencies,
|
|
154
|
+
): ConfigLoader {
|
|
155
|
+
return {
|
|
156
|
+
defaultUserWorkflowDirectory: () =>
|
|
157
|
+
defaultUserWorkflowDirectory(dependencies),
|
|
158
|
+
loadSettings: (userDirectory) =>
|
|
159
|
+
loadSettings(dependencies.fileSystem, userDirectory),
|
|
160
|
+
loadCatalog: (options) => loadCatalog(dependencies, options),
|
|
161
|
+
};
|
|
162
|
+
}
|
package/src/config/ceiling.ts
CHANGED
|
@@ -2,12 +2,14 @@ import type {
|
|
|
2
2
|
BashPermission,
|
|
3
3
|
BashRule,
|
|
4
4
|
PermissionCeiling,
|
|
5
|
+
StepSubagent,
|
|
6
|
+
SubagentPermissionCeiling,
|
|
5
7
|
WorkflowDefinition,
|
|
6
8
|
} from './types.ts';
|
|
7
9
|
|
|
8
10
|
function selectorAllowed(
|
|
9
11
|
requested: string,
|
|
10
|
-
ceiling:
|
|
12
|
+
ceiling: ReadonlyArray<string>,
|
|
11
13
|
): boolean {
|
|
12
14
|
const separator = requested.indexOf('/');
|
|
13
15
|
if (separator === -1) return ceiling.includes(requested);
|
|
@@ -39,123 +41,121 @@ function bashWithinCeiling(
|
|
|
39
41
|
);
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
function turnBudgetErrors(
|
|
45
|
+
subagent: StepSubagent,
|
|
46
|
+
ceiling: SubagentPermissionCeiling,
|
|
47
|
+
path: string,
|
|
48
|
+
): Array<string> {
|
|
49
|
+
if (!subagent.turnBudget) {
|
|
50
|
+
return [`${path}.turnBudget: required for a project workflow`];
|
|
51
|
+
}
|
|
52
|
+
return [
|
|
53
|
+
...(subagent.turnBudget.maxTurns > ceiling.maxTurns
|
|
54
|
+
? [`${path}.turnBudget.maxTurns: exceeds the user permission ceiling`]
|
|
55
|
+
: []),
|
|
56
|
+
...((subagent.turnBudget.graceTurns ?? 0) > ceiling.maxGraceTurns
|
|
57
|
+
? [`${path}.turnBudget.graceTurns: exceeds the user permission ceiling`]
|
|
58
|
+
: []),
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function toolBudgetErrors(
|
|
63
|
+
subagent: StepSubagent,
|
|
64
|
+
ceiling: SubagentPermissionCeiling,
|
|
65
|
+
path: string,
|
|
66
|
+
): Array<string> {
|
|
67
|
+
if (!subagent.toolBudget) {
|
|
68
|
+
return [`${path}.toolBudget: required for a project workflow`];
|
|
69
|
+
}
|
|
70
|
+
return [
|
|
71
|
+
...(subagent.toolBudget.hard > ceiling.maxToolCalls
|
|
72
|
+
? [`${path}.toolBudget.hard: exceeds the user permission ceiling`]
|
|
73
|
+
: []),
|
|
74
|
+
...(subagent.toolBudget.block !== '*'
|
|
75
|
+
? [`${path}.toolBudget.block: must be "*" for a project workflow`]
|
|
76
|
+
: []),
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function subagentErrors(
|
|
81
|
+
subagent: StepSubagent | undefined,
|
|
82
|
+
ceiling: SubagentPermissionCeiling | undefined,
|
|
83
|
+
path: string,
|
|
84
|
+
): Array<string> {
|
|
85
|
+
if (!subagent) return [];
|
|
86
|
+
if (!ceiling) {
|
|
87
|
+
return [`${path}: subagent execution exceeds the user permission ceiling`];
|
|
88
|
+
}
|
|
89
|
+
return [
|
|
90
|
+
...(!ceiling.agents.includes(subagent.agent)
|
|
91
|
+
? [
|
|
92
|
+
`${path}.agent: "${subagent.agent}" exceeds the user permission ceiling`,
|
|
93
|
+
]
|
|
94
|
+
: []),
|
|
95
|
+
...(!ceiling.contexts.includes(subagent.context)
|
|
96
|
+
? [
|
|
97
|
+
`${path}.context: "${subagent.context}" exceeds the user permission ceiling`,
|
|
98
|
+
]
|
|
99
|
+
: []),
|
|
100
|
+
...(subagent.model && !ceiling.models.includes(subagent.model)
|
|
101
|
+
? [
|
|
102
|
+
`${path}.model: "${subagent.model}" exceeds the user permission ceiling`,
|
|
103
|
+
]
|
|
104
|
+
: []),
|
|
105
|
+
...(subagent.timeoutMs > ceiling.maxTimeoutMs
|
|
106
|
+
? [`${path}.timeoutMs: exceeds the user permission ceiling`]
|
|
107
|
+
: []),
|
|
108
|
+
...(subagent.artifacts && !ceiling.artifacts
|
|
109
|
+
? [`${path}.artifacts: exceeds the user permission ceiling`]
|
|
110
|
+
: []),
|
|
111
|
+
...(subagent.retryToolFailures && !ceiling.retryToolFailures
|
|
112
|
+
? [`${path}.retryToolFailures: exceeds the user permission ceiling`]
|
|
113
|
+
: []),
|
|
114
|
+
...turnBudgetErrors(subagent, ceiling, path),
|
|
115
|
+
...toolBudgetErrors(subagent, ceiling, path),
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Return every project-workflow permission that exceeds the user ceiling. */
|
|
42
120
|
export function checkWorkflowAgainstCeiling(
|
|
43
121
|
workflow: WorkflowDefinition,
|
|
44
122
|
ceiling: PermissionCeiling,
|
|
45
|
-
): string
|
|
46
|
-
|
|
47
|
-
for (const [stepId, step] of Object.entries(workflow.steps)) {
|
|
123
|
+
): Array<string> {
|
|
124
|
+
return Object.entries(workflow.steps).flatMap(([stepId, step]) => {
|
|
48
125
|
const path = `workflow.steps.${stepId}.permissions`;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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 (
|
|
117
|
-
step.subagent.retryToolFailures &&
|
|
118
|
-
!ceiling.subagent.retryToolFailures
|
|
119
|
-
) {
|
|
120
|
-
errors.push(
|
|
121
|
-
`${subagentPath}.retryToolFailures: exceeds the user permission ceiling`,
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
if (!step.subagent.turnBudget) {
|
|
125
|
-
errors.push(
|
|
126
|
-
`${subagentPath}.turnBudget: required for a project workflow`,
|
|
127
|
-
);
|
|
128
|
-
} else {
|
|
129
|
-
if (step.subagent.turnBudget.maxTurns > ceiling.subagent.maxTurns) {
|
|
130
|
-
errors.push(
|
|
131
|
-
`${subagentPath}.turnBudget.maxTurns: exceeds the user permission ceiling`,
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
if (
|
|
135
|
-
(step.subagent.turnBudget.graceTurns ?? 0) >
|
|
136
|
-
ceiling.subagent.maxGraceTurns
|
|
137
|
-
) {
|
|
138
|
-
errors.push(
|
|
139
|
-
`${subagentPath}.turnBudget.graceTurns: exceeds the user permission ceiling`,
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
if (!step.subagent.toolBudget) {
|
|
144
|
-
errors.push(
|
|
145
|
-
`${subagentPath}.toolBudget: required for a project workflow`,
|
|
146
|
-
);
|
|
147
|
-
} else {
|
|
148
|
-
if (step.subagent.toolBudget.hard > ceiling.subagent.maxToolCalls) {
|
|
149
|
-
errors.push(
|
|
150
|
-
`${subagentPath}.toolBudget.hard: exceeds the user permission ceiling`,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
if (step.subagent.toolBudget.block !== '*') {
|
|
154
|
-
errors.push(
|
|
155
|
-
`${subagentPath}.toolBudget.block: must be "*" for a project workflow`,
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return errors;
|
|
126
|
+
return [
|
|
127
|
+
...step.permissions.tools
|
|
128
|
+
.filter((tool) => !ceiling.tools.includes(tool))
|
|
129
|
+
.map(
|
|
130
|
+
(tool) =>
|
|
131
|
+
`${path}.tools: "${tool}" exceeds the user permission ceiling`,
|
|
132
|
+
),
|
|
133
|
+
...step.permissions.mcp
|
|
134
|
+
.filter((selector) => !selectorAllowed(selector, ceiling.mcp))
|
|
135
|
+
.map(
|
|
136
|
+
(selector) =>
|
|
137
|
+
`${path}.mcp: "${selector}" exceeds the user permission ceiling`,
|
|
138
|
+
),
|
|
139
|
+
...step.permissions.extensions
|
|
140
|
+
.filter((extension) => !ceiling.extensions.includes(extension))
|
|
141
|
+
.map(
|
|
142
|
+
(extension) =>
|
|
143
|
+
`${path}.extensions: "${extension}" exceeds the user permission ceiling`,
|
|
144
|
+
),
|
|
145
|
+
...step.permissions.skills
|
|
146
|
+
.filter((skill) => !ceiling.skills.includes(skill))
|
|
147
|
+
.map(
|
|
148
|
+
(skill) =>
|
|
149
|
+
`${path}.skills: "${skill}" exceeds the user permission ceiling`,
|
|
150
|
+
),
|
|
151
|
+
...(!bashWithinCeiling(step.permissions.bash, ceiling.bash)
|
|
152
|
+
? [`${path}.bash: exceeds the user permission ceiling`]
|
|
153
|
+
: []),
|
|
154
|
+
...subagentErrors(
|
|
155
|
+
step.subagent,
|
|
156
|
+
ceiling.subagent,
|
|
157
|
+
`workflow.steps.${stepId}.subagent`,
|
|
158
|
+
),
|
|
159
|
+
];
|
|
160
|
+
});
|
|
161
161
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
name: string;
|
|
3
|
-
}
|
|
1
|
+
type CommandLike = {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
};
|
|
4
4
|
|
|
5
5
|
function isSuffixedInvocation(name: string, command: string): boolean {
|
|
6
6
|
if (!name.startsWith(`${command}:`)) return false;
|
|
@@ -14,7 +14,7 @@ function isSuffixedInvocation(name: string, command: string): boolean {
|
|
|
14
14
|
*/
|
|
15
15
|
export function hasRuntimeCommandConflict(
|
|
16
16
|
command: string,
|
|
17
|
-
availableCommands:
|
|
17
|
+
availableCommands: ReadonlyArray<CommandLike>,
|
|
18
18
|
ownedAliases: ReadonlySet<string>,
|
|
19
19
|
): boolean {
|
|
20
20
|
if (
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ConfigDiagnostic } from './types.ts';
|
|
2
|
+
|
|
3
|
+
/** Create one normalized configuration diagnostic. */
|
|
4
|
+
export function createDiagnostic(
|
|
5
|
+
path: string,
|
|
6
|
+
message: string,
|
|
7
|
+
level: ConfigDiagnostic['level'] = 'error',
|
|
8
|
+
): ConfigDiagnostic {
|
|
9
|
+
return { path, message, level };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Convert any caught value to a stable diagnostic message. */
|
|
13
|
+
export function errorMessage(error: unknown): string {
|
|
14
|
+
return error instanceof Error ? error.message : String(error);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Read a Node-style string error code without asserting the caught value. */
|
|
18
|
+
export function errorCode(error: unknown): string | undefined {
|
|
19
|
+
if (
|
|
20
|
+
typeof error !== 'object' ||
|
|
21
|
+
error === null ||
|
|
22
|
+
!('code' in error) ||
|
|
23
|
+
typeof error.code !== 'string'
|
|
24
|
+
) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return error.code;
|
|
28
|
+
}
|