guild-agents 1.4.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 +69 -68
- package/bin/guild.js +4 -85
- package/package.json +2 -2
- package/src/commands/doctor.js +11 -33
- package/src/commands/init.js +1 -1
- package/src/templates/agents/advisor.md +0 -1
- package/src/templates/agents/developer.md +2 -2
- package/src/templates/agents/qa.md +1 -1
- package/src/templates/agents/tech-lead.md +2 -2
- package/src/templates/skills/build-feature/SKILL.md +59 -117
- package/src/templates/skills/build-feature/evals/evals.json +3 -4
- package/src/templates/skills/council/SKILL.md +6 -16
- 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 -9
- 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/db-migration.md +0 -51
- package/src/templates/agents/learnings-extractor.md +0 -49
- package/src/templates/agents/platform-expert.md +0 -92
- package/src/templates/agents/product-owner.md +0 -52
- package/src/templates/skills/dev-flow/SKILL.md +0 -83
- 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 -74
- package/src/utils/dispatch.js +0 -172
- package/src/utils/executor.js +0 -183
- 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
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* skill-loader.js — Loads and validates Guild skills from disk.
|
|
3
|
-
*
|
|
4
|
-
* Reads SKILL.md files from .claude/skills/ directories, parses them
|
|
5
|
-
* using the workflow parser, and returns structured skill objects ready
|
|
6
|
-
* for consumption by the dispatcher or doctor checks.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
10
|
-
import { join } from 'path';
|
|
11
|
-
import { parseSkill, validateWorkflow } from './workflow-parser.js';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Loads a skill by name from the skills directory.
|
|
15
|
-
* Resolves the SKILL.md file, parses it, and validates the workflow if present.
|
|
16
|
-
*
|
|
17
|
-
* @param {string} skillName - Name of the skill (directory name)
|
|
18
|
-
* @param {string} [basePath='.claude/skills'] - Base path for skills
|
|
19
|
-
* @returns {{ name: string, description: string, userInvocable: boolean, workflow: object|null, body: string, errors: string[] }}
|
|
20
|
-
* @throws {Error} If skill directory or SKILL.md not found
|
|
21
|
-
*/
|
|
22
|
-
export function loadSkill(skillName, basePath = join('.claude', 'skills')) {
|
|
23
|
-
const skillPath = join(basePath, skillName, 'SKILL.md');
|
|
24
|
-
|
|
25
|
-
if (!existsSync(skillPath)) {
|
|
26
|
-
throw new Error(`Skill not found: ${skillPath}`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const content = readFileSync(skillPath, 'utf8');
|
|
30
|
-
const skill = parseSkill(content);
|
|
31
|
-
|
|
32
|
-
const errors = [];
|
|
33
|
-
if (skill.workflow) {
|
|
34
|
-
const validationErrors = validateWorkflow(skill.workflow);
|
|
35
|
-
errors.push(...validationErrors);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return { ...skill, errors };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Loads all skills from the skills directory.
|
|
43
|
-
* Skills with invalid workflows are included with their errors populated.
|
|
44
|
-
* Missing SKILL.md files are logged as warnings but not included.
|
|
45
|
-
*
|
|
46
|
-
* @param {string} [basePath='.claude/skills'] - Base path for skills
|
|
47
|
-
* @returns {Map<string, { name: string, description: string, userInvocable: boolean, workflow: object|null, body: string, errors: string[] }>}
|
|
48
|
-
*/
|
|
49
|
-
export function loadAllSkills(basePath = join('.claude', 'skills')) {
|
|
50
|
-
const skills = new Map();
|
|
51
|
-
|
|
52
|
-
if (!existsSync(basePath)) {
|
|
53
|
-
return skills;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const dirs = readdirSync(basePath, { withFileTypes: true })
|
|
57
|
-
.filter(d => d.isDirectory())
|
|
58
|
-
.map(d => d.name);
|
|
59
|
-
|
|
60
|
-
for (const dir of dirs) {
|
|
61
|
-
const skillPath = join(basePath, dir, 'SKILL.md');
|
|
62
|
-
if (!existsSync(skillPath)) {
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const skill = loadSkill(dir, basePath);
|
|
68
|
-
skills.set(dir, skill);
|
|
69
|
-
} catch (_err) {
|
|
70
|
-
// Skip skills that fail to load entirely (malformed YAML, etc.)
|
|
71
|
-
skills.set(dir, {
|
|
72
|
-
name: dir,
|
|
73
|
-
description: '',
|
|
74
|
-
userInvocable: false,
|
|
75
|
-
workflow: null,
|
|
76
|
-
body: '',
|
|
77
|
-
errors: [`Failed to load: ${_err.message}`],
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return skills;
|
|
83
|
-
}
|
package/src/utils/trace.js
DELETED
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* trace.js — Structured trace file utilities for Guild workflows.
|
|
3
|
-
*
|
|
4
|
-
* Provides pure rendering functions (renderTrace, renderStep, renderSummary)
|
|
5
|
-
* that take data in and return strings out, with zero I/O. Also provides
|
|
6
|
-
* I/O functions (createTrace, finalizeTrace, listTraces, cleanTraces) for
|
|
7
|
-
* file operations on `.claude/guild/traces/`.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { mkdirSync, writeFileSync, existsSync, readdirSync, readFileSync, unlinkSync, statSync } from 'fs';
|
|
11
|
-
import { join, dirname } from 'path';
|
|
12
|
-
import { estimateTokens } from './learnings.js';
|
|
13
|
-
|
|
14
|
-
/** @typedef {'default' | 'verbose' | 'debug'} TraceLevel */
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @typedef {Object} TraceContext
|
|
18
|
-
* @property {string} filePath - Absolute path to the trace file
|
|
19
|
-
* @property {string} workflow - Skill/workflow name
|
|
20
|
-
* @property {TraceLevel} level - Logging level
|
|
21
|
-
* @property {Date} started - Workflow start time
|
|
22
|
-
* @property {Array<TraceStep>} steps - Recorded steps
|
|
23
|
-
* @property {number} totalTokens - Running token total
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @typedef {Object} TraceStep
|
|
28
|
-
* @property {string} role - Agent role (e.g. 'tech-lead')
|
|
29
|
-
* @property {string} intent - Brief description of what the agent was asked
|
|
30
|
-
* @property {string} tier - Tier assignment (e.g. 'reasoning', 'execution')
|
|
31
|
-
* @property {string} model - Resolved model ID
|
|
32
|
-
* @property {boolean} fallback - Whether a fallback model was used
|
|
33
|
-
* @property {string} started - ISO-8601 timestamp
|
|
34
|
-
* @property {number} duration - Duration in seconds
|
|
35
|
-
* @property {number} tokens - Token count for this step
|
|
36
|
-
* @property {string} result - 'pass', 'fail', or 'skip'
|
|
37
|
-
* @property {string} [decision] - Verbose+: why this model was chosen
|
|
38
|
-
* @property {string} [reasoning] - Verbose+: agent's self-reported reasoning
|
|
39
|
-
* @property {string} [fullPrompt] - Debug only: complete prompt sent
|
|
40
|
-
* @property {string} [fullResponse] - Debug only: complete response received
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @typedef {Object} TraceSummary
|
|
45
|
-
* @property {string} result - 'pass' or 'fail'
|
|
46
|
-
* @property {boolean} testsPass - Whether tests passed
|
|
47
|
-
* @property {boolean} lintPass - Whether lint passed
|
|
48
|
-
*/
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Resolves the trace level from CLI option flags.
|
|
52
|
-
* If both verbose and debug are set, debug wins (highest level).
|
|
53
|
-
* @param {Object} [options] - CLI options
|
|
54
|
-
* @param {boolean} [options.verbose] - Enable verbose logging
|
|
55
|
-
* @param {boolean} [options.debug] - Enable debug logging
|
|
56
|
-
* @returns {TraceLevel}
|
|
57
|
-
*/
|
|
58
|
-
export function resolveTraceLevel(options = {}) {
|
|
59
|
-
if (options.debug) return 'debug';
|
|
60
|
-
if (options.verbose) return 'verbose';
|
|
61
|
-
return 'default';
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Formats a duration in milliseconds to HH:MM:SS string.
|
|
66
|
-
* @param {number} ms - Duration in milliseconds
|
|
67
|
-
* @returns {string} Formatted as HH:MM:SS
|
|
68
|
-
*/
|
|
69
|
-
export function formatDuration(ms) {
|
|
70
|
-
const totalSeconds = Math.floor(ms / 1000);
|
|
71
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
72
|
-
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
73
|
-
const seconds = totalSeconds % 60;
|
|
74
|
-
return [
|
|
75
|
-
String(hours).padStart(2, '0'),
|
|
76
|
-
String(minutes).padStart(2, '0'),
|
|
77
|
-
String(seconds).padStart(2, '0'),
|
|
78
|
-
].join(':');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Creates a new trace context object. Creates the traces directory if needed.
|
|
83
|
-
* Does NOT write anything to disk — writing happens on finalize (lazy).
|
|
84
|
-
* @param {string} workflowName - Skill/workflow name (e.g. 'build-feature')
|
|
85
|
-
* @param {TraceLevel} level - Logging level
|
|
86
|
-
* @param {string} [tracesDir] - Directory for trace files (default: .claude/guild/traces/)
|
|
87
|
-
* @returns {TraceContext}
|
|
88
|
-
*/
|
|
89
|
-
export function createTrace(workflowName, level, tracesDir) {
|
|
90
|
-
const dir = tracesDir || join('.claude', 'guild', 'traces');
|
|
91
|
-
mkdirSync(dir, { recursive: true });
|
|
92
|
-
|
|
93
|
-
// Ensure traces directory is gitignored (local-only artifacts)
|
|
94
|
-
const gitignorePath = join(dir, '.gitignore');
|
|
95
|
-
if (!existsSync(gitignorePath)) {
|
|
96
|
-
writeFileSync(gitignorePath, '*\n!.gitignore\n', 'utf8');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const now = new Date();
|
|
100
|
-
const timestamp = now.toISOString()
|
|
101
|
-
.replace(/[-:]/g, '')
|
|
102
|
-
.replace(/\.\d{3}Z$/, '')
|
|
103
|
-
.replace('T', 'T');
|
|
104
|
-
const filename = `guild-trace-${timestamp}.md`;
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
filePath: join(dir, filename),
|
|
108
|
-
workflow: workflowName,
|
|
109
|
-
level,
|
|
110
|
-
started: now,
|
|
111
|
-
steps: [],
|
|
112
|
-
totalTokens: 0,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Records a step in the trace context. Pushes step data and updates token total.
|
|
118
|
-
* @param {TraceContext} traceCtx - The trace context from createTrace
|
|
119
|
-
* @param {TraceStep} stepData - The step data to record
|
|
120
|
-
* @returns {TraceContext} The same context (for chaining)
|
|
121
|
-
*/
|
|
122
|
-
export function recordStep(traceCtx, stepData) {
|
|
123
|
-
traceCtx.steps.push(stepData);
|
|
124
|
-
traceCtx.totalTokens += stepData.tokens || 0;
|
|
125
|
-
return traceCtx;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Renders a single step section as Markdown. PURE function — no I/O.
|
|
130
|
-
* Only includes decision/reasoning for verbose+. Only includes
|
|
131
|
-
* fullPrompt/fullResponse for debug level.
|
|
132
|
-
* @param {TraceStep} step - Step data
|
|
133
|
-
* @param {number} stepNumber - 1-based step index
|
|
134
|
-
* @param {TraceLevel} level - Current trace level
|
|
135
|
-
* @returns {string} Markdown string for the step
|
|
136
|
-
*/
|
|
137
|
-
export function renderStep(step, stepNumber, level) {
|
|
138
|
-
const lines = [];
|
|
139
|
-
lines.push(`### Step ${stepNumber} — ${step.role}: ${step.intent}`);
|
|
140
|
-
lines.push('');
|
|
141
|
-
lines.push('| Field | Value |');
|
|
142
|
-
lines.push('|-------|-------|');
|
|
143
|
-
lines.push(`| Tier | ${step.tier} |`);
|
|
144
|
-
lines.push(`| Model | ${step.model} |`);
|
|
145
|
-
lines.push(`| Fallback | ${step.fallback ? 'yes' : 'no'} |`);
|
|
146
|
-
lines.push(`| Started | ${step.started} |`);
|
|
147
|
-
lines.push(`| Duration | ${step.duration}s |`);
|
|
148
|
-
lines.push(`| Tokens | ${step.tokens} |`);
|
|
149
|
-
lines.push(`| Result | ${step.result} |`);
|
|
150
|
-
|
|
151
|
-
if ((level === 'verbose' || level === 'debug') && step.decision) {
|
|
152
|
-
lines.push('');
|
|
153
|
-
lines.push(`**Decision:** ${step.decision}`);
|
|
154
|
-
}
|
|
155
|
-
if ((level === 'verbose' || level === 'debug') && step.reasoning) {
|
|
156
|
-
lines.push(`**Reasoning:** ${step.reasoning}`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (level === 'debug' && step.fullPrompt) {
|
|
160
|
-
lines.push('');
|
|
161
|
-
lines.push('<details>');
|
|
162
|
-
lines.push('<summary>Full prompt</summary>');
|
|
163
|
-
lines.push('');
|
|
164
|
-
lines.push(step.fullPrompt);
|
|
165
|
-
lines.push('');
|
|
166
|
-
lines.push('</details>');
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (level === 'debug' && step.fullResponse) {
|
|
170
|
-
lines.push('');
|
|
171
|
-
lines.push('<details>');
|
|
172
|
-
lines.push('<summary>Full response</summary>');
|
|
173
|
-
lines.push('');
|
|
174
|
-
lines.push(step.fullResponse);
|
|
175
|
-
lines.push('');
|
|
176
|
-
lines.push('</details>');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return lines.join('\n');
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Renders the summary section as Markdown. PURE function — no I/O.
|
|
184
|
-
* @param {TraceSummary} summary - Summary data
|
|
185
|
-
* @param {number} totalTokens - Total tokens across all steps
|
|
186
|
-
* @param {string} duration - Formatted duration string (HH:MM:SS)
|
|
187
|
-
* @returns {string} Markdown string for the summary
|
|
188
|
-
*/
|
|
189
|
-
export function renderSummary(summary, totalTokens, duration) {
|
|
190
|
-
const lines = [];
|
|
191
|
-
lines.push('## Summary');
|
|
192
|
-
lines.push('');
|
|
193
|
-
lines.push(`- **Result**: ${summary.result}`);
|
|
194
|
-
lines.push(`- **Tests**: ${summary.testsPass ? 'pass' : 'fail'}`);
|
|
195
|
-
lines.push(`- **Lint**: ${summary.lintPass ? 'pass' : 'fail'}`);
|
|
196
|
-
lines.push(`- **Total tokens**: ${totalTokens}`);
|
|
197
|
-
lines.push(`- **Duration**: ${duration}`);
|
|
198
|
-
return lines.join('\n');
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Renders a complete trace as a Markdown string. PURE function — no I/O.
|
|
203
|
-
* @param {TraceContext} traceCtx - Trace context with all steps
|
|
204
|
-
* @param {TraceSummary} summary - Final summary data
|
|
205
|
-
* @param {Date} [finished] - Finish time (default: new Date())
|
|
206
|
-
* @returns {string} Complete Markdown content
|
|
207
|
-
*/
|
|
208
|
-
export function renderTrace(traceCtx, summary, finished) {
|
|
209
|
-
const end = finished || new Date();
|
|
210
|
-
const durationMs = end.getTime() - traceCtx.started.getTime();
|
|
211
|
-
const durationStr = formatDuration(durationMs);
|
|
212
|
-
|
|
213
|
-
const lines = [];
|
|
214
|
-
|
|
215
|
-
// Header
|
|
216
|
-
lines.push(`# Guild Trace — ${traceCtx.workflow}`);
|
|
217
|
-
lines.push('');
|
|
218
|
-
lines.push(`> Level: ${traceCtx.level}`);
|
|
219
|
-
lines.push(`> Started: ${traceCtx.started.toISOString()}`);
|
|
220
|
-
lines.push(`> Finished: ${end.toISOString()}`);
|
|
221
|
-
lines.push(`> Duration: ${durationStr}`);
|
|
222
|
-
lines.push(`> Total tokens: ${traceCtx.totalTokens}`);
|
|
223
|
-
lines.push(`> Result: ${summary.result}`);
|
|
224
|
-
|
|
225
|
-
// Steps
|
|
226
|
-
lines.push('');
|
|
227
|
-
lines.push('## Steps');
|
|
228
|
-
|
|
229
|
-
for (let i = 0; i < traceCtx.steps.length; i++) {
|
|
230
|
-
lines.push('');
|
|
231
|
-
lines.push(renderStep(traceCtx.steps[i], i + 1, traceCtx.level));
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Summary
|
|
235
|
-
lines.push('');
|
|
236
|
-
lines.push(renderSummary(summary, traceCtx.totalTokens, durationStr));
|
|
237
|
-
|
|
238
|
-
return lines.join('\n');
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** Maximum token budget for execution summaries. */
|
|
242
|
-
export const EXECUTION_SUMMARY_BUDGET = 500;
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Generates a compact execution summary for context injection.
|
|
246
|
-
* Designed for the learnings-extractor agent which runs on tier routine (Haiku).
|
|
247
|
-
* Pure function — no I/O.
|
|
248
|
-
*
|
|
249
|
-
* Dependency direction: trace.js imports estimateTokens from learnings.js.
|
|
250
|
-
* learnings.js must NOT import from trace.js.
|
|
251
|
-
*
|
|
252
|
-
* @param {TraceContext} traceCtx - The trace context with recorded steps
|
|
253
|
-
* @param {TraceSummary} summary - Final summary data (result, testsPass, lintPass)
|
|
254
|
-
* @param {Date} [finished] - Finish time (default: new Date())
|
|
255
|
-
* @returns {string} Plain text summary, estimated at <= 500 tokens
|
|
256
|
-
*/
|
|
257
|
-
export function generateExecutionSummary(traceCtx, summary, finished) {
|
|
258
|
-
const end = finished || new Date();
|
|
259
|
-
const durationMs = end.getTime() - traceCtx.started.getTime();
|
|
260
|
-
const durationStr = formatDuration(durationMs);
|
|
261
|
-
|
|
262
|
-
const headerLines = [
|
|
263
|
-
`Workflow: ${traceCtx.workflow}`,
|
|
264
|
-
`Result: ${summary.result}`,
|
|
265
|
-
`Steps: ${traceCtx.steps.length}`,
|
|
266
|
-
`Tokens: ${traceCtx.totalTokens}`,
|
|
267
|
-
`Duration: ${durationStr}`,
|
|
268
|
-
];
|
|
269
|
-
|
|
270
|
-
const header = headerLines.join('\n') + '\n';
|
|
271
|
-
|
|
272
|
-
if (traceCtx.steps.length === 0) {
|
|
273
|
-
return header;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const stepLines = traceCtx.steps.map((step, i) =>
|
|
277
|
-
`${i + 1}. ${step.role}: ${step.intent} → ${step.result} (${step.tokens} tokens, ${step.duration}s)`
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
// Try full summary first
|
|
281
|
-
const full = header + '\n' + stepLines.join('\n');
|
|
282
|
-
if (estimateTokens(full) <= EXECUTION_SUMMARY_BUDGET) {
|
|
283
|
-
return full;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Truncate step lines until within budget
|
|
287
|
-
const included = [];
|
|
288
|
-
for (let i = 0; i < stepLines.length; i++) {
|
|
289
|
-
const omissionNotice = `\n... (${traceCtx.steps.length - (i + 1)} steps omitted)`;
|
|
290
|
-
const candidate = header + '\n' + [...included, stepLines[i]].join('\n') + omissionNotice;
|
|
291
|
-
if (estimateTokens(candidate) > EXECUTION_SUMMARY_BUDGET) {
|
|
292
|
-
break;
|
|
293
|
-
}
|
|
294
|
-
included.push(stepLines[i]);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const omitted = traceCtx.steps.length - included.length;
|
|
298
|
-
if (included.length === 0) {
|
|
299
|
-
return header + `\n... (${omitted} steps omitted)`;
|
|
300
|
-
}
|
|
301
|
-
return header + '\n' + included.join('\n') + `\n... (${omitted} steps omitted)`;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Finalizes a trace: computes duration, renders markdown, writes to disk.
|
|
306
|
-
* @param {TraceContext} traceCtx - The trace context
|
|
307
|
-
* @param {TraceSummary} summary - Final summary data (result, testsPass, lintPass)
|
|
308
|
-
* @returns {{ filePath: string, duration: number, totalTokens: number, executionSummary: string }}
|
|
309
|
-
*/
|
|
310
|
-
export function finalizeTrace(traceCtx, summary) {
|
|
311
|
-
const finished = new Date();
|
|
312
|
-
const durationMs = finished.getTime() - traceCtx.started.getTime();
|
|
313
|
-
const content = renderTrace(traceCtx, summary, finished);
|
|
314
|
-
|
|
315
|
-
// Ensure directory exists (may have been cleaned between create and finalize)
|
|
316
|
-
const dir = dirname(traceCtx.filePath);
|
|
317
|
-
mkdirSync(dir, { recursive: true });
|
|
318
|
-
|
|
319
|
-
writeFileSync(traceCtx.filePath, content, 'utf8');
|
|
320
|
-
|
|
321
|
-
const executionSummary = generateExecutionSummary(traceCtx, summary, finished);
|
|
322
|
-
|
|
323
|
-
return {
|
|
324
|
-
filePath: traceCtx.filePath,
|
|
325
|
-
duration: durationMs,
|
|
326
|
-
totalTokens: traceCtx.totalTokens,
|
|
327
|
-
executionSummary,
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Lists all trace files in the traces directory, sorted newest first.
|
|
333
|
-
* Parses the header of each file to extract metadata.
|
|
334
|
-
* Returns empty array if directory does not exist (no throw).
|
|
335
|
-
* @param {string} [tracesDir] - Directory to scan (default: .claude/guild/traces/)
|
|
336
|
-
* @returns {Array<{ filePath: string, workflow: string, date: string, level: string, result: string }>}
|
|
337
|
-
*/
|
|
338
|
-
export function listTraces(tracesDir) {
|
|
339
|
-
const dir = tracesDir || join('.claude', 'guild', 'traces');
|
|
340
|
-
if (!existsSync(dir)) return [];
|
|
341
|
-
|
|
342
|
-
const files = readdirSync(dir)
|
|
343
|
-
.filter(f => f.startsWith('guild-trace-') && f.endsWith('.md'))
|
|
344
|
-
.sort()
|
|
345
|
-
.reverse();
|
|
346
|
-
|
|
347
|
-
return files.map(filename => {
|
|
348
|
-
const filePath = join(dir, filename);
|
|
349
|
-
const head = readFileSync(filePath, 'utf8').split('\n').slice(0, 10).join('\n');
|
|
350
|
-
|
|
351
|
-
const workflowMatch = head.match(/^# Guild Trace — (.+)$/m);
|
|
352
|
-
const levelMatch = head.match(/^> Level: (.+)$/m);
|
|
353
|
-
const resultMatch = head.match(/^> Result: (.+)$/m);
|
|
354
|
-
const startedMatch = head.match(/^> Started: (.+)$/m);
|
|
355
|
-
|
|
356
|
-
return {
|
|
357
|
-
filePath,
|
|
358
|
-
workflow: workflowMatch ? workflowMatch[1] : 'unknown',
|
|
359
|
-
date: startedMatch ? startedMatch[1] : 'unknown',
|
|
360
|
-
level: levelMatch ? levelMatch[1] : 'default',
|
|
361
|
-
result: resultMatch ? resultMatch[1] : 'unknown',
|
|
362
|
-
};
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Deletes trace files older than maxAgeDays. If maxAgeDays is 0, deletes ALL traces.
|
|
368
|
-
* Returns count of deleted files. Returns 0 if directory does not exist (no throw).
|
|
369
|
-
* @param {number} maxAgeDays - Traces older than this many days are deleted (0 = delete all)
|
|
370
|
-
* @param {string} [tracesDir] - Directory to clean (default: .claude/guild/traces/)
|
|
371
|
-
* @returns {number} Count of deleted files
|
|
372
|
-
*/
|
|
373
|
-
export function cleanTraces(maxAgeDays, tracesDir) {
|
|
374
|
-
const dir = tracesDir || join('.claude', 'guild', 'traces');
|
|
375
|
-
if (!existsSync(dir)) return 0;
|
|
376
|
-
|
|
377
|
-
const files = readdirSync(dir)
|
|
378
|
-
.filter(f => f.startsWith('guild-trace-') && f.endsWith('.md'));
|
|
379
|
-
|
|
380
|
-
const now = Date.now();
|
|
381
|
-
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
|
382
|
-
let deleted = 0;
|
|
383
|
-
|
|
384
|
-
for (const filename of files) {
|
|
385
|
-
const filePath = join(dir, filename);
|
|
386
|
-
if (maxAgeDays === 0) {
|
|
387
|
-
unlinkSync(filePath);
|
|
388
|
-
deleted++;
|
|
389
|
-
} else {
|
|
390
|
-
const stat = statSync(filePath);
|
|
391
|
-
const ageMs = now - stat.mtimeMs;
|
|
392
|
-
if (ageMs > maxAgeMs) {
|
|
393
|
-
unlinkSync(filePath);
|
|
394
|
-
deleted++;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return deleted;
|
|
400
|
-
}
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* workflow-parser.js — Parser and validator for Guild declarative workflows.
|
|
3
|
-
*
|
|
4
|
-
* Parses SKILL.md files with nested YAML frontmatter (workflow definitions)
|
|
5
|
-
* using a full YAML parser. Validates workflow structure against the schema.
|
|
6
|
-
* Backward compatible: skills without a `workflow` key return workflow: null.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import YAML from 'yaml';
|
|
10
|
-
import { MODEL_TIERS, FAILURE_STRATEGIES, DEFAULT_FAILURE_STRATEGY } from './dispatch-protocol.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Extracts the raw YAML frontmatter string and body from markdown content.
|
|
14
|
-
* @param {string} content - Raw markdown content
|
|
15
|
-
* @returns {{ yaml: string, body: string } | null} Null if no frontmatter found
|
|
16
|
-
*/
|
|
17
|
-
export function extractFrontmatterBlock(content) {
|
|
18
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
19
|
-
if (!match) return null;
|
|
20
|
-
return {
|
|
21
|
-
yaml: match[1],
|
|
22
|
-
body: content.slice(match[0].length).trim(),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Parses YAML frontmatter with full nested structure support.
|
|
28
|
-
* Uses the `yaml` npm package for spec-compliant parsing.
|
|
29
|
-
* @param {string} yamlString - Raw YAML frontmatter
|
|
30
|
-
* @returns {object} Parsed frontmatter object
|
|
31
|
-
*/
|
|
32
|
-
export function parseYamlFrontmatter(yamlString) {
|
|
33
|
-
return YAML.parse(yamlString) || {};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Normalizes a raw workflow step from YAML into a consistent object shape.
|
|
38
|
-
* Converts kebab-case keys to camelCase where appropriate.
|
|
39
|
-
* @param {object} raw - Raw step object from YAML
|
|
40
|
-
* @returns {object} Normalized step
|
|
41
|
-
*/
|
|
42
|
-
function normalizeStep(raw) {
|
|
43
|
-
return {
|
|
44
|
-
id: raw.id,
|
|
45
|
-
role: raw.role,
|
|
46
|
-
intent: raw.intent,
|
|
47
|
-
requires: raw.requires || [],
|
|
48
|
-
produces: raw.produces || [],
|
|
49
|
-
modelTier: raw['model-tier'] || undefined,
|
|
50
|
-
blocking: raw.blocking !== undefined ? raw.blocking : true,
|
|
51
|
-
onFailure: raw['on-failure'] || DEFAULT_FAILURE_STRATEGY,
|
|
52
|
-
gate: raw.gate || false,
|
|
53
|
-
retry: raw.retry || undefined,
|
|
54
|
-
condition: raw.condition || undefined,
|
|
55
|
-
parallel: raw.parallel || undefined,
|
|
56
|
-
commands: raw.commands || undefined,
|
|
57
|
-
delegatesTo: raw['delegates-to'] || undefined,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Parses a SKILL.md file and extracts the workflow definition.
|
|
63
|
-
* If no `workflow` key exists in frontmatter, returns { workflow: null }
|
|
64
|
-
* to indicate the Skill uses prose-based execution (backward compatible).
|
|
65
|
-
*
|
|
66
|
-
* @param {string} content - Raw content of SKILL.md
|
|
67
|
-
* @returns {{ name: string, description: string, userInvocable: boolean, workflow: object|null, body: string }}
|
|
68
|
-
* @throws {Error} If YAML frontmatter is malformed
|
|
69
|
-
*/
|
|
70
|
-
export function parseSkill(content) {
|
|
71
|
-
const block = extractFrontmatterBlock(content);
|
|
72
|
-
if (!block) {
|
|
73
|
-
return {
|
|
74
|
-
name: '',
|
|
75
|
-
description: '',
|
|
76
|
-
userInvocable: false,
|
|
77
|
-
workflow: null,
|
|
78
|
-
body: content,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const frontmatter = parseYamlFrontmatter(block.yaml);
|
|
83
|
-
|
|
84
|
-
const skill = {
|
|
85
|
-
name: frontmatter.name || '',
|
|
86
|
-
description: frontmatter.description || '',
|
|
87
|
-
userInvocable: frontmatter['user-invocable'] === true,
|
|
88
|
-
workflow: null,
|
|
89
|
-
body: block.body,
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
if (frontmatter.workflow) {
|
|
93
|
-
const raw = frontmatter.workflow;
|
|
94
|
-
skill.workflow = {
|
|
95
|
-
version: raw.version,
|
|
96
|
-
steps: Array.isArray(raw.steps)
|
|
97
|
-
? raw.steps.map(normalizeStep)
|
|
98
|
-
: [],
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return skill;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Validates a workflow definition against the schema.
|
|
107
|
-
* Returns an array of validation errors (empty if valid).
|
|
108
|
-
*
|
|
109
|
-
* @param {object} workflow - Parsed workflow object with version and steps
|
|
110
|
-
* @returns {string[]} Array of error messages
|
|
111
|
-
*/
|
|
112
|
-
export function validateWorkflow(workflow) {
|
|
113
|
-
const errors = [];
|
|
114
|
-
|
|
115
|
-
if (workflow.version !== 1) {
|
|
116
|
-
errors.push(`Unsupported workflow version: ${workflow.version}. This Guild version supports workflow version 1.`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (!Array.isArray(workflow.steps) || workflow.steps.length === 0) {
|
|
120
|
-
errors.push('Workflow must have at least one step.');
|
|
121
|
-
return errors;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const ids = new Set();
|
|
125
|
-
|
|
126
|
-
for (const step of workflow.steps) {
|
|
127
|
-
// Required fields
|
|
128
|
-
if (!step.id) {
|
|
129
|
-
errors.push('Step missing required field: id');
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (ids.has(step.id)) {
|
|
134
|
-
errors.push(`Duplicate step id: "${step.id}"`);
|
|
135
|
-
}
|
|
136
|
-
ids.add(step.id);
|
|
137
|
-
|
|
138
|
-
if (!step.role) {
|
|
139
|
-
errors.push(`Step "${step.id}" missing required field: role`);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (!step.intent) {
|
|
143
|
-
errors.push(`Step "${step.id}" missing required field: intent`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Valid model-tier
|
|
147
|
-
if (step.modelTier && !MODEL_TIERS.includes(step.modelTier)) {
|
|
148
|
-
errors.push(`Step "${step.id}" has invalid model-tier: "${step.modelTier}"`);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Valid on-failure
|
|
152
|
-
if (step.onFailure) {
|
|
153
|
-
const isGoto = step.onFailure.startsWith('goto:');
|
|
154
|
-
if (!FAILURE_STRATEGIES.includes(step.onFailure) && !isGoto) {
|
|
155
|
-
errors.push(`Step "${step.id}" has invalid on-failure: "${step.onFailure}"`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Retry limits
|
|
160
|
-
if (step.retry) {
|
|
161
|
-
if (step.retry.max !== undefined && step.retry.max > 10) {
|
|
162
|
-
errors.push(`Step "${step.id}" retry.max exceeds limit of 10`);
|
|
163
|
-
}
|
|
164
|
-
if (step.retry.max !== undefined && step.retry.max < 1) {
|
|
165
|
-
errors.push(`Step "${step.id}" retry.max must be at least 1`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// System step validation
|
|
170
|
-
if (step.role === 'system') {
|
|
171
|
-
if (!step.commands && !step.delegatesTo && !step.gate) {
|
|
172
|
-
errors.push(`System step "${step.id}" must have commands, delegates-to, or gate`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Validate goto targets exist
|
|
178
|
-
for (const step of workflow.steps) {
|
|
179
|
-
if (step.onFailure && step.onFailure.startsWith('goto:')) {
|
|
180
|
-
const target = step.onFailure.split(':')[1];
|
|
181
|
-
if (!ids.has(target)) {
|
|
182
|
-
errors.push(`Step "${step.id}" on-failure goto target "${target}" does not exist`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return errors;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Resolves the execution order of steps considering conditions,
|
|
192
|
-
* dependencies (requires/produces), and parallel groups.
|
|
193
|
-
* Returns a flat execution plan with groupings.
|
|
194
|
-
*
|
|
195
|
-
* @param {object} workflow - Validated workflow
|
|
196
|
-
* @returns {{ groups: Array<{ steps: object[], parallel: boolean }> }} Ordered execution plan
|
|
197
|
-
*/
|
|
198
|
-
export function resolveExecutionPlan(workflow) {
|
|
199
|
-
const groups = [];
|
|
200
|
-
let i = 0;
|
|
201
|
-
const steps = workflow.steps;
|
|
202
|
-
|
|
203
|
-
while (i < steps.length) {
|
|
204
|
-
const step = steps[i];
|
|
205
|
-
|
|
206
|
-
// Check if this step has parallel peers
|
|
207
|
-
if (step.parallel && step.parallel.length > 0) {
|
|
208
|
-
const parallelIds = new Set([step.id, ...step.parallel]);
|
|
209
|
-
const parallelSteps = [];
|
|
210
|
-
|
|
211
|
-
// Collect all steps in this parallel group
|
|
212
|
-
while (i < steps.length && parallelIds.has(steps[i].id)) {
|
|
213
|
-
parallelSteps.push(steps[i]);
|
|
214
|
-
i++;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
groups.push({ steps: parallelSteps, parallel: true });
|
|
218
|
-
} else {
|
|
219
|
-
groups.push({ steps: [step], parallel: false });
|
|
220
|
-
i++;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return { groups };
|
|
225
|
-
}
|