guild-agents 1.1.0 → 1.3.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 +8 -1
- package/bin/guild.js +84 -0
- package/package.json +5 -2
- package/src/commands/init.js +8 -1
- package/src/commands/workspace.js +92 -0
- package/src/templates/skills/build-feature/evals/evals.json +53 -0
- package/src/templates/skills/council/SKILL.md +27 -6
- package/src/templates/skills/council/evals/evals.json +41 -0
- package/src/templates/skills/debug/SKILL.md +145 -0
- package/src/templates/skills/guild-specialize/SKILL.md +20 -0
- package/src/templates/skills/re-specialize/SKILL.md +153 -0
- package/src/templates/skills/tdd/SKILL.md +159 -0
- package/src/templates/skills/verify/SKILL.md +114 -0
- package/src/utils/eval-runner.js +139 -0
- package/src/utils/generators.js +11 -9
- package/src/utils/workspace.js +171 -0
- package/src/utils/zones.js +39 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { join, dirname, resolve } from 'path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
export const WORKSPACE_FILE = 'guild-workspace.json';
|
|
6
|
+
|
|
7
|
+
export const PRESET_COMMANDS = {
|
|
8
|
+
test: { cmd: 'npm', args: ['test'] },
|
|
9
|
+
lint: { cmd: 'npm', args: ['run', 'lint'] },
|
|
10
|
+
build: { cmd: 'npm', args: ['run', 'build'] },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function findWorkspaceRoot(startDir = process.cwd()) {
|
|
14
|
+
let dir = resolve(startDir);
|
|
15
|
+
while (true) {
|
|
16
|
+
if (existsSync(join(dir, WORKSPACE_FILE))) {
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
const parent = dirname(dir);
|
|
20
|
+
if (parent === dir) return null;
|
|
21
|
+
dir = parent;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function loadWorkspace(startDir = process.cwd()) {
|
|
26
|
+
const root = findWorkspaceRoot(startDir);
|
|
27
|
+
if (!root) return null;
|
|
28
|
+
|
|
29
|
+
const filePath = join(root, WORKSPACE_FILE);
|
|
30
|
+
const raw = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
...raw,
|
|
34
|
+
root,
|
|
35
|
+
members: (raw.members || []).map(m => ({
|
|
36
|
+
...m,
|
|
37
|
+
absolutePath: resolve(root, m.path),
|
|
38
|
+
})),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveWorkspaceAgents(workspace, localAgentsDir) {
|
|
43
|
+
const agents = new Map();
|
|
44
|
+
|
|
45
|
+
if (workspace?.shared?.agents) {
|
|
46
|
+
const sharedDir = resolve(workspace.root, workspace.shared.agents);
|
|
47
|
+
if (existsSync(sharedDir)) {
|
|
48
|
+
for (const file of readdirSync(sharedDir).filter(f => f.endsWith('.md'))) {
|
|
49
|
+
const name = file.replace('.md', '');
|
|
50
|
+
agents.set(name, { name, path: join(sharedDir, file), source: 'workspace' });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (existsSync(localAgentsDir)) {
|
|
56
|
+
for (const file of readdirSync(localAgentsDir).filter(f => f.endsWith('.md'))) {
|
|
57
|
+
const name = file.replace('.md', '');
|
|
58
|
+
agents.set(name, { name, path: join(localAgentsDir, file), source: 'local' });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return [...agents.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function generateWorkspaceContext(workspace, currentMemberName) {
|
|
66
|
+
if (!workspace) return '';
|
|
67
|
+
|
|
68
|
+
const otherMembers = workspace.members.filter(m => m.name !== currentMemberName);
|
|
69
|
+
if (otherMembers.length === 0) return '';
|
|
70
|
+
|
|
71
|
+
const lines = [
|
|
72
|
+
'## Workspace context',
|
|
73
|
+
`- **Workspace:** ${workspace.name}`,
|
|
74
|
+
`- **Members:** ${workspace.members.map(m => m.name === currentMemberName ? `${m.name} (this)` : m.name).join(', ')}`,
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
for (const member of otherMembers) {
|
|
78
|
+
const projectMdPath = join(member.absolutePath, 'PROJECT.md');
|
|
79
|
+
if (existsSync(projectMdPath)) {
|
|
80
|
+
const content = readFileSync(projectMdPath, 'utf8');
|
|
81
|
+
const stackMatch = content.match(/\*\*Stack:\*\*\s*(.+)/);
|
|
82
|
+
if (stackMatch) {
|
|
83
|
+
lines.push(`- **${member.name} stack:** ${stackMatch[1].trim()}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function collectMemberContext(workspace, currentMemberName) {
|
|
92
|
+
if (!workspace) return '';
|
|
93
|
+
|
|
94
|
+
const siblings = workspace.members.filter(m => m.name !== currentMemberName);
|
|
95
|
+
if (siblings.length === 0) return '';
|
|
96
|
+
|
|
97
|
+
const lines = [`## Workspace: ${workspace.name}`, ''];
|
|
98
|
+
|
|
99
|
+
for (const member of siblings) {
|
|
100
|
+
lines.push(`### ${member.name} (sibling — ${member.absolutePath})`);
|
|
101
|
+
|
|
102
|
+
const projectMdPath = join(member.absolutePath, 'PROJECT.md');
|
|
103
|
+
if (existsSync(projectMdPath)) {
|
|
104
|
+
const content = readFileSync(projectMdPath, 'utf8');
|
|
105
|
+
const stackMatch = content.match(/\*\*Stack:\*\*\s*(.+)/);
|
|
106
|
+
if (stackMatch) {
|
|
107
|
+
lines.push(`- **Stack:** ${stackMatch[1].trim()}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const claudeMdPath = join(member.absolutePath, 'CLAUDE.md');
|
|
112
|
+
if (existsSync(claudeMdPath)) {
|
|
113
|
+
const content = readFileSync(claudeMdPath, 'utf8');
|
|
114
|
+
const structureMatch = content.match(/## Project structure\n(.+)/);
|
|
115
|
+
if (structureMatch) {
|
|
116
|
+
lines.push(`- **Structure:** ${structureMatch[1].trim()}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const sessionMdPath = join(member.absolutePath, 'SESSION.md');
|
|
121
|
+
if (existsSync(sessionMdPath)) {
|
|
122
|
+
const content = readFileSync(sessionMdPath, 'utf8');
|
|
123
|
+
const taskMatch = content.match(/\*\*Current task:\*\*\s*(.+)/);
|
|
124
|
+
if (taskMatch) {
|
|
125
|
+
lines.push(`- **Current task:** ${taskMatch[1].trim()}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
lines.push(`You can read any file under ${member.absolutePath}/ for deeper analysis.`);
|
|
130
|
+
lines.push('');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return lines.join('\n').trim();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function runInMember(member, cmd, args) {
|
|
137
|
+
if (!existsSync(member.absolutePath)) {
|
|
138
|
+
return {
|
|
139
|
+
member: member.name,
|
|
140
|
+
status: 'failed',
|
|
141
|
+
output: `Directory not found: ${member.absolutePath}`,
|
|
142
|
+
duration: 0,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const start = Date.now();
|
|
147
|
+
try {
|
|
148
|
+
const stdout = execFileSync(cmd, args, {
|
|
149
|
+
cwd: member.absolutePath,
|
|
150
|
+
encoding: 'utf8',
|
|
151
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
152
|
+
});
|
|
153
|
+
const duration = Date.now() - start;
|
|
154
|
+
return {
|
|
155
|
+
member: member.name,
|
|
156
|
+
status: 'passed',
|
|
157
|
+
output: stdout.trim(),
|
|
158
|
+
duration,
|
|
159
|
+
};
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const duration = Date.now() - start;
|
|
162
|
+
const stdout = error.stdout || '';
|
|
163
|
+
const stderr = error.stderr || '';
|
|
164
|
+
return {
|
|
165
|
+
member: member.name,
|
|
166
|
+
status: 'failed',
|
|
167
|
+
output: (stdout + stderr).trim(),
|
|
168
|
+
duration,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const MARKER_START = 'guild:auto-start';
|
|
2
|
+
const MARKER_END = 'guild:auto-end';
|
|
3
|
+
|
|
4
|
+
export function wrapZone(zoneId, content) {
|
|
5
|
+
const trimmed = content.endsWith('\n') ? content.slice(0, -1) : content;
|
|
6
|
+
return `<!-- ${MARKER_START}:${zoneId} -->\n${trimmed}\n<!-- ${MARKER_END}:${zoneId} -->`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function extractZones(content) {
|
|
10
|
+
const zones = new Map();
|
|
11
|
+
const startPattern = /^<!-- guild:auto-start:(\S+) -->$/gm;
|
|
12
|
+
let match;
|
|
13
|
+
|
|
14
|
+
while ((match = startPattern.exec(content)) !== null) {
|
|
15
|
+
const zoneId = match[1];
|
|
16
|
+
const contentStart = match.index + match[0].length + 1; // +1 for newline
|
|
17
|
+
const endMarker = `<!-- ${MARKER_END}:${zoneId} -->`;
|
|
18
|
+
const endIndex = content.indexOf(endMarker, contentStart);
|
|
19
|
+
if (endIndex === -1) continue;
|
|
20
|
+
|
|
21
|
+
zones.set(zoneId, {
|
|
22
|
+
start: match.index,
|
|
23
|
+
end: endIndex + endMarker.length,
|
|
24
|
+
content: content.slice(contentStart, endIndex - 1), // -1 to trim newline before end marker
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return zones;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function replaceZone(content, zoneId, newContent) {
|
|
32
|
+
const zones = extractZones(content);
|
|
33
|
+
if (!zones.has(zoneId)) return content;
|
|
34
|
+
|
|
35
|
+
const zone = zones.get(zoneId);
|
|
36
|
+
const before = content.slice(0, zone.start);
|
|
37
|
+
const after = content.slice(zone.end);
|
|
38
|
+
return before + wrapZone(zoneId, newContent) + after;
|
|
39
|
+
}
|