agent-workflow-kit-cli 1.2.0 → 1.3.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/dist/cli/commands/add.js +50 -12
- package/dist/cli/commands/adr.js +98 -0
- package/dist/cli/commands/export.js +13 -0
- package/dist/cli/commands/init.js +136 -30
- package/dist/cli/commands/profile.js +35 -0
- package/dist/cli/commands/role.js +75 -0
- package/dist/cli/commands/run.js +78 -0
- package/dist/cli/commands/workflow.js +95 -0
- package/dist/cli/index.js +193 -2
- package/dist/core/awos/adr.js +208 -0
- package/dist/core/awos/intelligence.js +235 -0
- package/dist/core/awos/profiles.js +272 -0
- package/dist/core/awos/registry.js +224 -0
- package/dist/core/awos/runtime.js +322 -0
- package/dist/core/awos/types.js +5 -0
- package/dist/core/config.js +27 -0
- package/dist/core/parser.js +143 -0
- package/dist/core/renderer.js +74 -23
- package/package.json +3 -2
- package/templates/common/ide-rules.hbs +12 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { validateWorkflowGraph } from "./runtime.js";
|
|
8
|
+
import { loadConfig } from "../config.js";
|
|
9
|
+
/**
|
|
10
|
+
* Registry for discovery, validation, and loading of reusable workflow graph definitions.
|
|
11
|
+
*/
|
|
12
|
+
export class WorkflowRegistry {
|
|
13
|
+
/**
|
|
14
|
+
* Scans a directory recursively for workflow JSON files.
|
|
15
|
+
*/
|
|
16
|
+
static async discover(dir) {
|
|
17
|
+
const workflows = [];
|
|
18
|
+
async function traverse(currentDir) {
|
|
19
|
+
try {
|
|
20
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
if (entry.name.startsWith(".") ||
|
|
25
|
+
entry.name === "node_modules" ||
|
|
26
|
+
entry.name === "dist" ||
|
|
27
|
+
entry.name === "build") {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
await traverse(fullPath);
|
|
31
|
+
}
|
|
32
|
+
else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
33
|
+
try {
|
|
34
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
35
|
+
const parsed = JSON.parse(content);
|
|
36
|
+
if (WorkflowRegistry.isValidSchema(parsed)) {
|
|
37
|
+
// Perform graph cycles check
|
|
38
|
+
validateWorkflowGraph(parsed.graph);
|
|
39
|
+
workflows.push(parsed);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Ignore invalid JSON files or validation failures during traversal
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Ignore read errors
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
await traverse(dir);
|
|
53
|
+
return workflows;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Asserts schema validity for WorkflowDefinition.
|
|
57
|
+
*/
|
|
58
|
+
static isValidSchema(obj) {
|
|
59
|
+
return (obj &&
|
|
60
|
+
typeof obj.id === "string" &&
|
|
61
|
+
typeof obj.name === "string" &&
|
|
62
|
+
typeof obj.description === "string" &&
|
|
63
|
+
typeof obj.version === "string" &&
|
|
64
|
+
Array.isArray(obj.supportedArchitectures) &&
|
|
65
|
+
Array.isArray(obj.requiredRoles) &&
|
|
66
|
+
obj.graph &&
|
|
67
|
+
Array.isArray(obj.graph.nodes) &&
|
|
68
|
+
Array.isArray(obj.graph.edges));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Validates a workflow definition schema and graph.
|
|
72
|
+
*/
|
|
73
|
+
static validate(wf) {
|
|
74
|
+
if (!WorkflowRegistry.isValidSchema(wf)) {
|
|
75
|
+
throw new Error("Workflow schema is invalid. Missing required fields: 'id', 'name', 'version', 'supportedArchitectures', 'requiredRoles', or 'graph'.");
|
|
76
|
+
}
|
|
77
|
+
// Check graph validity
|
|
78
|
+
validateWorkflowGraph(wf.graph);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Loads a specific workflow by ID.
|
|
82
|
+
*/
|
|
83
|
+
static async load(id, dir) {
|
|
84
|
+
const list = await WorkflowRegistry.discover(dir);
|
|
85
|
+
const matched = list.find((w) => w.id === id);
|
|
86
|
+
if (!matched) {
|
|
87
|
+
throw new Error(`Workflow Pack '${id}' not found under directory '${dir}'.`);
|
|
88
|
+
}
|
|
89
|
+
return matched;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Registry for discovery, validation, and loading of agent role profiles.
|
|
94
|
+
*/
|
|
95
|
+
export class RoleRegistry {
|
|
96
|
+
/**
|
|
97
|
+
* Scans a directory recursively for role JSON files.
|
|
98
|
+
*/
|
|
99
|
+
static async discover(dir) {
|
|
100
|
+
const roles = [];
|
|
101
|
+
async function traverse(currentDir) {
|
|
102
|
+
try {
|
|
103
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
106
|
+
if (entry.isDirectory()) {
|
|
107
|
+
if (entry.name.startsWith(".") ||
|
|
108
|
+
entry.name === "node_modules" ||
|
|
109
|
+
entry.name === "dist" ||
|
|
110
|
+
entry.name === "build") {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
await traverse(fullPath);
|
|
114
|
+
}
|
|
115
|
+
else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
116
|
+
try {
|
|
117
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
118
|
+
const parsed = JSON.parse(content);
|
|
119
|
+
if (RoleRegistry.isValidSchema(parsed)) {
|
|
120
|
+
roles.push(parsed);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Ignore invalid files
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Ignore read errors
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
await traverse(dir);
|
|
134
|
+
return roles;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Asserts schema validity for AgentRoleDefinition.
|
|
138
|
+
*/
|
|
139
|
+
static isValidSchema(obj) {
|
|
140
|
+
return (obj &&
|
|
141
|
+
typeof obj.id === "string" &&
|
|
142
|
+
typeof obj.name === "string" &&
|
|
143
|
+
typeof obj.description === "string" &&
|
|
144
|
+
Array.isArray(obj.responsibilities) &&
|
|
145
|
+
Array.isArray(obj.requiredInputs) &&
|
|
146
|
+
Array.isArray(obj.expectedOutputs) &&
|
|
147
|
+
Array.isArray(obj.validationChecklist) &&
|
|
148
|
+
Array.isArray(obj.reviewChecklist));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Validates a role definition.
|
|
152
|
+
*/
|
|
153
|
+
static validate(role) {
|
|
154
|
+
if (!RoleRegistry.isValidSchema(role)) {
|
|
155
|
+
throw new Error("Agent Role schema is invalid. Missing required fields: 'id', 'name', 'responsibilities', 'requiredInputs', 'expectedOutputs', 'validationChecklist', or 'reviewChecklist'.");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Loads a specific role by ID.
|
|
160
|
+
*/
|
|
161
|
+
static async load(id, dir) {
|
|
162
|
+
const list = await RoleRegistry.discover(dir);
|
|
163
|
+
const matched = list.find((r) => r.id === id);
|
|
164
|
+
if (!matched) {
|
|
165
|
+
throw new Error(`Agent Role '${id}' not found under directory '${dir}'.`);
|
|
166
|
+
}
|
|
167
|
+
return matched;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export class PluginRegistryImpl {
|
|
171
|
+
analyzers = [];
|
|
172
|
+
profiles = new Map();
|
|
173
|
+
roles = new Map();
|
|
174
|
+
executors = new Map();
|
|
175
|
+
registerAnalyzer(analyzer) {
|
|
176
|
+
this.analyzers.push(analyzer);
|
|
177
|
+
}
|
|
178
|
+
registerArchitectureProfile(profile) {
|
|
179
|
+
this.profiles.set(profile.name, profile);
|
|
180
|
+
}
|
|
181
|
+
registerAgentRole(role) {
|
|
182
|
+
this.roles.set(role.id, role);
|
|
183
|
+
}
|
|
184
|
+
registerExecutor(name, executeFn) {
|
|
185
|
+
this.executors.set(name, executeFn);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export const pluginRegistry = new PluginRegistryImpl();
|
|
189
|
+
let pluginsLoaded = false;
|
|
190
|
+
export async function loadAWOSPlugins(workspaceRoot) {
|
|
191
|
+
if (pluginsLoaded) {
|
|
192
|
+
return pluginRegistry;
|
|
193
|
+
}
|
|
194
|
+
const config = await loadConfig(workspaceRoot);
|
|
195
|
+
if (config.plugins && config.plugins.length > 0) {
|
|
196
|
+
for (const pluginSpec of config.plugins) {
|
|
197
|
+
try {
|
|
198
|
+
let importPath = pluginSpec;
|
|
199
|
+
if (pluginSpec.startsWith(".") || pluginSpec.startsWith("/") || pluginSpec.includes("\\")) {
|
|
200
|
+
importPath = path.resolve(workspaceRoot, pluginSpec);
|
|
201
|
+
}
|
|
202
|
+
const module = await import(importPath);
|
|
203
|
+
const PluginClass = module.default || module.Plugin || module;
|
|
204
|
+
const pluginInstance = typeof PluginClass === "function" ? new PluginClass() : PluginClass;
|
|
205
|
+
if (pluginInstance && typeof pluginInstance.register === "function") {
|
|
206
|
+
pluginInstance.register(pluginRegistry);
|
|
207
|
+
console.log(`[AWOS Registry] Registered plugin: ${pluginInstance.manifest?.id || pluginSpec}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
console.warn(`[AWOS Registry] Failed to load plugin '${pluginSpec}':`, err);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
pluginsLoaded = true;
|
|
216
|
+
return pluginRegistry;
|
|
217
|
+
}
|
|
218
|
+
export function clearPluginsCache() {
|
|
219
|
+
pluginsLoaded = false;
|
|
220
|
+
pluginRegistry.analyzers = [];
|
|
221
|
+
pluginRegistry.profiles.clear();
|
|
222
|
+
pluginRegistry.roles.clear();
|
|
223
|
+
pluginRegistry.executors.clear();
|
|
224
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
import { exec } from "child_process";
|
|
9
|
+
import { promisify } from "util";
|
|
10
|
+
const execAsync = promisify(exec);
|
|
11
|
+
const RUNS_DIR = ".agents/state/runs";
|
|
12
|
+
/**
|
|
13
|
+
* Validates a DirectedWorkflowGraph to guarantee it is Acyclic and structurally correct.
|
|
14
|
+
*/
|
|
15
|
+
export function validateWorkflowGraph(graph) {
|
|
16
|
+
const nodeMap = new Map();
|
|
17
|
+
const adjList = new Map();
|
|
18
|
+
const inDegree = new Map();
|
|
19
|
+
for (const node of graph.nodes) {
|
|
20
|
+
nodeMap.set(node.id, node);
|
|
21
|
+
adjList.set(node.id, []);
|
|
22
|
+
inDegree.set(node.id, 0);
|
|
23
|
+
}
|
|
24
|
+
for (const edge of graph.edges) {
|
|
25
|
+
if (!nodeMap.has(edge.sourceId) || !nodeMap.has(edge.targetId)) {
|
|
26
|
+
throw new Error(`Edge references non-existent node: ${edge.sourceId} -> ${edge.targetId}`);
|
|
27
|
+
}
|
|
28
|
+
adjList.get(edge.sourceId).push(edge.targetId);
|
|
29
|
+
inDegree.set(edge.targetId, (inDegree.get(edge.targetId) || 0) + 1);
|
|
30
|
+
}
|
|
31
|
+
// 1. Cycle Detection using DFS
|
|
32
|
+
const visited = new Set();
|
|
33
|
+
const recStack = new Set();
|
|
34
|
+
function dfs(nodeId) {
|
|
35
|
+
visited.add(nodeId);
|
|
36
|
+
recStack.add(nodeId);
|
|
37
|
+
const neighbors = adjList.get(nodeId) || [];
|
|
38
|
+
for (const neighbor of neighbors) {
|
|
39
|
+
if (!visited.has(neighbor)) {
|
|
40
|
+
dfs(neighbor);
|
|
41
|
+
}
|
|
42
|
+
else if (recStack.has(neighbor)) {
|
|
43
|
+
// Exclude self-referencing retry/rollback configurations if explicit
|
|
44
|
+
const node = nodeMap.get(nodeId);
|
|
45
|
+
if (node?.type !== "retry" && node?.type !== "rollback") {
|
|
46
|
+
throw new Error(`Cycle detected involving node: ${nodeId} -> ${neighbor}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
recStack.delete(nodeId);
|
|
51
|
+
}
|
|
52
|
+
for (const node of graph.nodes) {
|
|
53
|
+
if (!visited.has(node.id)) {
|
|
54
|
+
dfs(node.id);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// 2. Fork/Join balance validation
|
|
58
|
+
const forkNodes = graph.nodes.filter((n) => n.type === "fork");
|
|
59
|
+
const joinNodes = graph.nodes.filter((n) => n.type === "join");
|
|
60
|
+
if (forkNodes.length !== joinNodes.length) {
|
|
61
|
+
throw new Error(`Fork/Join count mismatch. Forks: ${forkNodes.length}, Joins: ${joinNodes.length}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Helper to evaluate condition expressions.
|
|
66
|
+
*/
|
|
67
|
+
function evaluateCondition(expression, context) {
|
|
68
|
+
try {
|
|
69
|
+
// Simple sandbox execution for safe expressions
|
|
70
|
+
const keys = Object.keys(context);
|
|
71
|
+
const values = Object.values(context);
|
|
72
|
+
const fn = new Function(...keys, `return ${expression};`);
|
|
73
|
+
return !!fn(...values);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
console.warn(`Condition evaluation failed for '${expression}':`, err);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Workflow Runtime execution engine.
|
|
82
|
+
*/
|
|
83
|
+
export class WorkflowRuntime {
|
|
84
|
+
workspaceRoot;
|
|
85
|
+
runState;
|
|
86
|
+
constructor(workspaceRoot) {
|
|
87
|
+
this.workspaceRoot = workspaceRoot;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Initializes or resumes execution state file.
|
|
91
|
+
*/
|
|
92
|
+
async initRunState(graph, initialCtx) {
|
|
93
|
+
const runId = "run_" + crypto.randomBytes(4).toString("hex");
|
|
94
|
+
this.runState = {
|
|
95
|
+
runId,
|
|
96
|
+
workflowId: graph.id,
|
|
97
|
+
status: "IDLE",
|
|
98
|
+
context: { ...initialCtx },
|
|
99
|
+
currentStepIndex: 0,
|
|
100
|
+
steps: graph.nodes.map((n) => ({
|
|
101
|
+
id: n.id,
|
|
102
|
+
name: n.name,
|
|
103
|
+
status: "PENDING",
|
|
104
|
+
})),
|
|
105
|
+
};
|
|
106
|
+
await this.persistState();
|
|
107
|
+
}
|
|
108
|
+
async persistState() {
|
|
109
|
+
const runDir = path.join(this.workspaceRoot, RUNS_DIR);
|
|
110
|
+
const runFile = path.join(runDir, `${this.runState.runId}.json`);
|
|
111
|
+
await fs.mkdir(runDir, { recursive: true });
|
|
112
|
+
await fs.writeFile(runFile, JSON.stringify(this.runState, null, 2), "utf8");
|
|
113
|
+
}
|
|
114
|
+
getRunState() {
|
|
115
|
+
return this.runState;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Runs the workflow graph execution loop.
|
|
119
|
+
*/
|
|
120
|
+
async run(graph, initialContext = {}, options = {}) {
|
|
121
|
+
validateWorkflowGraph(graph);
|
|
122
|
+
await this.initRunState(graph, initialContext);
|
|
123
|
+
this.runState.status = "EXECUTING";
|
|
124
|
+
await this.persistState();
|
|
125
|
+
const nodeMap = new Map();
|
|
126
|
+
for (const node of graph.nodes) {
|
|
127
|
+
nodeMap.set(node.id, node);
|
|
128
|
+
}
|
|
129
|
+
const inDegree = new Map();
|
|
130
|
+
const parentsMap = new Map();
|
|
131
|
+
for (const n of graph.nodes) {
|
|
132
|
+
inDegree.set(n.id, 0);
|
|
133
|
+
parentsMap.set(n.id, []);
|
|
134
|
+
}
|
|
135
|
+
for (const edge of graph.edges) {
|
|
136
|
+
inDegree.set(edge.targetId, inDegree.get(edge.targetId) + 1);
|
|
137
|
+
parentsMap.get(edge.targetId).push(edge.sourceId);
|
|
138
|
+
}
|
|
139
|
+
// Nodes ready to execute
|
|
140
|
+
const queue = graph.nodes
|
|
141
|
+
.filter((n) => (inDegree.get(n.id) || 0) === 0)
|
|
142
|
+
.map((n) => n.id);
|
|
143
|
+
// Keep track of resolved step status
|
|
144
|
+
const stepStatuses = new Map();
|
|
145
|
+
for (const s of this.runState.steps) {
|
|
146
|
+
stepStatuses.set(s.id, "PENDING");
|
|
147
|
+
}
|
|
148
|
+
while (queue.length > 0) {
|
|
149
|
+
const nodeId = queue.shift();
|
|
150
|
+
const node = nodeMap.get(nodeId);
|
|
151
|
+
const stepIdx = this.runState.steps.findIndex((s) => s.id === nodeId);
|
|
152
|
+
this.runState.steps[stepIdx].status = "RUNNING";
|
|
153
|
+
this.runState.steps[stepIdx].startedAt = new Date().toISOString();
|
|
154
|
+
await this.persistState();
|
|
155
|
+
// Resolve role checks if configured
|
|
156
|
+
if (node.role) {
|
|
157
|
+
try {
|
|
158
|
+
const { loadAWOSPlugins } = await import("./registry.js");
|
|
159
|
+
const registry = await loadAWOSPlugins(this.workspaceRoot);
|
|
160
|
+
const customRole = registry.roles.get(node.role);
|
|
161
|
+
if (customRole) {
|
|
162
|
+
console.log(`[AWOS Runtime] Custom Role '${node.role}' loaded for step '${node.name}'. Asserting hooks...`);
|
|
163
|
+
if (customRole.hooks?.preStep) {
|
|
164
|
+
const { getRepositoryContext } = await import("./intelligence.js");
|
|
165
|
+
const { loadProfile } = await import("./profiles.js");
|
|
166
|
+
const repoContext = await getRepositoryContext(this.workspaceRoot);
|
|
167
|
+
const archProfile = await loadProfile(repoContext.architecture);
|
|
168
|
+
const res = await customRole.hooks.preStep({
|
|
169
|
+
runId: this.runState.runId,
|
|
170
|
+
workspaceRoot: this.workspaceRoot,
|
|
171
|
+
repoContext,
|
|
172
|
+
profile: archProfile
|
|
173
|
+
}, node);
|
|
174
|
+
if (!res.proceed) {
|
|
175
|
+
throw new Error(`Role preStep hook rejected execution: ${res.error || "Unknown rejection"}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const { RoleRegistry } = await import("./registry.js");
|
|
181
|
+
const roleDef = await RoleRegistry.load(node.role, path.join(this.workspaceRoot, ".agents/roles"));
|
|
182
|
+
console.log(`[AWOS Runtime] Role '${roleDef.id}' loaded for step '${node.name}'. Asserting responsibilities...`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
console.log(`[AWOS Runtime] Warning: Active role metadata for '${node.role}' could not be loaded: ${err instanceof Error ? err.message : String(err)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
let success = true;
|
|
191
|
+
let outputs = {};
|
|
192
|
+
// Execute by step type
|
|
193
|
+
if (node.type === "task") {
|
|
194
|
+
const executor = node.executor || "command";
|
|
195
|
+
const params = node.params || {};
|
|
196
|
+
const { loadAWOSPlugins } = await import("./registry.js");
|
|
197
|
+
const registry = await loadAWOSPlugins(this.workspaceRoot);
|
|
198
|
+
if (registry.executors.has(executor)) {
|
|
199
|
+
if (options.dryRun) {
|
|
200
|
+
console.log(`[Dry Run] Would execute custom executor '${executor}' with params:`, params);
|
|
201
|
+
outputs = node.mockOutput || { log: "Custom dry run completed." };
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
const executeFn = registry.executors.get(executor);
|
|
205
|
+
outputs = await executeFn(params, this.runState);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else if (executor === "command" && params.command) {
|
|
209
|
+
if (options.dryRun) {
|
|
210
|
+
console.log(`[Dry Run] Would execute shell command: ${params.command}`);
|
|
211
|
+
outputs = node.mockOutput || { log: "Dry run completed." };
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const { stdout, stderr } = await execAsync(params.command, { cwd: this.workspaceRoot });
|
|
215
|
+
outputs = { stdout, stderr };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (executor === "adr-generate") {
|
|
219
|
+
if (options.dryRun) {
|
|
220
|
+
console.log(`[Dry Run] Would generate ADR: ${params.title || "Decision"}`);
|
|
221
|
+
outputs = node.mockOutput || { adrId: "ADR-MOCK", status: "proposed" };
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const { ADRService } = await import("./adr.js");
|
|
225
|
+
const generated = await ADRService.create(this.workspaceRoot, {
|
|
226
|
+
title: params.title || "Decision",
|
|
227
|
+
status: params.status || "proposed",
|
|
228
|
+
context: params.context || "",
|
|
229
|
+
decision: params.decision || "",
|
|
230
|
+
consequences: params.consequences || "",
|
|
231
|
+
metadata: params.metadata || {},
|
|
232
|
+
});
|
|
233
|
+
outputs = { adrId: generated.id, status: generated.status };
|
|
234
|
+
console.log(`[AWOS Runtime] Generated Architectural Decision Record: ${generated.id}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else if (node.type === "conditional") {
|
|
239
|
+
const expression = node.params?.expression || "true";
|
|
240
|
+
const evalResult = evaluateCondition(expression, this.runState.context);
|
|
241
|
+
outputs = { conditionResult: evalResult };
|
|
242
|
+
}
|
|
243
|
+
else if (node.type === "approval") {
|
|
244
|
+
// Pause execution and ask for human approval
|
|
245
|
+
this.runState.steps[stepIdx].status = "WAITING_APPROVAL";
|
|
246
|
+
this.runState.status = "PAUSED";
|
|
247
|
+
await this.persistState();
|
|
248
|
+
console.log(`\n⚠️ Workflow paused at step '${node.name}'. Requires human approval to resume.`);
|
|
249
|
+
console.log(`Run 'awk run resume ${this.runState.runId}' to proceed.\n`);
|
|
250
|
+
return this.runState; // Suspends execution
|
|
251
|
+
}
|
|
252
|
+
if (success) {
|
|
253
|
+
stepStatuses.set(nodeId, "COMPLETED");
|
|
254
|
+
this.runState.steps[stepIdx].status = "COMPLETED";
|
|
255
|
+
this.runState.steps[stepIdx].outputs = outputs;
|
|
256
|
+
this.runState.steps[stepIdx].completedAt = new Date().toISOString();
|
|
257
|
+
// Merge outcomes into runtime context
|
|
258
|
+
this.runState.context = {
|
|
259
|
+
...this.runState.context,
|
|
260
|
+
[nodeId]: outputs,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
throw new Error(`Execution failed at node '${node.name}'`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
stepStatuses.set(nodeId, "FAILED");
|
|
269
|
+
this.runState.steps[stepIdx].status = "FAILED";
|
|
270
|
+
this.runState.steps[stepIdx].completedAt = new Date().toISOString();
|
|
271
|
+
await this.persistState();
|
|
272
|
+
// Handle retry and rollback
|
|
273
|
+
if (node.rollbackNodeId) {
|
|
274
|
+
console.warn(`Node failed. Triggering rollback node: ${node.rollbackNodeId}`);
|
|
275
|
+
queue.push(node.rollbackNodeId);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
this.runState.status = "FAILURE";
|
|
279
|
+
await this.persistState();
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
await this.persistState();
|
|
284
|
+
// Find children next nodes to push to queue
|
|
285
|
+
const edges = graph.edges.filter((e) => e.sourceId === nodeId);
|
|
286
|
+
for (const edge of edges) {
|
|
287
|
+
const targetId = edge.targetId;
|
|
288
|
+
const targetNode = nodeMap.get(targetId);
|
|
289
|
+
// Verify if condition is met
|
|
290
|
+
if (node.type === "conditional" && edge.conditionExpression) {
|
|
291
|
+
const condPassed = evaluateCondition(edge.conditionExpression, this.runState.context);
|
|
292
|
+
if (!condPassed) {
|
|
293
|
+
stepStatuses.set(targetId, "SKIPPED");
|
|
294
|
+
const targetIdx = this.runState.steps.findIndex((s) => s.id === targetId);
|
|
295
|
+
this.runState.steps[targetIdx].status = "SKIPPED";
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Check incoming paths dependency for JOIN nodes
|
|
300
|
+
if (targetNode.type === "join") {
|
|
301
|
+
const parents = parentsMap.get(targetId) || [];
|
|
302
|
+
const allParentsFinished = parents.every((p) => {
|
|
303
|
+
const status = stepStatuses.get(p);
|
|
304
|
+
return status === "COMPLETED" || status === "SKIPPED" || status === "FAILED";
|
|
305
|
+
});
|
|
306
|
+
if (allParentsFinished && !queue.includes(targetId)) {
|
|
307
|
+
queue.push(targetId);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
if (!queue.includes(targetId)) {
|
|
312
|
+
queue.push(targetId);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const hasFailures = Array.from(stepStatuses.values()).includes("FAILED");
|
|
318
|
+
this.runState.status = hasFailures ? "FAILURE" : "SUCCESS";
|
|
319
|
+
await this.persistState();
|
|
320
|
+
return this.runState;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
let loadedConfig = null;
|
|
8
|
+
let isLoaded = false;
|
|
9
|
+
export async function loadConfig(cwd = process.cwd()) {
|
|
10
|
+
if (isLoaded) {
|
|
11
|
+
return loadedConfig || {};
|
|
12
|
+
}
|
|
13
|
+
const configPath = path.join(cwd, "awk.config.json");
|
|
14
|
+
try {
|
|
15
|
+
const content = await fs.readFile(configPath, "utf8");
|
|
16
|
+
loadedConfig = JSON.parse(content);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
loadedConfig = {};
|
|
20
|
+
}
|
|
21
|
+
isLoaded = true;
|
|
22
|
+
return loadedConfig || {};
|
|
23
|
+
}
|
|
24
|
+
export function clearConfigCache() {
|
|
25
|
+
loadedConfig = null;
|
|
26
|
+
isLoaded = false;
|
|
27
|
+
}
|