guild-agents 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/guild.js +3 -2
- package/package.json +1 -1
- package/src/commands/run.js +66 -23
- package/src/utils/executor.js +183 -0
- package/src/utils/providers/claude-code.js +43 -0
package/bin/guild.js
CHANGED
|
@@ -123,10 +123,11 @@ program
|
|
|
123
123
|
// guild run
|
|
124
124
|
program
|
|
125
125
|
.command('run')
|
|
126
|
-
.description('
|
|
126
|
+
.description('Execute a skill workflow')
|
|
127
127
|
.argument('<skill>', 'Skill name to run')
|
|
128
128
|
.argument('[input]', 'Input text for the skill', '')
|
|
129
|
-
.option('--profile <profile>', 'Model profile (max,
|
|
129
|
+
.option('--profile <profile>', 'Model profile (max, pro)', 'max')
|
|
130
|
+
.option('--dry-run', 'Display the execution plan without running it')
|
|
130
131
|
.action(async (skill, input, options) => {
|
|
131
132
|
try {
|
|
132
133
|
const { runRun } = await import('../src/commands/run.js');
|
package/package.json
CHANGED
package/src/commands/run.js
CHANGED
|
@@ -1,34 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* run.js —
|
|
2
|
+
* run.js — Executes a skill workflow (or displays the plan in dry-run mode)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import * as p from '@clack/prompts';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { ensureProjectRoot } from '../utils/files.js';
|
|
8
|
-
import { orchestrate } from '../utils/orchestrator-io.js';
|
|
8
|
+
import { orchestrate, finalizeWorkflowTrace } from '../utils/orchestrator-io.js';
|
|
9
|
+
import { execute } from '../utils/executor.js';
|
|
10
|
+
import { createClaudeCodeProvider } from '../utils/providers/claude-code.js';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* @param {string} skillName - Name of the skill to run
|
|
15
|
-
* @param {string} [input=''] - Optional input text for the skill
|
|
16
|
-
* @param {object} [options={}] - Options
|
|
17
|
-
* @param {string} [options.profile='max'] - Model profile (max, balanced, fast)
|
|
13
|
+
* Displays the execution plan without running it.
|
|
14
|
+
* @param {object} plan
|
|
15
|
+
* @param {object} dispatchInfoMap
|
|
18
16
|
*/
|
|
19
|
-
|
|
20
|
-
const root = ensureProjectRoot();
|
|
21
|
-
const { profile = 'max' } = options;
|
|
22
|
-
|
|
23
|
-
p.intro(chalk.bold.cyan(' Guild — Run: ' + skillName + ' '));
|
|
24
|
-
|
|
25
|
-
const { plan, dispatchInfoMap } = await orchestrate(skillName, input, {
|
|
26
|
-
profile,
|
|
27
|
-
projectRoot: root,
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
p.log.info(chalk.gray(`Profile: ${profile} | Steps: ${plan.totalSteps}`));
|
|
31
|
-
|
|
17
|
+
function displayPlan(plan, dispatchInfoMap) {
|
|
32
18
|
for (let i = 0; i < plan.groups.length; i++) {
|
|
33
19
|
const group = plan.groups[i];
|
|
34
20
|
const label = group.parallel ? `Group ${i + 1} (parallel)` : `Group ${i + 1}`;
|
|
@@ -57,6 +43,63 @@ export async function runRun(skillName, input = '', options = {}) {
|
|
|
57
43
|
}
|
|
58
44
|
}
|
|
59
45
|
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Runs the `guild run <skill>` command.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} skillName - Name of the skill to run
|
|
52
|
+
* @param {string} [input=''] - Optional input text for the skill
|
|
53
|
+
* @param {object} [options={}] - Options
|
|
54
|
+
* @param {string} [options.profile='max'] - Model profile
|
|
55
|
+
* @param {boolean} [options.dryRun=false] - Show plan without executing
|
|
56
|
+
*/
|
|
57
|
+
export async function runRun(skillName, input = '', options = {}) {
|
|
58
|
+
const root = ensureProjectRoot();
|
|
59
|
+
const { profile = 'max', dryRun = false } = options;
|
|
60
|
+
|
|
61
|
+
p.intro(chalk.bold.cyan(' Guild — Run: ' + skillName + ' '));
|
|
62
|
+
|
|
63
|
+
const { plan, trace, dispatchInfoMap } = await orchestrate(skillName, input, {
|
|
64
|
+
profile,
|
|
65
|
+
projectRoot: root,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
p.log.info(chalk.gray(`Profile: ${profile} | Steps: ${plan.totalSteps}`));
|
|
69
|
+
|
|
70
|
+
if (dryRun) {
|
|
71
|
+
displayPlan(plan, dispatchInfoMap);
|
|
72
|
+
p.outro(chalk.gray('Plan generated (dry-run). No steps were executed.'));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Real execution
|
|
77
|
+
const provider = createClaudeCodeProvider({ projectRoot: root });
|
|
78
|
+
|
|
79
|
+
const finalPlan = await execute(plan, dispatchInfoMap, {
|
|
80
|
+
provider,
|
|
81
|
+
skillBody: input,
|
|
82
|
+
trace,
|
|
83
|
+
projectRoot: root,
|
|
84
|
+
|
|
85
|
+
onStepStart(step, dispatch) {
|
|
86
|
+
const roleLabel = step.role === 'system' ? chalk.yellow('system') : chalk.blue(step.role);
|
|
87
|
+
const modelLabel = dispatch.model ? chalk.gray(` (${dispatch.model})`) : '';
|
|
88
|
+
p.log.step(`${chalk.bold(step.id)} ${roleLabel}${modelLabel}`);
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
onStepEnd(step, result) {
|
|
92
|
+
const icon = result.status === 'passed' ? chalk.green('✓') : chalk.red('✗');
|
|
93
|
+
p.log.info(` ${icon} ${result.status}`);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Finalize trace
|
|
98
|
+
const { executionSummary } = finalizeWorkflowTrace(trace, finalPlan);
|
|
99
|
+
|
|
100
|
+
const statusLabel = finalPlan.status === 'completed'
|
|
101
|
+
? chalk.green('completed')
|
|
102
|
+
: chalk.red(finalPlan.status);
|
|
60
103
|
|
|
61
|
-
p.outro(
|
|
104
|
+
p.outro(`${statusLabel} | ${executionSummary}`);
|
|
62
105
|
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* executor.js — Execution loop for Guild workflow plans.
|
|
3
|
+
*
|
|
4
|
+
* Drives a plan to completion by iterating through steps, dispatching
|
|
5
|
+
* agent steps to a provider function and system steps to local commands.
|
|
6
|
+
* Sequential execution only (v1.1); parallel groups deferred to v1.2.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execFile } from 'child_process';
|
|
10
|
+
import {
|
|
11
|
+
advanceStep,
|
|
12
|
+
getNextSteps,
|
|
13
|
+
isPlanComplete,
|
|
14
|
+
} from './orchestrator.js';
|
|
15
|
+
import { buildStepContext, recordStepTrace } from './orchestrator-io.js';
|
|
16
|
+
|
|
17
|
+
const SYSTEM_STEP_TIMEOUT = 120_000; // 2 minutes
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Promisified execFile wrapper that always resolves (never rejects).
|
|
21
|
+
*
|
|
22
|
+
* @param {string} cmd - Command to execute
|
|
23
|
+
* @param {string[]} args - Arguments
|
|
24
|
+
* @param {object} opts - execFile options
|
|
25
|
+
* @returns {Promise<{ stdout: string, stderr: string, exitCode: number }>}
|
|
26
|
+
*/
|
|
27
|
+
function execFileAsync(cmd, args, opts) {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
execFile(cmd, args, opts, (error, stdout, stderr) => {
|
|
30
|
+
resolve({
|
|
31
|
+
stdout: stdout || '',
|
|
32
|
+
stderr: stderr || (error && error.message) || '',
|
|
33
|
+
exitCode: error ? (typeof error.code === 'number' ? error.code : 1) : 0,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Executes a system step by running its commands or handling delegation.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} step - System step definition
|
|
43
|
+
* @param {object} [options={}] - Options
|
|
44
|
+
* @param {string} [options.projectRoot=process.cwd()] - Working directory for commands
|
|
45
|
+
* @returns {Promise<{ status: string, output: string }>}
|
|
46
|
+
*/
|
|
47
|
+
async function executeSystemStep(step, options = {}) {
|
|
48
|
+
const { projectRoot = process.cwd() } = options;
|
|
49
|
+
|
|
50
|
+
if (step.commands && step.commands.length > 0) {
|
|
51
|
+
const outputs = [];
|
|
52
|
+
for (const cmd of step.commands) {
|
|
53
|
+
// v1.1: simple split — commands with quoted args or shell features
|
|
54
|
+
// are not supported. Use simple commands like "npm test".
|
|
55
|
+
const [bin, ...args] = cmd.split(' ');
|
|
56
|
+
const result = await execFileAsync(bin, args, {
|
|
57
|
+
cwd: projectRoot,
|
|
58
|
+
timeout: SYSTEM_STEP_TIMEOUT,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (result.exitCode !== 0) {
|
|
62
|
+
return {
|
|
63
|
+
status: 'failed',
|
|
64
|
+
output: result.stderr || result.stdout || `Command failed: ${cmd}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
outputs.push(result.stdout);
|
|
68
|
+
}
|
|
69
|
+
return { status: 'passed', output: outputs.join('\n') };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (step.delegatesTo) {
|
|
73
|
+
return { status: 'passed', output: `Delegation to "${step.delegatesTo}" skipped (v1.1)` };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { status: 'passed', output: 'System step completed' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Finds a step definition by ID across all groups in a plan.
|
|
81
|
+
*
|
|
82
|
+
* @param {object} plan - Execution plan
|
|
83
|
+
* @param {string} stepId - Step ID to find
|
|
84
|
+
* @returns {object|null}
|
|
85
|
+
*/
|
|
86
|
+
function findStepInPlan(plan, stepId) {
|
|
87
|
+
for (const group of plan.groups) {
|
|
88
|
+
for (const step of group.steps) {
|
|
89
|
+
if (step.id === stepId) return step;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Executes a workflow plan to completion.
|
|
97
|
+
*
|
|
98
|
+
* Drives the orchestrator state machine by repeatedly calling getNextSteps,
|
|
99
|
+
* dispatching each step (agent via provider, system via local commands),
|
|
100
|
+
* and advancing the plan with the result.
|
|
101
|
+
*
|
|
102
|
+
* @param {import('./orchestrator.js').ExecutionPlan} plan - Initial execution plan
|
|
103
|
+
* @param {Object.<string, import('./orchestrator-io.js').StepDispatchInfo>} dispatchInfoMap - Dispatch info per step
|
|
104
|
+
* @param {object} [options={}] - Options
|
|
105
|
+
* @param {Function} options.provider - Agent step provider: (step, dispatch, context) => { status, output, outcome?, error?, tokens? }
|
|
106
|
+
* @param {object} [options.trace] - Trace context for recording step executions
|
|
107
|
+
* @param {string} [options.projectRoot] - Working directory for system commands
|
|
108
|
+
* @param {string} [options.skillBody=''] - Skill body text for context building
|
|
109
|
+
* @param {Function} [options.onStepStart] - Callback before each step: (step, dispatch) => void
|
|
110
|
+
* @param {Function} [options.onStepEnd] - Callback after each step: (step, result) => void
|
|
111
|
+
* @returns {Promise<import('./orchestrator.js').ExecutionPlan>} Final plan state
|
|
112
|
+
*/
|
|
113
|
+
export async function execute(plan, dispatchInfoMap, options = {}) {
|
|
114
|
+
const {
|
|
115
|
+
provider,
|
|
116
|
+
trace,
|
|
117
|
+
projectRoot,
|
|
118
|
+
skillBody = '',
|
|
119
|
+
onStepStart,
|
|
120
|
+
onStepEnd,
|
|
121
|
+
} = options;
|
|
122
|
+
|
|
123
|
+
let currentPlan = plan;
|
|
124
|
+
let emptyIterations = 0;
|
|
125
|
+
const MAX_EMPTY_ITERATIONS = 100;
|
|
126
|
+
|
|
127
|
+
while (!isPlanComplete(currentPlan)) {
|
|
128
|
+
const { steps, skipped } = getNextSteps(currentPlan);
|
|
129
|
+
|
|
130
|
+
// Advance skipped steps first
|
|
131
|
+
for (const stepId of skipped) {
|
|
132
|
+
currentPlan = advanceStep(currentPlan, stepId, { status: 'skipped' });
|
|
133
|
+
|
|
134
|
+
if (trace) {
|
|
135
|
+
const step = findStepInPlan(currentPlan, stepId);
|
|
136
|
+
const dispatch = dispatchInfoMap[stepId] || {};
|
|
137
|
+
if (step) {
|
|
138
|
+
recordStepTrace(trace, step, currentPlan.stepStates[stepId], dispatch);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// If no executable steps remain, check completion again
|
|
144
|
+
if (steps.length === 0) {
|
|
145
|
+
if (isPlanComplete(currentPlan)) break;
|
|
146
|
+
if (++emptyIterations > MAX_EMPTY_ITERATIONS) {
|
|
147
|
+
currentPlan = { ...currentPlan, status: 'aborted' };
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
emptyIterations = 0;
|
|
153
|
+
|
|
154
|
+
// v1.1: sequential execution — one step at a time
|
|
155
|
+
const step = steps[0];
|
|
156
|
+
const dispatch = dispatchInfoMap[step.id] || {};
|
|
157
|
+
|
|
158
|
+
onStepStart?.(step, dispatch);
|
|
159
|
+
|
|
160
|
+
let result;
|
|
161
|
+
if (step.role === 'system') {
|
|
162
|
+
result = await executeSystemStep(step, { projectRoot });
|
|
163
|
+
} else {
|
|
164
|
+
const context = buildStepContext(step, currentPlan, { skillBody });
|
|
165
|
+
result = await provider(step, dispatch, context);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
currentPlan = advanceStep(currentPlan, step.id, result);
|
|
169
|
+
|
|
170
|
+
if (trace) {
|
|
171
|
+
recordStepTrace(trace, step, currentPlan.stepStates[step.id], dispatch);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
onStepEnd?.(step, result);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Mark plan as completed if all steps reached terminal state and plan is still running
|
|
178
|
+
if (currentPlan.status === 'running' && isPlanComplete(currentPlan)) {
|
|
179
|
+
currentPlan = { ...currentPlan, status: 'completed' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return currentPlan;
|
|
183
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TIMEOUT = 300000; // 5 minutes
|
|
4
|
+
|
|
5
|
+
function execFileAsync(cmd, args, opts) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
execFile(cmd, args, opts, (error, stdout, stderr) => {
|
|
8
|
+
if (error && error.code === 'ENOENT') {
|
|
9
|
+
reject(new Error(
|
|
10
|
+
'Claude Code CLI not found. Install it with: npm install -g @anthropic-ai/claude-code'
|
|
11
|
+
));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
resolve({
|
|
15
|
+
stdout: stdout || (error && error.stdout) || '',
|
|
16
|
+
stderr: stderr || (error && error.stderr) || '',
|
|
17
|
+
exitCode: error ? (typeof error.code === 'number' ? error.code : 1) : 0,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createClaudeCodeProvider(config) {
|
|
24
|
+
const { projectRoot, stepTimeout = DEFAULT_TIMEOUT } = config;
|
|
25
|
+
|
|
26
|
+
return async function claudeCodeProvider(step, dispatch, context) {
|
|
27
|
+
const args = ['-p', context];
|
|
28
|
+
if (dispatch.model) {
|
|
29
|
+
args.push('--model', dispatch.model);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const result = await execFileAsync('claude', args, {
|
|
33
|
+
cwd: projectRoot,
|
|
34
|
+
timeout: stepTimeout,
|
|
35
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
status: result.exitCode === 0 ? 'passed' : 'failed',
|
|
40
|
+
output: result.exitCode === 0 ? result.stdout : (result.stderr || result.stdout),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
}
|