guild-agents 1.5.0 → 2.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 +71 -67
- package/bin/guild.js +4 -85
- package/package.json +1 -1
- package/src/commands/doctor.js +11 -33
- package/src/commands/init.js +1 -1
- package/src/templates/skills/build-feature/SKILL.md +7 -38
- package/src/templates/skills/build-feature/evals/evals.json +2 -2
- package/src/templates/skills/council/SKILL.md +4 -14
- package/src/templates/skills/council/evals/evals.json +3 -13
- package/src/templates/skills/create-pr/SKILL.md +2 -5
- package/src/templates/skills/guild-specialize/SKILL.md +2 -5
- package/src/templates/skills/qa-cycle/SKILL.md +0 -7
- package/src/templates/skills/re-specialize/SKILL.md +0 -3
- package/src/templates/skills/session-end/SKILL.md +77 -30
- package/src/templates/skills/session-start/SKILL.md +51 -20
- package/src/utils/eval-runner.js +2 -8
- package/src/utils/generators.js +3 -4
- package/src/utils/skill-parser.js +83 -0
- package/src/utils/trigger-runner.js +1 -1
- package/src/commands/logs.js +0 -63
- package/src/commands/reset-learnings.js +0 -44
- package/src/commands/run.js +0 -105
- package/src/commands/stats.js +0 -147
- package/src/templates/agents/learnings-extractor.md +0 -49
- package/src/templates/skills/dev-flow/SKILL.md +0 -81
- package/src/templates/skills/dev-flow/evals/evals.json +0 -36
- package/src/templates/skills/dev-flow/evals/triggers.json +0 -16
- package/src/templates/skills/new-feature/SKILL.md +0 -119
- package/src/templates/skills/new-feature/evals/evals.json +0 -41
- package/src/templates/skills/new-feature/evals/triggers.json +0 -16
- package/src/templates/skills/review/SKILL.md +0 -97
- package/src/templates/skills/review/evals/evals.json +0 -43
- package/src/templates/skills/review/evals/triggers.json +0 -16
- package/src/templates/skills/status/SKILL.md +0 -100
- package/src/templates/skills/status/evals/evals.json +0 -40
- package/src/templates/skills/status/evals/triggers.json +0 -16
- package/src/templates/skills/verify/SKILL.md +0 -114
- package/src/templates/skills/verify/evals/triggers.json +0 -16
- package/src/utils/accounting.js +0 -139
- package/src/utils/dispatch-protocol.js +0 -71
- package/src/utils/dispatch.js +0 -172
- package/src/utils/executor.js +0 -293
- package/src/utils/learnings-io.js +0 -76
- package/src/utils/learnings.js +0 -204
- package/src/utils/orchestrator-io.js +0 -356
- package/src/utils/orchestrator.js +0 -590
- package/src/utils/pricing.js +0 -28
- package/src/utils/providers/claude-code.js +0 -43
- package/src/utils/skill-loader.js +0 -83
- package/src/utils/trace.js +0 -400
- package/src/utils/workflow-parser.js +0 -225
package/src/utils/dispatch.js
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dispatch.js — Validation and resolution utilities for the Guild dispatch protocol.
|
|
3
|
-
*
|
|
4
|
-
* Provides functions to validate workflow step configurations, resolve agent
|
|
5
|
-
* metadata from frontmatter, determine effective model tiers, and resolve
|
|
6
|
-
* tiers to concrete model IDs.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { existsSync, readFileSync } from 'fs';
|
|
10
|
-
import { join } from 'path';
|
|
11
|
-
import { parseFrontmatter } from './files.js';
|
|
12
|
-
import { parseSkill } from './workflow-parser.js';
|
|
13
|
-
import {
|
|
14
|
-
MODEL_TIERS,
|
|
15
|
-
FAILURE_STRATEGIES,
|
|
16
|
-
DEFAULT_AGENT_TIERS,
|
|
17
|
-
DEFAULT_MODEL_PROFILES,
|
|
18
|
-
FALLBACK_CHAIN,
|
|
19
|
-
} from './dispatch-protocol.js';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Validates a workflow step configuration object.
|
|
23
|
-
* @param {object} config - Step config with role, intent, model-tier, etc.
|
|
24
|
-
* @returns {string[]} Array of error messages (empty means valid)
|
|
25
|
-
*/
|
|
26
|
-
export function validateStepConfig(config) {
|
|
27
|
-
const errors = [];
|
|
28
|
-
|
|
29
|
-
if (!config.role) {
|
|
30
|
-
errors.push('Missing required field: role');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (!config.intent) {
|
|
34
|
-
errors.push('Missing required field: intent');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (config['model-tier'] && !MODEL_TIERS.includes(config['model-tier'])) {
|
|
38
|
-
errors.push(`Invalid model-tier: "${config['model-tier']}". Must be one of: ${MODEL_TIERS.join(', ')}`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (config['on-failure']) {
|
|
42
|
-
const isGoto = config['on-failure'].startsWith('goto:');
|
|
43
|
-
if (!FAILURE_STRATEGIES.includes(config['on-failure']) && !isGoto) {
|
|
44
|
-
errors.push(`Invalid on-failure: "${config['on-failure']}". Must be one of: ${FAILURE_STRATEGIES.join(', ')}, or goto:<step-id>`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (config['max-retries'] !== undefined) {
|
|
49
|
-
const val = config['max-retries'];
|
|
50
|
-
if (!Number.isInteger(val) || val < 1) {
|
|
51
|
-
errors.push(`Invalid max-retries: ${val}. Must be a positive integer`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return errors;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Reads agent metadata from the agent's markdown frontmatter.
|
|
60
|
-
* @param {string} role - Agent role name (e.g., 'tech-lead')
|
|
61
|
-
* @param {string} [projectRoot=process.cwd()] - Project root directory
|
|
62
|
-
* @returns {{ name: string, role: string, defaultTier: string|undefined, [key: string]: unknown } | null}
|
|
63
|
-
*/
|
|
64
|
-
export function resolveAgentMetadata(role, projectRoot = process.cwd()) {
|
|
65
|
-
const agentPath = join(projectRoot, '.claude', 'agents', `${role}.md`);
|
|
66
|
-
|
|
67
|
-
if (!existsSync(agentPath)) {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const content = readFileSync(agentPath, 'utf8');
|
|
72
|
-
const frontmatter = parseFrontmatter(content);
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
...frontmatter,
|
|
76
|
-
role,
|
|
77
|
-
defaultTier: frontmatter['default-tier'] || undefined,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Resolves the effective model tier for a workflow step using the precedence chain:
|
|
83
|
-
* 1. stepConfig['model-tier'] (explicit in workflow step)
|
|
84
|
-
* 2. agentMetadata.defaultTier (from agent frontmatter)
|
|
85
|
-
* 3. DEFAULT_AGENT_TIERS[role] (hardcoded defaults)
|
|
86
|
-
* 4. 'execution' (ultimate fallback)
|
|
87
|
-
*
|
|
88
|
-
* @param {object} stepConfig - Workflow step with role and optional model-tier
|
|
89
|
-
* @param {object|null} [agentMetadata=null] - Agent metadata from resolveAgentMetadata
|
|
90
|
-
* @returns {string} One of MODEL_TIERS values
|
|
91
|
-
*/
|
|
92
|
-
export function resolveEffectiveTier(stepConfig, agentMetadata = null) {
|
|
93
|
-
if (stepConfig['model-tier'] && MODEL_TIERS.includes(stepConfig['model-tier'])) {
|
|
94
|
-
return stepConfig['model-tier'];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (agentMetadata?.defaultTier && MODEL_TIERS.includes(agentMetadata.defaultTier)) {
|
|
98
|
-
return agentMetadata.defaultTier;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const defaultTier = DEFAULT_AGENT_TIERS[stepConfig.role];
|
|
102
|
-
if (defaultTier) {
|
|
103
|
-
return defaultTier;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return 'execution';
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Resolves a model tier to a concrete model ID using a profile.
|
|
111
|
-
* Applies the fallback chain if the tier is not found in the profile.
|
|
112
|
-
*
|
|
113
|
-
* @param {string} tier - One of MODEL_TIERS
|
|
114
|
-
* @param {string|Record<string, string>} profile - Profile name ('max', 'pro') or custom mapping
|
|
115
|
-
* @returns {string} Concrete model ID (e.g., 'claude-opus-4-6')
|
|
116
|
-
* @throws {Error} If no model can be resolved after exhausting the fallback chain
|
|
117
|
-
*/
|
|
118
|
-
export function resolveModel(tier, profile) {
|
|
119
|
-
const profileMap = typeof profile === 'string'
|
|
120
|
-
? DEFAULT_MODEL_PROFILES[profile]
|
|
121
|
-
: profile;
|
|
122
|
-
|
|
123
|
-
if (!profileMap) {
|
|
124
|
-
throw new Error(`Unknown profile: "${profile}". Available: ${Object.keys(DEFAULT_MODEL_PROFILES).join(', ')}`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
let currentTier = tier;
|
|
128
|
-
const visited = new Set();
|
|
129
|
-
|
|
130
|
-
while (currentTier) {
|
|
131
|
-
if (visited.has(currentTier)) {
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
visited.add(currentTier);
|
|
135
|
-
|
|
136
|
-
if (profileMap[currentTier]) {
|
|
137
|
-
return profileMap[currentTier];
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
currentTier = FALLBACK_CHAIN[currentTier];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
throw new Error(`Cannot resolve model for tier "${tier}": no model available in profile after fallback chain`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Extracts dispatch configuration from skill markdown content.
|
|
148
|
-
* Precedence: workflow steps (frontmatter) > null (legacy prose).
|
|
149
|
-
*
|
|
150
|
-
* Dependency direction: dispatch.js imports from workflow-parser.js.
|
|
151
|
-
* Do not reverse this — workflow-parser.js must not import from dispatch.js.
|
|
152
|
-
*
|
|
153
|
-
* @param {string} skillMarkdown - Raw SKILL.md content
|
|
154
|
-
* @returns {{ source: 'workflow', steps: Array<object> } | { source: null }}
|
|
155
|
-
* @throws {Error} If YAML frontmatter is malformed (propagated from parseSkill)
|
|
156
|
-
*/
|
|
157
|
-
export function extractDispatchConfigs(skillMarkdown) {
|
|
158
|
-
if (!skillMarkdown) {
|
|
159
|
-
return { source: null };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const skill = parseSkill(skillMarkdown);
|
|
163
|
-
|
|
164
|
-
if (skill.workflow && Array.isArray(skill.workflow.steps) && skill.workflow.steps.length > 0) {
|
|
165
|
-
return {
|
|
166
|
-
source: 'workflow',
|
|
167
|
-
steps: skill.workflow.steps,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return { source: null };
|
|
172
|
-
}
|
package/src/utils/executor.js
DELETED
|
@@ -1,293 +0,0 @@
|
|
|
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
|
-
* Supports parallel execution (v1.2) and delegation to sub-skills.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { execFile } from 'child_process';
|
|
10
|
-
import {
|
|
11
|
-
advanceStep,
|
|
12
|
-
getNextSteps,
|
|
13
|
-
isPlanComplete,
|
|
14
|
-
MAX_DELEGATION_DEPTH,
|
|
15
|
-
createExecutionPlan,
|
|
16
|
-
} from './orchestrator.js';
|
|
17
|
-
import {
|
|
18
|
-
buildStepContext,
|
|
19
|
-
recordStepTrace,
|
|
20
|
-
loadWorkflow,
|
|
21
|
-
resolveStepDispatch,
|
|
22
|
-
} from './orchestrator-io.js';
|
|
23
|
-
|
|
24
|
-
const SYSTEM_STEP_TIMEOUT = 120_000; // 2 minutes
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Promisified execFile wrapper that always resolves (never rejects).
|
|
28
|
-
*
|
|
29
|
-
* @param {string} cmd - Command to execute
|
|
30
|
-
* @param {string[]} args - Arguments
|
|
31
|
-
* @param {object} opts - execFile options
|
|
32
|
-
* @returns {Promise<{ stdout: string, stderr: string, exitCode: number }>}
|
|
33
|
-
*/
|
|
34
|
-
function execFileAsync(cmd, args, opts) {
|
|
35
|
-
return new Promise((resolve) => {
|
|
36
|
-
execFile(cmd, args, opts, (error, stdout, stderr) => {
|
|
37
|
-
resolve({
|
|
38
|
-
stdout: stdout || '',
|
|
39
|
-
stderr: stderr || (error && error.message) || '',
|
|
40
|
-
exitCode: error ? (typeof error.code === 'number' ? error.code : 1) : 0,
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Executes a system step by running its commands or handling delegation.
|
|
48
|
-
*
|
|
49
|
-
* @param {object} step - System step definition
|
|
50
|
-
* @param {object} [options={}] - Options
|
|
51
|
-
* @param {string} [options.projectRoot=process.cwd()] - Working directory for commands
|
|
52
|
-
* @returns {Promise<{ status: string, output: string }>}
|
|
53
|
-
*/
|
|
54
|
-
async function executeSystemStep(step, options = {}) {
|
|
55
|
-
const { projectRoot = process.cwd() } = options;
|
|
56
|
-
|
|
57
|
-
if (step.commands && step.commands.length > 0) {
|
|
58
|
-
const outputs = [];
|
|
59
|
-
for (const cmd of step.commands) {
|
|
60
|
-
// v1.1: simple split — commands with quoted args or shell features
|
|
61
|
-
// are not supported. Use simple commands like "npm test".
|
|
62
|
-
const [bin, ...args] = cmd.split(' ');
|
|
63
|
-
const result = await execFileAsync(bin, args, {
|
|
64
|
-
cwd: projectRoot,
|
|
65
|
-
timeout: SYSTEM_STEP_TIMEOUT,
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
if (result.exitCode !== 0) {
|
|
69
|
-
return {
|
|
70
|
-
status: 'failed',
|
|
71
|
-
output: result.stderr || result.stdout || `Command failed: ${cmd}`,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
outputs.push(result.stdout);
|
|
75
|
-
}
|
|
76
|
-
return { status: 'passed', output: outputs.join('\n') };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (step.delegatesTo) {
|
|
80
|
-
return { status: 'passed', output: `System step with delegation — handled by executeDelegation` };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return { status: 'passed', output: 'System step completed' };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Finds a step definition by ID across all groups in a plan.
|
|
88
|
-
*
|
|
89
|
-
* @param {object} plan - Execution plan
|
|
90
|
-
* @param {string} stepId - Step ID to find
|
|
91
|
-
* @returns {object|null}
|
|
92
|
-
*/
|
|
93
|
-
function findStepInPlan(plan, stepId) {
|
|
94
|
-
for (const group of plan.groups) {
|
|
95
|
-
for (const step of group.steps) {
|
|
96
|
-
if (step.id === stepId) return step;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Dispatches a single step (agent or system) and returns its result.
|
|
104
|
-
*
|
|
105
|
-
* @param {object} step - Step definition
|
|
106
|
-
* @param {object} dispatch - Dispatch info for this step
|
|
107
|
-
* @param {object} context - Execution context
|
|
108
|
-
* @param {import('./orchestrator.js').ExecutionPlan} context.currentPlan - Current plan state
|
|
109
|
-
* @param {Function} context.provider - Agent step provider
|
|
110
|
-
* @param {string} context.projectRoot - Working directory
|
|
111
|
-
* @param {string} context.skillBody - Skill body text
|
|
112
|
-
* @param {object} context.executeOptions - Full options passed to execute()
|
|
113
|
-
* @returns {Promise<{ status: string, output: string, outcome?: object, error?: string }>}
|
|
114
|
-
*/
|
|
115
|
-
async function dispatchStep(step, dispatch, context) {
|
|
116
|
-
const { currentPlan, provider, projectRoot, skillBody, executeOptions } = context;
|
|
117
|
-
|
|
118
|
-
if (step.role === 'system' && step.delegatesTo) {
|
|
119
|
-
return executeDelegation(step, executeOptions);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (step.role === 'system') {
|
|
123
|
-
return executeSystemStep(step, { projectRoot });
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const stepContext = buildStepContext(step, currentPlan, { skillBody });
|
|
127
|
-
return provider(step, dispatch, stepContext);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Executes a delegation step by loading and running the sub-skill.
|
|
132
|
-
*
|
|
133
|
-
* @param {object} step - Delegation step (with delegatesTo field)
|
|
134
|
-
* @param {object} options - Execute options from parent
|
|
135
|
-
* @returns {Promise<{ status: string, output: string, error?: string }>}
|
|
136
|
-
*/
|
|
137
|
-
async function executeDelegation(step, options) {
|
|
138
|
-
const {
|
|
139
|
-
provider,
|
|
140
|
-
trace,
|
|
141
|
-
projectRoot,
|
|
142
|
-
profile = 'max',
|
|
143
|
-
onStepStart,
|
|
144
|
-
onStepEnd,
|
|
145
|
-
delegationDepth = 0,
|
|
146
|
-
} = options;
|
|
147
|
-
|
|
148
|
-
if (delegationDepth >= MAX_DELEGATION_DEPTH) {
|
|
149
|
-
return {
|
|
150
|
-
status: 'failed',
|
|
151
|
-
output: '',
|
|
152
|
-
error: `Delegation depth limit (${MAX_DELEGATION_DEPTH}) exceeded at step "${step.id}" delegating to "${step.delegatesTo}"`,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
let subSkill;
|
|
157
|
-
try {
|
|
158
|
-
subSkill = loadWorkflow(step.delegatesTo);
|
|
159
|
-
} catch (err) {
|
|
160
|
-
return {
|
|
161
|
-
status: 'failed',
|
|
162
|
-
output: '',
|
|
163
|
-
error: `Failed to load delegated skill "${step.delegatesTo}": ${err.message}`,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const subPlan = createExecutionPlan(subSkill.workflow, {
|
|
168
|
-
skillName: subSkill.name || step.delegatesTo,
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const subDispatchMap = {};
|
|
172
|
-
for (const group of subPlan.groups) {
|
|
173
|
-
for (const s of group.steps) {
|
|
174
|
-
subDispatchMap[s.id] = resolveStepDispatch(s, { profile, projectRoot });
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const finalSubPlan = await execute(subPlan, subDispatchMap, {
|
|
179
|
-
provider,
|
|
180
|
-
trace,
|
|
181
|
-
projectRoot,
|
|
182
|
-
skillBody: subSkill.body || '',
|
|
183
|
-
onStepStart,
|
|
184
|
-
onStepEnd,
|
|
185
|
-
delegationDepth: delegationDepth + 1,
|
|
186
|
-
profile,
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
if (finalSubPlan.status === 'completed') {
|
|
190
|
-
return { status: 'passed', output: `Delegation to "${step.delegatesTo}" completed` };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
status: 'failed',
|
|
195
|
-
output: '',
|
|
196
|
-
error: `Delegated skill "${step.delegatesTo}" ended with status: ${finalSubPlan.status}`,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Executes a workflow plan to completion.
|
|
202
|
-
*
|
|
203
|
-
* Drives the orchestrator state machine by repeatedly calling getNextSteps,
|
|
204
|
-
* dispatching each step (agent via provider, system via local commands),
|
|
205
|
-
* and advancing the plan with the result. Parallel groups are dispatched
|
|
206
|
-
* concurrently via Promise.all.
|
|
207
|
-
*
|
|
208
|
-
* @param {import('./orchestrator.js').ExecutionPlan} plan - Initial execution plan
|
|
209
|
-
* @param {Object.<string, import('./orchestrator-io.js').StepDispatchInfo>} dispatchInfoMap - Dispatch info per step
|
|
210
|
-
* @param {object} [options={}] - Options
|
|
211
|
-
* @param {Function} options.provider - Agent step provider: (step, dispatch, context) => { status, output, outcome?, error?, tokens? }
|
|
212
|
-
* @param {object} [options.trace] - Trace context for recording step executions
|
|
213
|
-
* @param {string} [options.projectRoot] - Working directory for system commands
|
|
214
|
-
* @param {string} [options.skillBody=''] - Skill body text for context building
|
|
215
|
-
* @param {Function} [options.onStepStart] - Callback before each step: (step, dispatch) => void
|
|
216
|
-
* @param {Function} [options.onStepEnd] - Callback after each step: (step, result) => void
|
|
217
|
-
* @param {number} [options.delegationDepth=0] - Current delegation nesting depth
|
|
218
|
-
* @param {string} [options.profile='max'] - Model profile for delegation dispatch
|
|
219
|
-
* @returns {Promise<import('./orchestrator.js').ExecutionPlan>} Final plan state
|
|
220
|
-
*/
|
|
221
|
-
export async function execute(plan, dispatchInfoMap, options = {}) {
|
|
222
|
-
const {
|
|
223
|
-
provider,
|
|
224
|
-
trace,
|
|
225
|
-
projectRoot,
|
|
226
|
-
skillBody = '',
|
|
227
|
-
onStepStart,
|
|
228
|
-
onStepEnd,
|
|
229
|
-
} = options;
|
|
230
|
-
|
|
231
|
-
let currentPlan = plan;
|
|
232
|
-
let emptyIterations = 0;
|
|
233
|
-
const MAX_EMPTY_ITERATIONS = 100;
|
|
234
|
-
|
|
235
|
-
while (!isPlanComplete(currentPlan)) {
|
|
236
|
-
const { steps, skipped } = getNextSteps(currentPlan);
|
|
237
|
-
|
|
238
|
-
for (const stepId of skipped) {
|
|
239
|
-
currentPlan = advanceStep(currentPlan, stepId, { status: 'skipped' });
|
|
240
|
-
|
|
241
|
-
if (trace) {
|
|
242
|
-
const step = findStepInPlan(currentPlan, stepId);
|
|
243
|
-
const dispatch = dispatchInfoMap[stepId] || {};
|
|
244
|
-
if (step) {
|
|
245
|
-
recordStepTrace(trace, step, currentPlan.stepStates[stepId], dispatch);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (steps.length === 0) {
|
|
251
|
-
if (isPlanComplete(currentPlan)) break;
|
|
252
|
-
if (++emptyIterations > MAX_EMPTY_ITERATIONS) {
|
|
253
|
-
currentPlan = { ...currentPlan, status: 'aborted' };
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
emptyIterations = 0;
|
|
259
|
-
|
|
260
|
-
const dispatchContext = {
|
|
261
|
-
currentPlan,
|
|
262
|
-
provider,
|
|
263
|
-
projectRoot,
|
|
264
|
-
skillBody,
|
|
265
|
-
executeOptions: options,
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const settled = await Promise.all(
|
|
269
|
-
steps.map(async (step) => {
|
|
270
|
-
const dispatch = dispatchInfoMap[step.id] || {};
|
|
271
|
-
onStepStart?.(step, dispatch);
|
|
272
|
-
const result = await dispatchStep(step, dispatch, dispatchContext);
|
|
273
|
-
return { step, dispatch, result };
|
|
274
|
-
})
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
for (const { step, dispatch, result } of settled) {
|
|
278
|
-
currentPlan = advanceStep(currentPlan, step.id, result);
|
|
279
|
-
|
|
280
|
-
if (trace) {
|
|
281
|
-
recordStepTrace(trace, step, currentPlan.stepStates[step.id], dispatch);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
onStepEnd?.(step, result);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (currentPlan.status === 'running' && isPlanComplete(currentPlan)) {
|
|
289
|
-
currentPlan = { ...currentPlan, status: 'completed' };
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return currentPlan;
|
|
293
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* learnings-io.js — File I/O operations for compound learnings.
|
|
3
|
-
*
|
|
4
|
-
* Read, write, init, exists, and delete operations for .claude/guild/learnings.md.
|
|
5
|
-
* Separated from the pure functions in learnings.js following the trace.js pattern.
|
|
6
|
-
*
|
|
7
|
-
* NOTE: File locking for concurrent access is intentionally omitted.
|
|
8
|
-
* Concurrent workflow execution is a v2 concern — current Guild workflows
|
|
9
|
-
* are single-session and sequential.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync } from 'fs';
|
|
13
|
-
import { dirname } from 'path';
|
|
14
|
-
import { GUILD_LEARNINGS_PATH, renderEmptyLearnings } from './learnings.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Reads the learnings file from disk.
|
|
18
|
-
* Returns the raw content as a string, or null if the file does not exist.
|
|
19
|
-
* @param {string} [filePath] - Override path (default: GUILD_LEARNINGS_PATH)
|
|
20
|
-
* @returns {string | null}
|
|
21
|
-
*/
|
|
22
|
-
export function readLearnings(filePath) {
|
|
23
|
-
const target = filePath || GUILD_LEARNINGS_PATH;
|
|
24
|
-
if (!existsSync(target)) return null;
|
|
25
|
-
return readFileSync(target, 'utf8');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Writes content to the learnings file.
|
|
30
|
-
* Creates parent directories if needed.
|
|
31
|
-
* @param {string} content - Markdown content to write
|
|
32
|
-
* @param {string} [filePath] - Override path (default: GUILD_LEARNINGS_PATH)
|
|
33
|
-
*/
|
|
34
|
-
export function writeLearnings(content, filePath) {
|
|
35
|
-
const target = filePath || GUILD_LEARNINGS_PATH;
|
|
36
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
37
|
-
writeFileSync(target, content, 'utf8');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Checks whether the learnings file exists on disk.
|
|
42
|
-
* @param {string} [filePath] - Override path (default: GUILD_LEARNINGS_PATH)
|
|
43
|
-
* @returns {boolean}
|
|
44
|
-
*/
|
|
45
|
-
export function learningsExist(filePath) {
|
|
46
|
-
const target = filePath || GUILD_LEARNINGS_PATH;
|
|
47
|
-
return existsSync(target);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Initializes the learnings file with empty scaffold content.
|
|
52
|
-
* No-ops if the file already exists.
|
|
53
|
-
* @param {string} [projectName='Project'] - Project name for the header
|
|
54
|
-
* @param {string} [filePath] - Override path (default: GUILD_LEARNINGS_PATH)
|
|
55
|
-
* @returns {{ created: boolean }}
|
|
56
|
-
*/
|
|
57
|
-
export function initLearnings(projectName = 'Project', filePath) {
|
|
58
|
-
const target = filePath || GUILD_LEARNINGS_PATH;
|
|
59
|
-
if (existsSync(target)) return { created: false };
|
|
60
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
61
|
-
writeFileSync(target, renderEmptyLearnings(projectName), 'utf8');
|
|
62
|
-
return { created: true };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Deletes the learnings file from disk.
|
|
67
|
-
* Returns { deleted: false } if the file does not exist (no throw).
|
|
68
|
-
* @param {string} [filePath] - Override path (default: GUILD_LEARNINGS_PATH)
|
|
69
|
-
* @returns {{ deleted: boolean }}
|
|
70
|
-
*/
|
|
71
|
-
export function deleteLearnings(filePath) {
|
|
72
|
-
const target = filePath || GUILD_LEARNINGS_PATH;
|
|
73
|
-
if (!existsSync(target)) return { deleted: false };
|
|
74
|
-
unlinkSync(target);
|
|
75
|
-
return { deleted: true };
|
|
76
|
-
}
|