@wichayutdew/pi-workflows 0.2.3 → 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 +66 -38
- package/dist/index.js +5903 -4782
- 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 +237 -2201
- 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 -799
- 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 -245
- 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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { createDiagnostic, errorCode, errorMessage } from './diagnostics.ts';
|
|
3
|
+
import type { ConfigFileSystem, LoadedSettings } from './load-types.ts';
|
|
4
|
+
import { DEFAULT_SETTINGS } from './types.ts';
|
|
5
|
+
import { validateSettings } from './validate.ts';
|
|
6
|
+
import { parseYaml } from './yaml.ts';
|
|
7
|
+
|
|
8
|
+
/** Load user settings through the injected filesystem boundary. */
|
|
9
|
+
export async function loadSettings(
|
|
10
|
+
fileSystem: ConfigFileSystem,
|
|
11
|
+
userDirectory: string,
|
|
12
|
+
): Promise<LoadedSettings> {
|
|
13
|
+
const path = join(userDirectory, 'settings.yaml');
|
|
14
|
+
try {
|
|
15
|
+
const text = await fileSystem.readTextFile(path);
|
|
16
|
+
const validation = validateSettings(parseYaml(text, 'settings'));
|
|
17
|
+
if (!validation.value) {
|
|
18
|
+
return {
|
|
19
|
+
settings: { ...DEFAULT_SETTINGS },
|
|
20
|
+
diagnostics: validation.errors.map((message) =>
|
|
21
|
+
createDiagnostic(path, message),
|
|
22
|
+
),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return { settings: validation.value, diagnostics: [] };
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (errorCode(error) === 'ENOENT') {
|
|
28
|
+
return { settings: { ...DEFAULT_SETTINGS }, diagnostics: [] };
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
settings: { ...DEFAULT_SETTINGS },
|
|
32
|
+
diagnostics: [
|
|
33
|
+
createDiagnostic(path, `cannot read settings: ${errorMessage(error)}`),
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConfigDiagnostic,
|
|
3
|
+
LoadedWorkflow,
|
|
4
|
+
WorkflowCatalog,
|
|
5
|
+
WorkflowSettings,
|
|
6
|
+
} from './types.ts';
|
|
7
|
+
|
|
8
|
+
export type LoadCatalogOptions = {
|
|
9
|
+
readonly cwd: string;
|
|
10
|
+
readonly projectTrusted: boolean;
|
|
11
|
+
readonly userDirectory?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type WorkflowDirectoryEntry = {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly isFile: () => boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type ConfigFileSystem = {
|
|
20
|
+
readonly readDirectory: (
|
|
21
|
+
path: string,
|
|
22
|
+
) => Promise<ReadonlyArray<WorkflowDirectoryEntry>>;
|
|
23
|
+
readonly readTextFile: (path: string) => Promise<string>;
|
|
24
|
+
readonly realPath: (path: string) => Promise<string>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ConfigEnvironment = {
|
|
28
|
+
readonly getVariable: (name: string) => string | undefined;
|
|
29
|
+
readonly homeDirectory: () => string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ConfigLoaderDependencies = {
|
|
33
|
+
readonly fileSystem: ConfigFileSystem;
|
|
34
|
+
readonly environment: ConfigEnvironment;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type LoadedDirectory = {
|
|
38
|
+
readonly workflows: ReadonlyArray<LoadedWorkflow>;
|
|
39
|
+
readonly diagnostics: ReadonlyArray<ConfigDiagnostic>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type LoadedSettings = {
|
|
43
|
+
readonly settings: WorkflowSettings;
|
|
44
|
+
readonly diagnostics: ReadonlyArray<ConfigDiagnostic>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type ConfigLoader = {
|
|
48
|
+
readonly defaultUserWorkflowDirectory: () => string;
|
|
49
|
+
readonly loadSettings: (userDirectory: string) => Promise<LoadedSettings>;
|
|
50
|
+
readonly loadCatalog: (
|
|
51
|
+
options: LoadCatalogOptions,
|
|
52
|
+
) => Promise<WorkflowCatalog>;
|
|
53
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
2
|
+
import { digest } from '../digest.ts';
|
|
3
|
+
import { createDiagnostic, errorCode, errorMessage } from './diagnostics.ts';
|
|
4
|
+
import type { ConfigFileSystem, LoadedDirectory } from './load-types.ts';
|
|
5
|
+
import type {
|
|
6
|
+
LoadedWorkflow,
|
|
7
|
+
WorkflowDefinition,
|
|
8
|
+
WorkflowSourceKind,
|
|
9
|
+
} from './types.ts';
|
|
10
|
+
import { validatePromptText, validateWorkflow } from './validate.ts';
|
|
11
|
+
import { parseYaml } from './yaml.ts';
|
|
12
|
+
|
|
13
|
+
async function readYamlFile(
|
|
14
|
+
fileSystem: ConfigFileSystem,
|
|
15
|
+
path: string,
|
|
16
|
+
): Promise<unknown> {
|
|
17
|
+
return parseYaml(await fileSystem.readTextFile(path), 'workflow');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isInside(root: string, candidate: string): boolean {
|
|
21
|
+
const pathFromRoot = relative(root, candidate);
|
|
22
|
+
return (
|
|
23
|
+
pathFromRoot === '' ||
|
|
24
|
+
(pathFromRoot !== '..' &&
|
|
25
|
+
!pathFromRoot.startsWith(`..${sep}`) &&
|
|
26
|
+
!isAbsolute(pathFromRoot))
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function loadPrompt(
|
|
31
|
+
fileSystem: ConfigFileSystem,
|
|
32
|
+
sourcePath: string,
|
|
33
|
+
definition: WorkflowDefinition,
|
|
34
|
+
stepId: string,
|
|
35
|
+
): Promise<string> {
|
|
36
|
+
const prompt = definition.steps[stepId]?.prompt;
|
|
37
|
+
if (!prompt) throw new Error(`unknown step "${stepId}"`);
|
|
38
|
+
if ('inline' in prompt) return prompt.inline;
|
|
39
|
+
|
|
40
|
+
const sourceDirectory = await fileSystem.realPath(dirname(sourcePath));
|
|
41
|
+
const requestedPath = resolve(sourceDirectory, prompt.file);
|
|
42
|
+
if (!isInside(sourceDirectory, requestedPath)) {
|
|
43
|
+
throw new Error(`prompt file escapes workflow directory: ${prompt.file}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const actualPath = await fileSystem.realPath(requestedPath);
|
|
47
|
+
if (!isInside(sourceDirectory, actualPath)) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`prompt file symlink escapes workflow directory: ${prompt.file}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return fileSystem.readTextFile(actualPath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function loadWorkflowFile(
|
|
56
|
+
fileSystem: ConfigFileSystem,
|
|
57
|
+
sourcePath: string,
|
|
58
|
+
sourceKind: WorkflowSourceKind,
|
|
59
|
+
): Promise<LoadedWorkflow> {
|
|
60
|
+
const validation = validateWorkflow(
|
|
61
|
+
await readYamlFile(fileSystem, sourcePath),
|
|
62
|
+
);
|
|
63
|
+
if (!validation.value) {
|
|
64
|
+
throw new Error(validation.errors.join('\n'));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const definition = validation.value;
|
|
68
|
+
const prompts: Record<string, string> = {};
|
|
69
|
+
for (const stepId of Object.keys(definition.steps)) {
|
|
70
|
+
const text = await loadPrompt(fileSystem, sourcePath, definition, stepId);
|
|
71
|
+
const promptErrors = validatePromptText(
|
|
72
|
+
text,
|
|
73
|
+
`workflow.steps.${stepId}.prompt`,
|
|
74
|
+
);
|
|
75
|
+
if (promptErrors.length > 0) {
|
|
76
|
+
throw new Error(promptErrors.join('\n'));
|
|
77
|
+
}
|
|
78
|
+
prompts[stepId] = text;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const stepDigests = Object.fromEntries(
|
|
82
|
+
Object.entries(definition.steps).map(([stepId, step]) => [
|
|
83
|
+
stepId,
|
|
84
|
+
digest({ step, prompt: prompts[stepId] }),
|
|
85
|
+
]),
|
|
86
|
+
);
|
|
87
|
+
return {
|
|
88
|
+
definition,
|
|
89
|
+
prompts,
|
|
90
|
+
digest: digest({ definition, prompts }),
|
|
91
|
+
stepDigests,
|
|
92
|
+
sourcePath,
|
|
93
|
+
sourceKind,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Load and validate every workflow file in one source directory. */
|
|
98
|
+
export async function loadWorkflowDirectory(
|
|
99
|
+
fileSystem: ConfigFileSystem,
|
|
100
|
+
directory: string,
|
|
101
|
+
sourceKind: WorkflowSourceKind,
|
|
102
|
+
): Promise<LoadedDirectory> {
|
|
103
|
+
let entries: Awaited<ReturnType<ConfigFileSystem['readDirectory']>>;
|
|
104
|
+
try {
|
|
105
|
+
entries = await fileSystem.readDirectory(directory);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (errorCode(error) === 'ENOENT') {
|
|
108
|
+
return { workflows: [], diagnostics: [] };
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
workflows: [],
|
|
112
|
+
diagnostics: [
|
|
113
|
+
createDiagnostic(
|
|
114
|
+
directory,
|
|
115
|
+
`cannot read workflow directory: ${String(error)}`,
|
|
116
|
+
),
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const files = entries
|
|
122
|
+
.filter((entry) => entry.isFile() && /\.workflow\.ya?ml$/i.test(entry.name))
|
|
123
|
+
.map((entry) => join(directory, entry.name))
|
|
124
|
+
.sort();
|
|
125
|
+
const workflows: Array<LoadedWorkflow> = [];
|
|
126
|
+
const diagnostics = [];
|
|
127
|
+
for (const path of files) {
|
|
128
|
+
try {
|
|
129
|
+
workflows.push(await loadWorkflowFile(fileSystem, path, sourceKind));
|
|
130
|
+
} catch (error) {
|
|
131
|
+
diagnostics.push(createDiagnostic(path, errorMessage(error)));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { workflows, diagnostics };
|
|
135
|
+
}
|
package/src/config/load.ts
CHANGED
|
@@ -1,321 +1,53 @@
|
|
|
1
1
|
import { readdir, readFile, realpath } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from './
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
):
|
|
38
|
-
return
|
|
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
|
-
export 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);
|
|
3
|
+
import { createConfigLoader } from './catalog.ts';
|
|
4
|
+
import type {
|
|
5
|
+
ConfigLoaderDependencies,
|
|
6
|
+
LoadCatalogOptions,
|
|
7
|
+
LoadedSettings,
|
|
8
|
+
} from './load-types.ts';
|
|
9
|
+
import type { WorkflowCatalog } from './types.ts';
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
ConfigEnvironment,
|
|
13
|
+
ConfigFileSystem,
|
|
14
|
+
ConfigLoader,
|
|
15
|
+
ConfigLoaderDependencies,
|
|
16
|
+
LoadCatalogOptions,
|
|
17
|
+
LoadedSettings,
|
|
18
|
+
WorkflowDirectoryEntry,
|
|
19
|
+
} from './load-types.ts';
|
|
20
|
+
export { createConfigLoader } from './catalog.ts';
|
|
21
|
+
|
|
22
|
+
const NODE_CONFIG_DEPENDENCIES = {
|
|
23
|
+
fileSystem: {
|
|
24
|
+
readDirectory: (path) => readdir(path, { withFileTypes: true }),
|
|
25
|
+
readTextFile: (path) => readFile(path, 'utf8'),
|
|
26
|
+
realPath: realpath,
|
|
27
|
+
},
|
|
28
|
+
environment: {
|
|
29
|
+
getVariable: (name) => process.env[name],
|
|
30
|
+
homeDirectory: homedir,
|
|
31
|
+
},
|
|
32
|
+
} as const satisfies ConfigLoaderDependencies;
|
|
33
|
+
|
|
34
|
+
const NODE_CONFIG_LOADER = createConfigLoader(NODE_CONFIG_DEPENDENCIES);
|
|
35
|
+
|
|
36
|
+
/** Resolve the default user workflow directory from the process environment. */
|
|
37
|
+
export function defaultUserWorkflowDirectory(): string {
|
|
38
|
+
return NODE_CONFIG_LOADER.defaultUserWorkflowDirectory();
|
|
249
39
|
}
|
|
250
40
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return join(agentDirectory, 'workflows');
|
|
41
|
+
/** Load workflow settings from the given user directory. */
|
|
42
|
+
export async function loadSettings(
|
|
43
|
+
userDirectory: string,
|
|
44
|
+
): Promise<LoadedSettings> {
|
|
45
|
+
return NODE_CONFIG_LOADER.loadSettings(userDirectory);
|
|
257
46
|
}
|
|
258
47
|
|
|
48
|
+
/** Load the complete workflow catalog using the default Node.js adapters. */
|
|
259
49
|
export async function loadCatalog(
|
|
260
50
|
options: LoadCatalogOptions,
|
|
261
51
|
): Promise<WorkflowCatalog> {
|
|
262
|
-
|
|
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 {
|
|
288
|
-
const permissionCeiling = settingsResult.settings.permissionCeiling!;
|
|
289
|
-
const projectResult = await loadWorkflowDirectory(
|
|
290
|
-
projectDirectory,
|
|
291
|
-
'project',
|
|
292
|
-
);
|
|
293
|
-
diagnostics.push(...projectResult.diagnostics);
|
|
294
|
-
for (const workflow of projectResult.workflows) {
|
|
295
|
-
const ceilingErrors = checkWorkflowAgainstCeiling(
|
|
296
|
-
workflow.definition,
|
|
297
|
-
permissionCeiling,
|
|
298
|
-
);
|
|
299
|
-
if (ceilingErrors.length > 0) {
|
|
300
|
-
diagnostics.push(
|
|
301
|
-
...ceilingErrors.map((message) =>
|
|
302
|
-
diagnostic(workflow.sourcePath, message),
|
|
303
|
-
),
|
|
304
|
-
);
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
addWorkflow(catalog, commands, workflow, diagnostics);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return {
|
|
313
|
-
workflows: catalog,
|
|
314
|
-
settings: settingsResult.settings,
|
|
315
|
-
diagnostics,
|
|
316
|
-
userDirectory,
|
|
317
|
-
...(settingsResult.settings.allowProjectWorkflows
|
|
318
|
-
? { projectDirectory }
|
|
319
|
-
: {}),
|
|
320
|
-
};
|
|
52
|
+
return NODE_CONFIG_LOADER.loadCatalog(options);
|
|
321
53
|
}
|