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,400 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* version.js — Version detection and computation utilities for Guild CLI.
|
|
3
|
+
*
|
|
4
|
+
* Provides pure functions for parsing semver strings, detecting pre-release
|
|
5
|
+
* channels, computing next snapshot/beta/stable versions, and generating
|
|
6
|
+
* user-facing warning messages.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parses a semver string into its components.
|
|
11
|
+
* @param {string} version - The version string from package.json
|
|
12
|
+
* @returns {{ base: string, channel: string, prerelease: string|null }}
|
|
13
|
+
*/
|
|
14
|
+
export function parseVersion(version) {
|
|
15
|
+
const match = version.match(/^(\d+\.\d+\.\d+)(?:-(.+))?$/);
|
|
16
|
+
if (!match) {
|
|
17
|
+
return { base: version, channel: 'stable', prerelease: null };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const base = match[1];
|
|
21
|
+
const prerelease = match[2] || null;
|
|
22
|
+
|
|
23
|
+
let channel = 'stable';
|
|
24
|
+
if (prerelease?.startsWith('beta')) channel = 'beta';
|
|
25
|
+
else if (prerelease?.startsWith('snapshot')) channel = 'snapshot';
|
|
26
|
+
|
|
27
|
+
return { base, channel, prerelease };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns a warning string for pre-release versions, or null for stable.
|
|
32
|
+
* @param {string} version
|
|
33
|
+
* @returns {string|null}
|
|
34
|
+
*/
|
|
35
|
+
export function getPreReleaseWarning(version) {
|
|
36
|
+
const { channel } = parseVersion(version);
|
|
37
|
+
|
|
38
|
+
if (channel === 'snapshot') {
|
|
39
|
+
return 'Snapshot build: for development/testing only';
|
|
40
|
+
}
|
|
41
|
+
if (channel === 'beta') {
|
|
42
|
+
return 'Pre-release: may contain experimental changes';
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Computes the next snapshot version.
|
|
49
|
+
* @param {string} currentVersion - e.g. "0.3.1" or "0.4.0-snapshot.20260225.1"
|
|
50
|
+
* @param {Date} [now=new Date()] - injectable for testing
|
|
51
|
+
* @returns {string} - e.g. "0.3.1-snapshot.20260225.1"
|
|
52
|
+
*/
|
|
53
|
+
export function computeSnapshotVersion(currentVersion, now = new Date()) {
|
|
54
|
+
const baseVersion = currentVersion.replace(/-.*$/, '');
|
|
55
|
+
const dateStr = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
56
|
+
|
|
57
|
+
let buildNum = 1;
|
|
58
|
+
const currentPrerelease = currentVersion.match(/-snapshot\.(\d+)\.(\d+)$/);
|
|
59
|
+
if (currentPrerelease && currentPrerelease[1] === dateStr) {
|
|
60
|
+
buildNum = parseInt(currentPrerelease[2], 10) + 1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return `${baseVersion}-snapshot.${dateStr}.${buildNum}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Computes the next beta version.
|
|
68
|
+
* @param {string} currentVersion - e.g. "0.4.0" or "0.4.0-beta.2"
|
|
69
|
+
* @returns {string} - e.g. "0.4.0-beta.1" or "0.4.0-beta.3"
|
|
70
|
+
*/
|
|
71
|
+
export function computeBetaVersion(currentVersion) {
|
|
72
|
+
const baseVersion = currentVersion.replace(/-.*$/, '');
|
|
73
|
+
|
|
74
|
+
let betaNum = 1;
|
|
75
|
+
const currentBeta = currentVersion.match(/-beta\.(\d+)$/);
|
|
76
|
+
if (currentBeta) {
|
|
77
|
+
betaNum = parseInt(currentBeta[1], 10) + 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return `${baseVersion}-beta.${betaNum}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Computes the stable version by stripping any prerelease suffix.
|
|
85
|
+
* @param {string} currentVersion - e.g. "0.4.0-beta.3"
|
|
86
|
+
* @returns {string} - e.g. "0.4.0"
|
|
87
|
+
*/
|
|
88
|
+
export function computeStableVersion(currentVersion) {
|
|
89
|
+
return currentVersion.replace(/-.*$/, '');
|
|
90
|
+
}
|