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.
Files changed (38) hide show
  1. package/README.md +5 -1
  2. package/bin/guild.js +74 -1
  3. package/package.json +12 -5
  4. package/src/commands/doctor.js +70 -1
  5. package/src/commands/logs.js +63 -0
  6. package/src/commands/reset-learnings.js +44 -0
  7. package/src/commands/run.js +62 -0
  8. package/src/templates/agents/advisor.md +1 -0
  9. package/src/templates/agents/bugfix.md +1 -0
  10. package/src/templates/agents/code-reviewer.md +1 -0
  11. package/src/templates/agents/db-migration.md +1 -0
  12. package/src/templates/agents/developer.md +1 -0
  13. package/src/templates/agents/learnings-extractor.md +49 -0
  14. package/src/templates/agents/platform-expert.md +1 -0
  15. package/src/templates/agents/product-owner.md +1 -0
  16. package/src/templates/agents/qa.md +1 -0
  17. package/src/templates/agents/tech-lead.md +1 -0
  18. package/src/templates/skills/build-feature/SKILL.md +130 -26
  19. package/src/templates/skills/council/SKILL.md +51 -4
  20. package/src/templates/skills/create-pr/SKILL.md +32 -0
  21. package/src/templates/skills/dev-flow/SKILL.md +14 -0
  22. package/src/templates/skills/guild-specialize/SKILL.md +45 -3
  23. package/src/templates/skills/new-feature/SKILL.md +33 -0
  24. package/src/templates/skills/qa-cycle/SKILL.md +48 -5
  25. package/src/templates/skills/review/SKILL.md +22 -1
  26. package/src/templates/skills/session-end/SKILL.md +27 -0
  27. package/src/templates/skills/session-start/SKILL.md +32 -0
  28. package/src/templates/skills/status/SKILL.md +19 -0
  29. package/src/utils/dispatch-protocol.js +74 -0
  30. package/src/utils/dispatch.js +172 -0
  31. package/src/utils/learnings-io.js +76 -0
  32. package/src/utils/learnings.js +204 -0
  33. package/src/utils/orchestrator-io.js +356 -0
  34. package/src/utils/orchestrator.js +590 -0
  35. package/src/utils/skill-loader.js +83 -0
  36. package/src/utils/trace.js +400 -0
  37. package/src/utils/version.js +90 -0
  38. 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
+ }