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 CHANGED
@@ -123,10 +123,11 @@ program
123
123
  // guild run
124
124
  program
125
125
  .command('run')
126
- .description('Display the execution plan for a skill')
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, balanced, fast)', '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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guild-agents",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Specification-driven development CLI for Claude Code — think before you build",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,34 +1,20 @@
1
1
  /**
2
- * run.js — Displays the execution plan for a skill (dry-run)
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
- * Runs the `guild run <skill>` command.
12
- * Loads the skill workflow, resolves dispatch info, and displays the plan.
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
- export async function runRun(skillName, input = '', options = {}) {
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(chalk.gray('Plan generated (dry-run). No steps were executed.'));
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
+ }