@tyvm/knowhow 0.0.130 → 0.0.132

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 (97) hide show
  1. package/package.json +1 -1
  2. package/src/agents/configurable/ConfigAgent.ts +0 -1
  3. package/src/agents/tools/startAgentTask.ts +17 -13
  4. package/src/ai.ts +55 -24
  5. package/src/chat/CliChatService.ts +7 -1
  6. package/src/chat/modules/AgentModule.ts +72 -14
  7. package/src/chat/renderer/PlainRenderer.ts +218 -0
  8. package/src/chat/renderer/index.ts +1 -0
  9. package/src/chat/renderer/loadRenderer.ts +2 -0
  10. package/src/chat.ts +2 -2
  11. package/src/cli.ts +23 -0
  12. package/src/clients/anthropic.ts +9 -1
  13. package/src/clients/fireworks.ts +10 -1
  14. package/src/clients/index.ts +6 -3
  15. package/src/clients/meta.ts +9 -0
  16. package/src/clients/pricing/anthropic.ts +11 -0
  17. package/src/clients/pricing/fireworks.ts +12 -0
  18. package/src/commands/agent.ts +53 -20
  19. package/src/commands/agents.ts +556 -0
  20. package/src/commands/hash.ts +170 -0
  21. package/src/commands/misc.ts +157 -3
  22. package/src/commands/skills.ts +36 -0
  23. package/src/config.ts +54 -2
  24. package/src/generate.ts +507 -0
  25. package/src/index.ts +32 -216
  26. package/src/services/BehaviorsService.ts +32 -0
  27. package/src/services/SkillsService.ts +198 -0
  28. package/src/types.ts +36 -4
  29. package/ts_build/package.json +1 -1
  30. package/ts_build/src/agents/configurable/ConfigAgent.js.map +1 -1
  31. package/ts_build/src/agents/tools/startAgentTask.d.ts +2 -2
  32. package/ts_build/src/agents/tools/startAgentTask.js +13 -12
  33. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  34. package/ts_build/src/ai.d.ts +10 -4
  35. package/ts_build/src/ai.js +27 -25
  36. package/ts_build/src/ai.js.map +1 -1
  37. package/ts_build/src/chat/CliChatService.d.ts +1 -1
  38. package/ts_build/src/chat/CliChatService.js +4 -1
  39. package/ts_build/src/chat/CliChatService.js.map +1 -1
  40. package/ts_build/src/chat/modules/AgentModule.js +61 -4
  41. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  42. package/ts_build/src/chat/renderer/PlainRenderer.d.ts +28 -0
  43. package/ts_build/src/chat/renderer/PlainRenderer.js +169 -0
  44. package/ts_build/src/chat/renderer/PlainRenderer.js.map +1 -0
  45. package/ts_build/src/chat/renderer/index.d.ts +1 -0
  46. package/ts_build/src/chat/renderer/index.js +3 -1
  47. package/ts_build/src/chat/renderer/index.js.map +1 -1
  48. package/ts_build/src/chat/renderer/loadRenderer.js +2 -0
  49. package/ts_build/src/chat/renderer/loadRenderer.js.map +1 -1
  50. package/ts_build/src/chat.d.ts +1 -1
  51. package/ts_build/src/chat.js +2 -2
  52. package/ts_build/src/chat.js.map +1 -1
  53. package/ts_build/src/cli.js +16 -0
  54. package/ts_build/src/cli.js.map +1 -1
  55. package/ts_build/src/clients/anthropic.js +4 -1
  56. package/ts_build/src/clients/anthropic.js.map +1 -1
  57. package/ts_build/src/clients/fireworks.js +7 -0
  58. package/ts_build/src/clients/fireworks.js.map +1 -1
  59. package/ts_build/src/clients/index.js +5 -1
  60. package/ts_build/src/clients/index.js.map +1 -1
  61. package/ts_build/src/clients/meta.d.ts +2 -0
  62. package/ts_build/src/clients/meta.js +5 -0
  63. package/ts_build/src/clients/meta.js.map +1 -1
  64. package/ts_build/src/clients/pricing/anthropic.d.ts +3 -0
  65. package/ts_build/src/clients/pricing/anthropic.js +6 -0
  66. package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
  67. package/ts_build/src/clients/pricing/fireworks.d.ts +1 -0
  68. package/ts_build/src/clients/pricing/fireworks.js +5 -1
  69. package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
  70. package/ts_build/src/commands/agent.js +44 -17
  71. package/ts_build/src/commands/agent.js.map +1 -1
  72. package/ts_build/src/commands/agents.d.ts +2 -0
  73. package/ts_build/src/commands/agents.js +460 -0
  74. package/ts_build/src/commands/agents.js.map +1 -0
  75. package/ts_build/src/commands/hash.d.ts +2 -0
  76. package/ts_build/src/commands/hash.js +150 -0
  77. package/ts_build/src/commands/hash.js.map +1 -0
  78. package/ts_build/src/commands/misc.js +108 -2
  79. package/ts_build/src/commands/misc.js.map +1 -1
  80. package/ts_build/src/commands/skills.js +24 -0
  81. package/ts_build/src/commands/skills.js.map +1 -1
  82. package/ts_build/src/config.js +37 -1
  83. package/ts_build/src/config.js.map +1 -1
  84. package/ts_build/src/generate.d.ts +16 -0
  85. package/ts_build/src/generate.js +304 -0
  86. package/ts_build/src/generate.js.map +1 -0
  87. package/ts_build/src/index.d.ts +1 -3
  88. package/ts_build/src/index.js +9 -118
  89. package/ts_build/src/index.js.map +1 -1
  90. package/ts_build/src/services/BehaviorsService.d.ts +2 -0
  91. package/ts_build/src/services/BehaviorsService.js +24 -0
  92. package/ts_build/src/services/BehaviorsService.js.map +1 -1
  93. package/ts_build/src/services/SkillsService.d.ts +7 -0
  94. package/ts_build/src/services/SkillsService.js +163 -0
  95. package/ts_build/src/services/SkillsService.js.map +1 -1
  96. package/ts_build/src/types.d.ts +14 -4
  97. package/ts_build/src/types.js.map +1 -1
