guild-agents 0.3.1 → 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 +5 -1
- package/bin/guild.js +74 -1
- package/package.json +12 -5
- package/src/commands/doctor.js +70 -1
- package/src/commands/logs.js +63 -0
- package/src/commands/reset-learnings.js +44 -0
- package/src/commands/run.js +62 -0
- package/src/templates/agents/advisor.md +1 -0
- package/src/templates/agents/bugfix.md +1 -0
- package/src/templates/agents/code-reviewer.md +1 -0
- package/src/templates/agents/db-migration.md +1 -0
- package/src/templates/agents/developer.md +1 -0
- package/src/templates/agents/learnings-extractor.md +49 -0
- package/src/templates/agents/platform-expert.md +1 -0
- package/src/templates/agents/product-owner.md +1 -0
- package/src/templates/agents/qa.md +1 -0
- package/src/templates/agents/tech-lead.md +1 -0
- package/src/templates/skills/build-feature/SKILL.md +130 -26
- package/src/templates/skills/council/SKILL.md +51 -4
- package/src/templates/skills/create-pr/SKILL.md +32 -0
- package/src/templates/skills/dev-flow/SKILL.md +14 -0
- package/src/templates/skills/guild-specialize/SKILL.md +45 -3
- package/src/templates/skills/new-feature/SKILL.md +33 -0
- package/src/templates/skills/qa-cycle/SKILL.md +48 -5
- package/src/templates/skills/review/SKILL.md +22 -1
- package/src/templates/skills/session-end/SKILL.md +27 -0
- package/src/templates/skills/session-start/SKILL.md +32 -0
- package/src/templates/skills/status/SKILL.md +19 -0
- package/src/utils/dispatch-protocol.js +74 -0
- package/src/utils/dispatch.js +172 -0
- package/src/utils/learnings-io.js +76 -0
- package/src/utils/learnings.js +204 -0
- package/src/utils/orchestrator-io.js +356 -0
- package/src/utils/orchestrator.js +590 -0
- package/src/utils/skill-loader.js +83 -0
- package/src/utils/trace.js +400 -0
- package/src/utils/version.js +90 -0
- package/src/utils/workflow-parser.js +225 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* orchestrator-io.js — I/O layer for the Guild runtime orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Handles file I/O, skill loading, dispatch resolution, learnings injection,
|
|
5
|
+
* and trace management. Depends on the pure orchestrator.js for plan logic.
|
|
6
|
+
*
|
|
7
|
+
* Re-exports all public functions from orchestrator.js for a single import point.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { loadSkill } from './skill-loader.js';
|
|
12
|
+
import { resolveAgentMetadata, resolveEffectiveTier, resolveModel } from './dispatch.js';
|
|
13
|
+
import { createTrace, recordStep, finalizeTrace } from './trace.js';
|
|
14
|
+
import { buildContextInjection } from './learnings.js';
|
|
15
|
+
import { readLearnings } from './learnings-io.js';
|
|
16
|
+
import {
|
|
17
|
+
createExecutionPlan,
|
|
18
|
+
advanceStep,
|
|
19
|
+
getNextSteps,
|
|
20
|
+
isPlanComplete,
|
|
21
|
+
parseCondition,
|
|
22
|
+
evaluateCondition,
|
|
23
|
+
shouldRetry,
|
|
24
|
+
resolveFailureTarget,
|
|
25
|
+
expandDelegation,
|
|
26
|
+
} from './orchestrator.js';
|
|
27
|
+
|
|
28
|
+
// Re-export all public functions from orchestrator.js
|
|
29
|
+
export {
|
|
30
|
+
createExecutionPlan,
|
|
31
|
+
advanceStep,
|
|
32
|
+
getNextSteps,
|
|
33
|
+
isPlanComplete,
|
|
34
|
+
parseCondition,
|
|
35
|
+
evaluateCondition,
|
|
36
|
+
shouldRetry,
|
|
37
|
+
resolveFailureTarget,
|
|
38
|
+
expandDelegation,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Also re-export constants
|
|
42
|
+
export { DEFAULT_CIRCUIT_BREAKER_LIMIT, MAX_DELEGATION_DEPTH } from './orchestrator.js';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {Object} StepDispatchInfo
|
|
46
|
+
* @property {'system'|'agent'} role - Whether this is a system or agent step
|
|
47
|
+
* @property {string|null} tier - Resolved model tier (null for system steps)
|
|
48
|
+
* @property {string|null} model - Resolved concrete model ID (null for system steps)
|
|
49
|
+
* @property {boolean} fallback - Whether a fallback model was used
|
|
50
|
+
* @property {object|null} agentMetadata - Agent frontmatter metadata (null for system steps)
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolves dispatch information for a workflow step.
|
|
55
|
+
* System role steps return minimal dispatch info (no model).
|
|
56
|
+
* Agent steps resolve tier and model using the dispatch utilities.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} step - Workflow step definition
|
|
59
|
+
* @param {object} [options={}] - Options
|
|
60
|
+
* @param {string} [options.profile='max'] - Model profile name
|
|
61
|
+
* @param {string} [options.projectRoot=process.cwd()] - Project root for agent lookup
|
|
62
|
+
* @returns {StepDispatchInfo}
|
|
63
|
+
*/
|
|
64
|
+
export function resolveStepDispatch(step, options = {}) {
|
|
65
|
+
const { profile = 'max', projectRoot = process.cwd() } = options;
|
|
66
|
+
|
|
67
|
+
if (step.role === 'system') {
|
|
68
|
+
return {
|
|
69
|
+
role: 'system',
|
|
70
|
+
tier: null,
|
|
71
|
+
model: null,
|
|
72
|
+
fallback: false,
|
|
73
|
+
agentMetadata: null,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const agentMetadata = resolveAgentMetadata(step.role, projectRoot);
|
|
78
|
+
|
|
79
|
+
// Build a step config object compatible with resolveEffectiveTier
|
|
80
|
+
const stepConfig = {
|
|
81
|
+
role: step.role,
|
|
82
|
+
'model-tier': step.modelTier,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const tier = resolveEffectiveTier(stepConfig, agentMetadata);
|
|
86
|
+
|
|
87
|
+
// W4: resolveModel already handles the fallback chain internally;
|
|
88
|
+
// a single try-catch is sufficient here.
|
|
89
|
+
let model;
|
|
90
|
+
let fallback;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
model = resolveModel(tier, profile);
|
|
94
|
+
fallback = false;
|
|
95
|
+
} catch {
|
|
96
|
+
model = null;
|
|
97
|
+
fallback = false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
role: 'agent',
|
|
102
|
+
tier,
|
|
103
|
+
model,
|
|
104
|
+
fallback,
|
|
105
|
+
agentMetadata,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Loads and validates a skill workflow from disk.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} skillName - Name of the skill directory
|
|
113
|
+
* @param {object} [options={}] - Options
|
|
114
|
+
* @param {string} [options.basePath] - Override base path for skills directory
|
|
115
|
+
* @returns {{ workflow: object, body: string, name: string }}
|
|
116
|
+
* @throws {Error} If skill not found, has no workflow, or has validation errors
|
|
117
|
+
*/
|
|
118
|
+
export function loadWorkflow(skillName, options = {}) {
|
|
119
|
+
const skill = options.basePath
|
|
120
|
+
? loadSkill(skillName, options.basePath)
|
|
121
|
+
: loadSkill(skillName);
|
|
122
|
+
|
|
123
|
+
if (!skill.workflow) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Skill "${skillName}" has no workflow definition. Only skills with a workflow block can be orchestrated.`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (skill.errors && skill.errors.length > 0) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Skill "${skillName}" has workflow validation errors:\n${skill.errors.join('\n')}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
workflow: skill.workflow,
|
|
137
|
+
body: skill.body,
|
|
138
|
+
name: skill.name || skillName,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Builds the context string for a step's execution.
|
|
144
|
+
* Combines step intent, learnings injection, skill body, and required step outcomes.
|
|
145
|
+
*
|
|
146
|
+
* @param {object} step - Workflow step definition
|
|
147
|
+
* @param {import('./orchestrator.js').ExecutionPlan} plan - Current execution plan
|
|
148
|
+
* @param {object} [options={}] - Options
|
|
149
|
+
* @param {string} [options.learningsPath] - Override path for learnings file
|
|
150
|
+
* @param {string} [options.skillBody=''] - Skill body (markdown prose)
|
|
151
|
+
* @returns {string} Context string for the step
|
|
152
|
+
*/
|
|
153
|
+
export function buildStepContext(step, plan, options = {}) {
|
|
154
|
+
const { skillBody = '' } = options;
|
|
155
|
+
const parts = [];
|
|
156
|
+
|
|
157
|
+
// Step intent
|
|
158
|
+
parts.push(`## Task: ${step.intent}`);
|
|
159
|
+
parts.push('');
|
|
160
|
+
|
|
161
|
+
// Learnings injection (graceful degradation if reading fails)
|
|
162
|
+
try {
|
|
163
|
+
const learningsContent = readLearnings(options.learningsPath || undefined);
|
|
164
|
+
if (learningsContent) {
|
|
165
|
+
const injection = buildContextInjection(learningsContent);
|
|
166
|
+
if (injection) {
|
|
167
|
+
parts.push(injection);
|
|
168
|
+
parts.push('');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
// Learnings reading failed — continue without them
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Skill body (prose instructions)
|
|
176
|
+
if (skillBody && skillBody.trim()) {
|
|
177
|
+
parts.push('## Skill Instructions');
|
|
178
|
+
parts.push('');
|
|
179
|
+
parts.push(skillBody.trim());
|
|
180
|
+
parts.push('');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Outcomes from required steps
|
|
184
|
+
if (step.requires && step.requires.length > 0) {
|
|
185
|
+
const requiredOutcomes = [];
|
|
186
|
+
for (const requirement of step.requires) {
|
|
187
|
+
// Find which step produces this requirement
|
|
188
|
+
for (const group of plan.groups) {
|
|
189
|
+
for (const s of group.steps) {
|
|
190
|
+
const state = plan.stepStates[s.id];
|
|
191
|
+
if (state && state.outcome && s.produces && s.produces.includes(requirement)) {
|
|
192
|
+
if (state.outcome[requirement] !== undefined) {
|
|
193
|
+
requiredOutcomes.push({ field: requirement, value: state.outcome[requirement], fromStep: s.id });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (requiredOutcomes.length > 0) {
|
|
201
|
+
parts.push('## Inputs from previous steps');
|
|
202
|
+
parts.push('');
|
|
203
|
+
for (const item of requiredOutcomes) {
|
|
204
|
+
parts.push(`### ${item.field} (from step: ${item.fromStep})`);
|
|
205
|
+
parts.push('');
|
|
206
|
+
parts.push(String(item.value));
|
|
207
|
+
parts.push('');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return parts.join('\n').trimEnd();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Records a step execution in the trace context.
|
|
217
|
+
*
|
|
218
|
+
* @param {import('./trace.js').TraceContext} traceCtx - Active trace context
|
|
219
|
+
* @param {object} step - Step definition
|
|
220
|
+
* @param {import('./orchestrator.js').StepState} stepState - Final state of the step
|
|
221
|
+
* @param {StepDispatchInfo} dispatchInfo - Resolved dispatch information
|
|
222
|
+
* @returns {import('./trace.js').TraceContext} Updated trace context (same reference)
|
|
223
|
+
*/
|
|
224
|
+
export function recordStepTrace(traceCtx, step, stepState, dispatchInfo) {
|
|
225
|
+
const started = new Date().toISOString();
|
|
226
|
+
|
|
227
|
+
return recordStep(traceCtx, {
|
|
228
|
+
role: step.role,
|
|
229
|
+
intent: step.intent,
|
|
230
|
+
tier: dispatchInfo.tier || 'system',
|
|
231
|
+
model: dispatchInfo.model || 'none',
|
|
232
|
+
fallback: dispatchInfo.fallback,
|
|
233
|
+
started,
|
|
234
|
+
// TODO(v2): capture actual wall-clock duration and token usage from step executor
|
|
235
|
+
duration: 0,
|
|
236
|
+
tokens: 0,
|
|
237
|
+
result: stepState.status === 'passed' ? 'pass' : stepState.status === 'skipped' ? 'skip' : 'fail',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Finalizes a workflow trace and produces an execution summary.
|
|
243
|
+
*
|
|
244
|
+
* testsPass and lintPass are derived from gate steps whose intent contains
|
|
245
|
+
* the keywords "test" or "lint" respectively. They can be overridden via
|
|
246
|
+
* options when the caller has better information.
|
|
247
|
+
*
|
|
248
|
+
* @param {import('./trace.js').TraceContext} traceCtx - Completed trace context
|
|
249
|
+
* @param {import('./orchestrator.js').ExecutionPlan} plan - Final execution plan
|
|
250
|
+
* @param {object} [options={}] - Optional overrides
|
|
251
|
+
* @param {boolean} [options.testsPass] - Override for testsPass (derived from gate steps if omitted)
|
|
252
|
+
* @param {boolean} [options.lintPass] - Override for lintPass (derived from gate steps if omitted)
|
|
253
|
+
* @returns {{ trace: object, executionSummary: string }}
|
|
254
|
+
*/
|
|
255
|
+
export function finalizeWorkflowTrace(traceCtx, plan, options = {}) {
|
|
256
|
+
const allStates = Object.values(plan.stepStates);
|
|
257
|
+
const anyFailed = allStates.some(s => s.status === 'failed');
|
|
258
|
+
|
|
259
|
+
// Derive testsPass / lintPass from gate steps when not explicitly provided
|
|
260
|
+
let testsPass = options.testsPass;
|
|
261
|
+
let lintPass = options.lintPass;
|
|
262
|
+
|
|
263
|
+
if (testsPass === undefined || lintPass === undefined) {
|
|
264
|
+
for (const group of plan.groups) {
|
|
265
|
+
for (const step of group.steps) {
|
|
266
|
+
if (!step.gate) continue;
|
|
267
|
+
const state = plan.stepStates[step.id];
|
|
268
|
+
const passed = state && state.status === 'passed';
|
|
269
|
+
const intent = (step.intent || '').toLowerCase();
|
|
270
|
+
if (testsPass === undefined && intent.includes('test')) {
|
|
271
|
+
testsPass = passed;
|
|
272
|
+
}
|
|
273
|
+
if (lintPass === undefined && intent.includes('lint')) {
|
|
274
|
+
lintPass = passed;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Fall back to true when no matching gate step found (v1 behaviour)
|
|
279
|
+
if (testsPass === undefined) testsPass = true;
|
|
280
|
+
if (lintPass === undefined) lintPass = true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const summary = {
|
|
284
|
+
result: anyFailed || plan.status === 'aborted' || plan.status === 'circuit-breaker' ? 'fail' : 'pass',
|
|
285
|
+
testsPass,
|
|
286
|
+
lintPass,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const trace = finalizeTrace(traceCtx, summary);
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
trace,
|
|
293
|
+
executionSummary: trace.executionSummary,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Main entry point for the orchestrator.
|
|
299
|
+
* Loads the workflow, creates an execution plan, resolves dispatch for all steps,
|
|
300
|
+
* and creates a trace context. In v1, this produces the plan only (no step execution).
|
|
301
|
+
*
|
|
302
|
+
* @param {string} skillName - Name of the skill to orchestrate
|
|
303
|
+
* @param {string} [input=''] - User input / feature description
|
|
304
|
+
* @param {object} [options={}] - Options
|
|
305
|
+
* @param {string} [options.profile='max'] - Model profile
|
|
306
|
+
* @param {string} [options.projectRoot=process.cwd()] - Project root
|
|
307
|
+
* @param {string} [options.basePath] - Override skills base path
|
|
308
|
+
* @param {string} [options.tracesDir] - Override traces directory
|
|
309
|
+
* @param {string} [options.traceLevel='default'] - Trace level
|
|
310
|
+
* @param {number} [options.circuitBreakerLimit] - Override circuit breaker limit
|
|
311
|
+
* @returns {Promise<{
|
|
312
|
+
* plan: import('./orchestrator.js').ExecutionPlan,
|
|
313
|
+
* trace: import('./trace.js').TraceContext,
|
|
314
|
+
* dispatchInfoMap: Object.<string, StepDispatchInfo>,
|
|
315
|
+
* input: string
|
|
316
|
+
* }>}
|
|
317
|
+
*/
|
|
318
|
+
export async function orchestrate(skillName, input = '', options = {}) {
|
|
319
|
+
const {
|
|
320
|
+
profile = 'max',
|
|
321
|
+
projectRoot = process.cwd(),
|
|
322
|
+
tracesDir,
|
|
323
|
+
traceLevel = 'default',
|
|
324
|
+
circuitBreakerLimit,
|
|
325
|
+
} = options;
|
|
326
|
+
|
|
327
|
+
// Load workflow
|
|
328
|
+
const loadOpts = {};
|
|
329
|
+
if (options.basePath) {
|
|
330
|
+
loadOpts.basePath = options.basePath;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const { workflow, name } = loadWorkflow(skillName, loadOpts);
|
|
334
|
+
|
|
335
|
+
// Create execution plan
|
|
336
|
+
const planOptions = { skillName: name || skillName };
|
|
337
|
+
if (circuitBreakerLimit !== undefined) {
|
|
338
|
+
planOptions.circuitBreakerLimit = circuitBreakerLimit;
|
|
339
|
+
}
|
|
340
|
+
const plan = createExecutionPlan(workflow, planOptions);
|
|
341
|
+
|
|
342
|
+
// Resolve dispatch info for all steps (for plan introspection/display)
|
|
343
|
+
const dispatchInfoMap = {};
|
|
344
|
+
for (const group of plan.groups) {
|
|
345
|
+
for (const step of group.steps) {
|
|
346
|
+
dispatchInfoMap[step.id] = resolveStepDispatch(step, { profile, projectRoot });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// W8: return dispatchInfoMap and input as separate result fields instead of
|
|
351
|
+
// mutating traceCtx, keeping the trace context pure.
|
|
352
|
+
const traceDirResolved = tracesDir || join(projectRoot, '.claude', 'guild', 'traces');
|
|
353
|
+
const traceCtx = createTrace(skillName, traceLevel, traceDirResolved);
|
|
354
|
+
|
|
355
|
+
return { plan, trace: traceCtx, dispatchInfoMap, input };
|
|
356
|
+
}
|