@@ -0,0 +1,507 @@
1
+ import { summarizeFiles, summarizeFile } from "./ai";
2
+ import type { AgentOptions } from "./ai";
3
+ import {
4
+ saveAllFileHashes,
5
+ getHashes,
6
+ checkNoFilesChanged,
7
+ } from "./hashes";
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+ import * as crypto from "crypto";
11
+ import { globSync } from "glob";
12
+
13
+ import { GenerationSource } from "./types";
14
+ import { readFile, writeFile } from "./utils";
15
+ import { getConfig, loadPrompt } from "./config";
16
+ import { services } from "./services/";
17
+ import { convertToText } from "./conversion";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Input pattern normalization
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /**
24
+ * Normalizes an input pattern to a valid glob pattern.
25
+ * Supports:
26
+ * - Standard glob patterns (e.g. "src/**\/*.ts")
27
+ * - Brace expansion (e.g. "{src/a.ts,src/b.ts}")
28
+ * - Comma-separated file paths (e.g. "src/a.ts,src/b.ts") — auto-converted to brace expansion
29
+ * - Mixed comma-separated list with globs (e.g. "src/a.ts,src/commands/**\/*.ts")
30
+ */
31
+ export function normalizeInputPattern(input: string): string {
32
+ if (input.includes("{")) return input;
33
+ if (input.includes(",")) {
34
+ const parts = input.split(",").map((p) => p.trim());
35
+ return `{${parts.join(",")}}`;
36
+ }
37
+ return input;
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // GenerateOptions
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export interface GenerateOptions {
45
+ /** Only run sources whose `name` matches exactly this value */
46
+ name?: string;
47
+ /** Only run sources whose name/input/output contains this substring */
48
+ filter?: string;
49
+ /**
50
+ * When true, agent-driven sources create .knowhow/processes/agents/<taskId>/
51
+ * so they appear in `knowhow agents list`. Overrides the per-source syncFs flag.
52
+ */
53
+ syncFs?: boolean;
54
+ /** When true, skip hash checks and regenerate all matching sources regardless of changes */
55
+ force?: boolean;
56
+ /**
57
+ * Maximum number of generation sources to run in parallel within a single
58
+ * dependency wave. Defaults to 3. Set to 1 for fully sequential execution.
59
+ */
60
+ concurrency?: number;
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Filter helper
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /**
68
+ * Returns true when a source should run given the provided filter options.
69
+ */
70
+ function sourceMatchesFilter(
71
+ source: GenerationSource,
72
+ options: GenerateOptions
73
+ ): boolean {
74
+ if (options.name) return source.name === options.name;
75
+ if (options.filter) {
76
+ const needle = options.filter.toLowerCase();
77
+ return [source.name, source.input, source.output]
78
+ .filter(Boolean)
79
+ .some((v) => v!.toLowerCase().includes(needle));
80
+ }
81
+ return true;
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Dependency / concurrency helpers
86
+ // ---------------------------------------------------------------------------
87
+
88
+ /**
89
+ * Detects whether sourceB's input references sourceA's output directory or file.
90
+ *
91
+ * Heuristic: if sourceA produces an output path and sourceB reads from that
92
+ * same path (or a sub-path of it) then B depends on A.
93
+ */
94
+ function ioOverlaps(sourceA: GenerationSource, sourceB: GenerationSource): boolean {
95
+ const aOut = sourceA.output;
96
+ if (!aOut) return false;
97
+
98
+ const bInputPattern = normalizeInputPattern(sourceB.input || "");
99
+
100
+ // Strip brace expansion to individual paths for comparison
101
+ const bPaths = bInputPattern.startsWith("{")
102
+ ? bInputPattern.slice(1, -1).split(",").map((p) => p.trim())
103
+ : [bInputPattern];
104
+
105
+ for (const bPath of bPaths) {
106
+ // If aOut is a directory (ends with /) check if bPath starts with it
107
+ if (aOut.endsWith("/") && bPath.startsWith(aOut)) return true;
108
+ // If aOut is a file, check if bPath starts with aOut's directory
109
+ const aDir = aOut.endsWith("/") ? aOut : path.dirname(aOut) + "/";
110
+ if (bPath.startsWith(aDir)) return true;
111
+ // Direct prefix/glob match: e.g. aOut = "autodoc/" bPath = "autodoc/**/*.md"
112
+ if (bPath.startsWith(aOut.replace(/\/$/, ""))) return true;
113
+ }
114
+ return false;
115
+ }
116
+
117
+ /**
118
+ * Builds a topological dependency graph and returns sources grouped into
119
+ * sequential "waves". All sources within a wave have no un-satisfied
120
+ * dependencies and can run concurrently. Each subsequent wave waits for
121
+ * the previous wave to complete.
122
+ *
123
+ * Dependency resolution:
124
+ * 1. Explicit `dependsOn: string[]` — names listed there must finish first.
125
+ * 2. Automatic I/O overlap detection — if A writes to a path that B reads,
126
+ * B is automatically placed after A.
127
+ */
128
+ export function buildWaves(sources: GenerationSource[]): GenerationSource[][] {
129
+ const nameIndex = new Map<string, GenerationSource>();
130
+ for (const s of sources) {
131
+ if (s.name) nameIndex.set(s.name, s);
132
+ }
133
+
134
+ // Build adjacency: deps[i] = set of indices that source[i] depends on
135
+ const n = sources.length;
136
+ const deps: Set<number>[] = Array.from({ length: n }, () => new Set());
137
+
138
+ for (let i = 0; i < n; i++) {
139
+ const b = sources[i];
140
+
141
+ // Explicit dependsOn
142
+ for (const depName of b.dependsOn ?? []) {
143
+ const depIdx = sources.findIndex((s) => s.name === depName);
144
+ if (depIdx !== -1 && depIdx !== i) deps[i].add(depIdx);
145
+ }
146
+
147
+ // Auto I/O overlap
148
+ for (let j = 0; j < n; j++) {
149
+ if (i === j) continue;
150
+ if (ioOverlaps(sources[j], b)) deps[i].add(j);
151
+ }
152
+ }
153
+
154
+ // Kahn's algorithm to produce waves
155
+ const waves: GenerationSource[][] = [];
156
+ const completed = new Set<number>();
157
+
158
+ while (completed.size < n) {
159
+ const wave: number[] = [];
160
+ for (let i = 0; i < n; i++) {
161
+ if (completed.has(i)) continue;
162
+ const remaining = [...deps[i]].filter((d) => !completed.has(d));
163
+ if (remaining.length === 0) wave.push(i);
164
+ }
165
+
166
+ if (wave.length === 0) {
167
+ // Cycle detected — fall back to running everything remaining sequentially
168
+ console.warn(
169
+ "⚠️ Dependency cycle detected in generation sources — running remaining sources sequentially."
170
+ );
171
+ for (let i = 0; i < n; i++) {
172
+ if (!completed.has(i)) wave.push(i);
173
+ }
174
+ }
175
+
176
+ waves.push(wave.map((i) => sources[i]));
177
+ for (const i of wave) completed.add(i);
178
+ }
179
+
180
+ return waves;
181
+ }
182
+
183
+ /**
184
+ * Runs up to `limit` async tasks concurrently, preserving order of
185
+ * completion relative to input order.
186
+ */
187
+ async function runWithConcurrency<T>(
188
+ items: T[],
189
+ limit: number,
190
+ fn: (item: T, index: number) => Promise<void>
191
+ ): Promise<void> {
192
+ // Use a simple semaphore-style approach: keep up to `limit` promises running
193
+ // at the same time. As each finishes, the next item is started immediately.
194
+ const entries = [...items.entries()]; // [[0, item0], [1, item1], ...]
195
+ let cursor = 0;
196
+
197
+ async function worker(): Promise<void> {
198
+ while (cursor < entries.length) {
199
+ const [index, item] = entries[cursor++];
200
+ await fn(item, index);
201
+ }
202
+ }
203
+
204
+ // Spawn exactly `limit` workers (or fewer if there aren't enough items).
205
+ const workers: Promise<void>[] = [];
206
+ for (let i = 0; i < Math.min(limit, entries.length); i++) {
207
+ workers.push(worker());
208
+ }
209
+ await Promise.all(workers);
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Output helpers
214
+ // ---------------------------------------------------------------------------
215
+
216
+ /**
217
+ * Appends an instruction telling an assigned agent exactly which file to write
218
+ * its generated output to.
219
+ */
220
+ export function withOutputTarget(prompt: string, outputFile: string): string {
221
+ return `${prompt}\n\n---\nGENERATION OUTPUT TARGET: Write your finished result to the file at \`${outputFile}\` using your file-writing tools (e.g. writeFileChunk). Create any parent directories as needed. Once the file is written with the complete, final content, call finalAnswer with a brief summary. Do NOT write to any other path.`;
222
+ }
223
+
224
+ /**
225
+ * For agent-driven generation: if the agent already wrote the output file
226
+ * itself, keep it. Otherwise, fall back to writing the returned content.
227
+ */
228
+ export async function writeAgentOrSummaryOutput(
229
+ outputFile: string,
230
+ summary: string,
231
+ agent?: string
232
+ ): Promise<void> {
233
+ const outputDir = path.dirname(outputFile);
234
+ if (outputDir && !fs.existsSync(outputDir)) {
235
+ fs.mkdirSync(outputDir, { recursive: true });
236
+ }
237
+
238
+ if (agent && fs.existsSync(outputFile)) {
239
+ console.log(
240
+ `Agent wrote output file directly: ${outputFile} (skipping summary write)`
241
+ );
242
+ return;
243
+ }
244
+
245
+ console.log("Writing summary to", outputFile);
246
+ await writeFile(outputFile, summary ?? "");
247
+ }
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Multi-output generation
251
+ // ---------------------------------------------------------------------------
252
+
253
+ export async function handleMultiOutputGeneration(
254
+ model: string,
255
+ inputPattern: string,
256
+ files: string[],
257
+ prompt: string,
258
+ output: string,
259
+ outputExt = "mdx",
260
+ outputName?: string,
261
+ kind?: string,
262
+ agent?: string,
263
+ agentOpts?: AgentOptions,
264
+ force?: boolean
265
+ ) {
266
+ const promptHash = crypto.createHash("md5").update(prompt).digest("hex");
267
+ const hashes = await getHashes();
268
+
269
+ const inputPath = inputPattern.includes("**")
270
+ ? inputPattern.split("**")[0]
271
+ : "";
272
+
273
+ for (const file of files) {
274
+ const fileContent = await convertToText(file);
275
+ const fileHash = crypto.createHash("md5").update(fileContent).digest("hex");
276
+
277
+ if (!hashes[file]) {
278
+ hashes[file] = { promptHash: "", fileHash: "" };
279
+ }
280
+
281
+ const { name, ext, dir } = path.parse(file);
282
+ const nestedFolder = inputPath ? (dir + "/").replace(inputPath, "") : "";
283
+ const outputFolder = path.join(output, nestedFolder);
284
+
285
+ if (!fs.existsSync(outputFolder)) {
286
+ fs.mkdirSync(outputFolder, { recursive: true });
287
+ }
288
+
289
+ const outputFileName = outputName || name;
290
+ const outputFile = path.join(outputFolder, outputFileName + "." + outputExt);
291
+ console.log({ dir, inputPath, nestedFolder, outputFile });
292
+
293
+ const toCheck = [file, outputFile];
294
+ const noChanges = await checkNoFilesChanged(toCheck, promptHash, hashes);
295
+ if (noChanges && !force) {
296
+ console.log("Skipping file", file, "because it hasn't changed");
297
+ continue;
298
+ }
299
+
300
+ console.log("Summarizing", file);
301
+ const effectivePrompt =
302
+ prompt && agent ? withOutputTarget(prompt, outputFile) : prompt;
303
+ const summary = prompt
304
+ ? await summarizeFile(file, effectivePrompt, model, agent, agentOpts)
305
+ : fileContent;
306
+
307
+ await writeAgentOrSummaryOutput(outputFile, summary, agent);
308
+ await saveAllFileHashes(toCheck, promptHash);
309
+ }
310
+ }
311
+
312
+ // ---------------------------------------------------------------------------
313
+ // Single-output generation
314
+ // ---------------------------------------------------------------------------
315
+
316
+ export async function handleSingleOutputGeneration(
317
+ model: string,
318
+ files: string[],
319
+ prompt: string,
320
+ outputFile: string,
321
+ kind?: string,
322
+ agent?: string,
323
+ agentOpts?: AgentOptions,
324
+ force?: boolean
325
+ ) {
326
+ const hashes = await getHashes();
327
+ const promptHash = crypto.createHash("md5").update(prompt).digest("hex");
328
+
329
+ const filesToCheck = [outputFile, ...files];
330
+ const noChanges = await checkNoFilesChanged(filesToCheck, promptHash, hashes);
331
+ if (noChanges && !force) {
332
+ console.log(`Skipping ${files.length} files because they haven't changed`);
333
+ return;
334
+ }
335
+
336
+ console.log("Summarizing", files.length, "files");
337
+ const outputDir = path.dirname(outputFile);
338
+ if (outputDir && !fs.existsSync(outputDir)) {
339
+ fs.mkdirSync(outputDir, { recursive: true });
340
+ }
341
+
342
+ const effectivePrompt =
343
+ prompt && agent ? withOutputTarget(prompt, outputFile) : prompt;
344
+
345
+ const summary = prompt
346
+ ? await summarizeFiles(files, effectivePrompt, model, agent, agentOpts)
347
+ : (await Promise.all(files.map(convertToText))).join("\n\n");
348
+
349
+ await writeAgentOrSummaryOutput(outputFile, summary, agent);
350
+ await saveAllFileHashes(filesToCheck, promptHash);
351
+ }
352
+
353
+ // ---------------------------------------------------------------------------
354
+ // All-kinds generation (plugin + file fallback)
355
+ // ---------------------------------------------------------------------------
356
+
357
+ async function handleAllKindsGeneration(source: GenerationSource) {
358
+ const { Plugins } = services();
359
+ const { kind, input } = source;
360
+ if (Plugins.isPlugin(kind)) {
361
+ const data = await Plugins.call(kind, input);
362
+ if (source.output.endsWith("/")) {
363
+ throw new Error(`Plugin ${kind} can only output to a single file`);
364
+ }
365
+ const pluginOutputDir = path.dirname(source.output);
366
+ if (pluginOutputDir && !fs.existsSync(pluginOutputDir)) {
367
+ fs.mkdirSync(pluginOutputDir, { recursive: true });
368
+ }
369
+ await writeFile(source.output, data);
370
+ }
371
+ return handleFileKindGeneration(source);
372
+ }
373
+
374
+ async function handleFileKindGeneration(
375
+ source: GenerationSource,
376
+ agentOpts?: AgentOptions
377
+ ) {
378
+ const prompt = await loadPrompt(source.prompt);
379
+ const files = globSync(normalizeInputPattern(source.input));
380
+ console.log("Analyzing files: ", files);
381
+
382
+ if (source.output.endsWith("/")) {
383
+ await handleMultiOutputGeneration(
384
+ source.model,
385
+ source.input,
386
+ files,
387
+ prompt,
388
+ source.output,
389
+ source.outputExt,
390
+ source.outputName,
391
+ source.kind,
392
+ source.agent,
393
+ agentOpts
394
+ );
395
+ } else {
396
+ await handleSingleOutputGeneration(
397
+ source.model,
398
+ files,
399
+ prompt,
400
+ source.output,
401
+ source.kind,
402
+ source.agent,
403
+ agentOpts
404
+ );
405
+ }
406
+ }
407
+
408
+ // ---------------------------------------------------------------------------
409
+ // Run a single source
410
+ // ---------------------------------------------------------------------------
411
+
412
+ async function runSource(
413
+ source: GenerationSource,
414
+ options: GenerateOptions
415
+ ): Promise<void> {
416
+ console.log("Generating", source.input, "to", source.output);
417
+
418
+ const effectiveSyncFs = options.syncFs ?? source.syncFs;
419
+ const agentOpts: AgentOptions | undefined = source.agent
420
+ ? {
421
+ syncFs: effectiveSyncFs,
422
+ taskId:
423
+ source.taskId ??
424
+ (effectiveSyncFs
425
+ ? `generate:${source.output.replace(/[^a-zA-Z0-9_.-]/g, "_")}`
426
+ : undefined),
427
+ maxTimeLimit: source.maxTimeLimit,
428
+ maxSpendLimit: source.maxSpendLimit,
429
+ }
430
+ : undefined;
431
+
432
+ if (source.kind === "file" || !source.kind) {
433
+ const files = globSync(normalizeInputPattern(source.input));
434
+ const prompt = await loadPrompt(source.prompt);
435
+
436
+ if (source.output.endsWith("/")) {
437
+ await handleMultiOutputGeneration(
438
+ source.model,
439
+ source.input,
440
+ files,
441
+ prompt,
442
+ source.output,
443
+ source.outputExt,
444
+ source.outputName,
445
+ source.kind,
446
+ source.agent,
447
+ agentOpts,
448
+ options.force
449
+ );
450
+ } else {
451
+ await handleSingleOutputGeneration(
452
+ source.model,
453
+ files,
454
+ prompt,
455
+ source.output,
456
+ source.kind,
457
+ source.agent,
458
+ agentOpts,
459
+ options.force
460
+ );
461
+ }
462
+ } else {
463
+ await handleAllKindsGeneration(source);
464
+ }
465
+ }
466
+
467
+ // ---------------------------------------------------------------------------
468
+ // Main generate entry point
469
+ // ---------------------------------------------------------------------------
470
+
471
+ export async function generate(options: GenerateOptions = {}): Promise<void> {
472
+ const config = await getConfig();
473
+ const allSources = config.sources ?? [];
474
+ const sources = allSources.filter((s) => sourceMatchesFilter(s, options));
475
+
476
+ if ((options.name || options.filter) && sources.length === 0) {
477
+ console.warn(
478
+ `No generation sources matched ${
479
+ options.name ? `name="${options.name}"` : `filter="${options.filter}"`
480
+ }. Available names: ${allSources
481
+ .map((s) => s.name)
482
+ .filter(Boolean)
483
+ .join(", ") || "(none defined)"}`
484
+ );
485
+ return;
486
+ }
487
+
488
+ const concurrency = options.concurrency ?? 3;
489
+ const waves = buildWaves(sources);
490
+
491
+ for (let wi = 0; wi < waves.length; wi++) {
492
+ const wave = waves[wi];
493
+ if (waves.length > 1) {
494
+ console.log(
495
+ `\n[Wave ${wi + 1}/${waves.length}] Running ${wave.length} source${wave.length !== 1 ? "s" : ""} (concurrency: ${concurrency})...`
496
+ );
497
+ }
498
+
499
+ await runWithConcurrency(wave, concurrency, (source) =>
500
+ runSource(source, options)
501
+ );
502
+ }
503
+
504
+ if (waves.length > 1) {
505
+ console.log(`\n✅ Generation complete (${waves.length} wave${waves.length !== 1 ? "s" : ""}, ${sources.length} sources).`);
506
+ }
507
+ }