skyloom 1.14.8 → 1.15.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 (156) hide show
  1. package/.github/workflows/ci.yml +2 -2
  2. package/.github/workflows/publish.yml +51 -4
  3. package/CONVERSION_PLAN.md +191 -191
  4. package/config/default.yaml +46 -43
  5. package/config/models.yaml +928 -155
  6. package/config/providers.yaml +109 -6
  7. package/dist/agents/snow.d.ts +2 -0
  8. package/dist/agents/snow.d.ts.map +1 -1
  9. package/dist/agents/snow.js +36 -5
  10. package/dist/agents/snow.js.map +1 -1
  11. package/dist/cli/loom_chat.d.ts.map +1 -1
  12. package/dist/cli/loom_chat.js +207 -1
  13. package/dist/cli/loom_chat.js.map +1 -1
  14. package/dist/cli/main.js +190 -40
  15. package/dist/cli/main.js.map +1 -1
  16. package/dist/cli/tui.d.ts.map +1 -1
  17. package/dist/cli/tui.js +6 -31
  18. package/dist/cli/tui.js.map +1 -1
  19. package/dist/core/agent.d.ts +6 -4
  20. package/dist/core/agent.d.ts.map +1 -1
  21. package/dist/core/agent.js +61 -20
  22. package/dist/core/agent.js.map +1 -1
  23. package/dist/core/catalog.d.ts.map +1 -1
  24. package/dist/core/catalog.js +30 -9
  25. package/dist/core/catalog.js.map +1 -1
  26. package/dist/core/commands.d.ts +110 -0
  27. package/dist/core/commands.d.ts.map +1 -0
  28. package/dist/core/commands.js +633 -0
  29. package/dist/core/commands.js.map +1 -0
  30. package/dist/core/concurrency.d.ts +38 -0
  31. package/dist/core/concurrency.d.ts.map +1 -0
  32. package/dist/core/concurrency.js +65 -0
  33. package/dist/core/concurrency.js.map +1 -0
  34. package/dist/core/factory.js +16 -16
  35. package/dist/core/file_checkpoint.d.ts +9 -0
  36. package/dist/core/file_checkpoint.d.ts.map +1 -1
  37. package/dist/core/file_checkpoint.js +33 -1
  38. package/dist/core/file_checkpoint.js.map +1 -1
  39. package/dist/core/llm.d.ts.map +1 -1
  40. package/dist/core/llm.js +66 -13
  41. package/dist/core/llm.js.map +1 -1
  42. package/dist/core/memory.js +51 -51
  43. package/dist/core/schemas.d.ts +16 -0
  44. package/dist/core/schemas.d.ts.map +1 -1
  45. package/dist/core/schemas.js +32 -0
  46. package/dist/core/schemas.js.map +1 -1
  47. package/dist/core/security.d.ts.map +1 -1
  48. package/dist/core/security.js +27 -0
  49. package/dist/core/security.js.map +1 -1
  50. package/dist/core/skymd.js +14 -14
  51. package/dist/core/trace.d.ts +105 -0
  52. package/dist/core/trace.d.ts.map +1 -0
  53. package/dist/core/trace.js +213 -0
  54. package/dist/core/trace.js.map +1 -0
  55. package/dist/tools/builtin.d.ts +2 -6
  56. package/dist/tools/builtin.d.ts.map +1 -1
  57. package/dist/tools/builtin.js +18 -111
  58. package/dist/tools/builtin.js.map +1 -1
  59. package/dist/tools/extra.d.ts +13 -0
  60. package/dist/tools/extra.d.ts.map +1 -0
  61. package/dist/tools/extra.js +827 -0
  62. package/dist/tools/extra.js.map +1 -0
  63. package/dist/tools/guards.d.ts +12 -0
  64. package/dist/tools/guards.d.ts.map +1 -0
  65. package/dist/tools/guards.js +143 -0
  66. package/dist/tools/guards.js.map +1 -0
  67. package/dist/tools/model_tool.d.ts.map +1 -1
  68. package/dist/tools/model_tool.js +24 -4
  69. package/dist/tools/model_tool.js.map +1 -1
  70. package/dist/web/markdown.d.ts +32 -0
  71. package/dist/web/markdown.d.ts.map +1 -0
  72. package/dist/web/markdown.js +202 -0
  73. package/dist/web/markdown.js.map +1 -0
  74. package/dist/web/server.d.ts +4 -0
  75. package/dist/web/server.d.ts.map +1 -1
  76. package/dist/web/server.js +14 -582
  77. package/dist/web/server.js.map +1 -1
  78. package/dist/web/ui.d.ts +31 -0
  79. package/dist/web/ui.d.ts.map +1 -0
  80. package/dist/web/ui.js +1009 -0
  81. package/dist/web/ui.js.map +1 -0
  82. package/docs/AESTHETIC_DESIGN.md +152 -152
  83. package/docs/OPTIMIZATION_PLAN.md +178 -178
  84. package/package.json +1 -1
  85. package/src/agents/snow.ts +38 -5
  86. package/src/cli/commands_md.ts +112 -112
  87. package/src/cli/input_macros.ts +83 -83
  88. package/src/cli/loom.ts +1041 -1041
  89. package/src/cli/loom_chat.ts +772 -603
  90. package/src/cli/main.ts +853 -723
  91. package/src/cli/tui.ts +264 -289
  92. package/src/core/agent/guard.ts +133 -133
  93. package/src/core/agent/task.ts +100 -100
  94. package/src/core/agent.ts +1630 -1590
  95. package/src/core/agent_helpers.ts +500 -500
  96. package/src/core/bus.ts +221 -221
  97. package/src/core/cache.ts +153 -153
  98. package/src/core/catalog.ts +199 -178
  99. package/src/core/circuit_breaker.ts +119 -119
  100. package/src/core/commands.ts +704 -0
  101. package/src/core/concurrency.ts +73 -0
  102. package/src/core/config.ts +365 -365
  103. package/src/core/constants.ts +95 -95
  104. package/src/core/factory.ts +656 -656
  105. package/src/core/file_checkpoint.ts +163 -136
  106. package/src/core/hooks.ts +126 -126
  107. package/src/core/llm.ts +972 -915
  108. package/src/core/logger.ts +143 -143
  109. package/src/core/mcp.ts +1001 -1001
  110. package/src/core/memory.ts +1201 -1201
  111. package/src/core/middleware.ts +350 -350
  112. package/src/core/model_config.ts +159 -159
  113. package/src/core/pipelines.ts +424 -424
  114. package/src/core/schemas.ts +319 -282
  115. package/src/core/security.ts +27 -0
  116. package/src/core/semantic.ts +211 -211
  117. package/src/core/skill.ts +384 -384
  118. package/src/core/skymd.ts +143 -143
  119. package/src/core/theme.ts +65 -65
  120. package/src/core/tool.ts +457 -457
  121. package/src/core/trace.ts +236 -0
  122. package/src/core/verify.ts +71 -71
  123. package/src/plugins/loader.ts +91 -91
  124. package/src/skills/loader.ts +75 -75
  125. package/src/tools/builtin.ts +571 -642
  126. package/src/tools/computer.ts +279 -279
  127. package/src/tools/extra.ts +662 -0
  128. package/src/tools/guards.ts +82 -0
  129. package/src/tools/model_tool.ts +93 -74
  130. package/src/tools/todo.ts +76 -76
  131. package/src/web/markdown.ts +193 -0
  132. package/src/web/server.ts +117 -693
  133. package/src/web/ui.ts +949 -0
  134. package/tests/agent.test.ts +211 -159
  135. package/tests/agent_helpers.test.ts +48 -48
  136. package/tests/catalog.test.ts +86 -86
  137. package/tests/checkpoint_commands.test.ts +124 -124
  138. package/tests/claude_compat.test.ts +110 -110
  139. package/tests/commands.test.ts +103 -0
  140. package/tests/concurrency.test.ts +102 -0
  141. package/tests/config.test.ts +41 -41
  142. package/tests/extra_tools.test.ts +212 -0
  143. package/tests/fence_plugin.test.ts +52 -52
  144. package/tests/guard.test.ts +75 -75
  145. package/tests/loom.test.ts +337 -337
  146. package/tests/memory.test.ts +170 -170
  147. package/tests/model_config.test.ts +109 -109
  148. package/tests/skymd.test.ts +146 -146
  149. package/tests/ssrf.test.ts +38 -38
  150. package/tests/structured_retry.test.ts +87 -0
  151. package/tests/task.test.ts +60 -60
  152. package/tests/todo_toolstats.test.ts +94 -94
  153. package/tests/trace.test.ts +128 -0
  154. package/tests/tui.test.ts +67 -67
  155. package/tests/web.test.ts +169 -0
  156. package/tsconfig.json +38 -38
@@ -1,424 +1,424 @@
1
- /**
2
- * Pipeline templates — predefined multi-agent DAGs.
3
- *
4
- * Why: Snow's task-decomposition LLM call costs 2-3k tokens per orchestration.
5
- * For common, recognizable workflows (code review, research-then-write, etc.),
6
- * we already know the right shape — paying an LLM to re-derive it every time
7
- * is pure waste. A pipeline match short-circuits Snow's planner entirely.
8
- *
9
- * The match function is rules-only (keyword + regex), runs in microseconds, and
10
- * falls back gracefully: ``matchPipeline`` returns null when nothing fits, and
11
- * ``factory.orchestrateTask`` then takes the original LLM-planning path.
12
- */
13
-
14
- /**
15
- * One step in a pipeline. Mirrors Task so we can map 1:1.
16
- */
17
- export interface PipelineStep {
18
- readonly id: string;
19
- readonly agent: string;
20
- readonly descriptionTemplate: string; // `{goal}` placeholder gets substituted at build time
21
- readonly dependsOn: readonly string[];
22
- }
23
-
24
- /**
25
- * A predefined collaboration template.
26
- */
27
- export interface Pipeline {
28
- readonly name: string;
29
- readonly triggers: readonly string[]; // case-insensitive substring tokens
30
- readonly steps: readonly PipelineStep[];
31
- readonly requireRegex: readonly string[]; // optional regex requirement
32
- }
33
-
34
- /**
35
- * Define a pipeline helper function.
36
- */
37
- function createPipeline(
38
- name: string,
39
- triggers: string[],
40
- steps: PipelineStep[],
41
- requireRegex?: string[]
42
- ): Pipeline {
43
- return {
44
- name,
45
- triggers: Object.freeze(triggers),
46
- steps: Object.freeze(steps),
47
- requireRegex: Object.freeze(requireRegex || []),
48
- };
49
- }
50
-
51
- /**
52
- * Helper to create pipeline steps.
53
- */
54
- function createStep(
55
- id: string,
56
- agent: string,
57
- descriptionTemplate: string,
58
- dependsOn?: string[]
59
- ): PipelineStep {
60
- return Object.freeze({
61
- id,
62
- agent,
63
- descriptionTemplate,
64
- dependsOn: Object.freeze(dependsOn || []),
65
- });
66
- }
67
-
68
- /**
69
- * Predefined pipeline templates.
70
- */
71
- const PIPELINES: readonly Pipeline[] = Object.freeze([
72
- createPipeline('code_review', ['审查代码', '代码审查', 'code review', 'review my code', '审计安全', '安全审计'], [
73
- createStep('1', 'frost', '审查代码: {goal}'),
74
- ]),
75
-
76
- createPipeline(
77
- 'research_then_write',
78
- ['调研后写', 'research and write', '先调研再写', 'research then write', '调研后生成', '调研并撰写'],
79
- [
80
- createStep('1', 'fog', '调研: {goal}'),
81
- createStep('2', 'rain', '基于第 1 步的调研结果撰写: {goal}', ['1']),
82
- ]
83
- ),
84
-
85
- createPipeline(
86
- 'research_review_write',
87
- ['调研审查后写', 'research review write', '先调研审查再写', '调研并审查后生成'],
88
- [
89
- createStep('1', 'fog', '调研: {goal}'),
90
- createStep('2', 'frost', '审查第 1 步的调研结果,确认可行性', ['1']),
91
- createStep('3', 'rain', '基于第 1 步调研和第 2 步审查意见撰写: {goal}', ['2']),
92
- ]
93
- ),
94
-
95
- createPipeline(
96
- 'implement_and_review',
97
- ['实现并审查', '写完后审查', 'implement and review', '写代码并 review', '实现并审计'],
98
- [
99
- createStep('1', 'rain', '实现: {goal}'),
100
- createStep('2', 'frost', '审查第 1 步的实现', ['1']),
101
- ]
102
- ),
103
-
104
- createPipeline(
105
- 'fix_and_verify',
106
- ['修复并验证', 'fix and verify', '修复bug并验证', '修复后审查', 'bugfix review'],
107
- [
108
- createStep('1', 'rain', '修复: {goal}'),
109
- createStep('2', 'frost', '验证第 1 步的修复是否正确', ['1']),
110
- ]
111
- ),
112
-
113
- createPipeline(
114
- 'implement_test_deploy',
115
- ['实现测试部署', '实现并部署', '写完测试再部署', 'implement test deploy'],
116
- [
117
- createStep('1', 'rain', '实现: {goal}'),
118
- createStep('2', 'frost', '审查第 1 步的实现', ['1']),
119
- createStep('3', 'dew', '部署第 1 步的实现', ['2']),
120
- ]
121
- ),
122
-
123
- createPipeline(
124
- 'design_implement_review_deploy',
125
- ['设计实现审查部署', '设计开发测试上线', 'design implement review deploy', '完整开发流程'],
126
- [
127
- createStep('1', 'fog', '设计方案: {goal}'),
128
- createStep('2', 'rain', '根据第 1 步的设计实现: {goal}', ['1']),
129
- createStep('3', 'frost', '审查第 2 步的实现代码', ['2']),
130
- createStep('4', 'dew', '部署第 2 步的实现到生产环境', ['3']),
131
- ]
132
- ),
133
-
134
- createPipeline(
135
- 'investigate_report',
136
- ['安全审计', 'security audit', '漏洞扫描', 'vulnerability scan', '安全检测'],
137
- [
138
- createStep('1', 'fog', '安全调研: {goal}'),
139
- createStep('2', 'frost', '基于第 1 步的调研生成安全报告', ['1']),
140
- ]
141
- ),
142
-
143
- createPipeline(
144
- 'debug_and_deploy',
145
- ['调试部署', 'debug and deploy', '修复并上线', 'hotfix deploy'],
146
- [
147
- createStep('1', 'rain', '调试修复: {goal}'),
148
- createStep('2', 'dew', '部署第 1 步的修复', ['1']),
149
- ]
150
- ),
151
- ]);
152
-
153
- /**
154
- * Compiled regex cache for require_regex.
155
- */
156
- const REGEX_CACHE: Map<number, RegExp[]> = new Map();
157
-
158
- /**
159
- * Compile and cache regex patterns.
160
- */
161
- function getCompiledRegex(p: Pipeline): RegExp[] {
162
- const key = p.requireRegex ? p.requireRegex.join('|').hashCode() : 0;
163
-
164
- if (REGEX_CACHE.has(key)) {
165
- return REGEX_CACHE.get(key)!;
166
- }
167
-
168
- const compiled = p.requireRegex.map(rx => new RegExp(rx, 'i'));
169
- REGEX_CACHE.set(key, compiled);
170
- return compiled;
171
- }
172
-
173
- /**
174
- * Hash function for strings (simple implementation).
175
- */
176
- declare global {
177
- interface String {
178
- hashCode(): number;
179
- }
180
- }
181
-
182
- String.prototype.hashCode = function (): number {
183
- let hash = 0;
184
- if (this.length === 0) return hash;
185
- for (let i = 0; i < this.length; i++) {
186
- const char = this.charCodeAt(i);
187
- hash = (hash << 5) - hash + char;
188
- hash = hash & hash; // Convert to 32bit integer
189
- }
190
- return Math.abs(hash);
191
- };
192
-
193
- /**
194
- * Match a goal string to a pipeline.
195
- *
196
- * Matching is case-insensitive substring + optional regex. Fast (~10us).
197
- * Returns the first matching pipeline, or null if no match.
198
- */
199
- export function matchPipeline(goal: string): Pipeline | null {
200
- if (!goal) {
201
- return null;
202
- }
203
-
204
- const lower = goal.toLowerCase();
205
-
206
- for (const p of PIPELINES) {
207
- // Check triggers
208
- if (!p.triggers.some(tok => lower.includes(tok.toLowerCase()))) {
209
- continue;
210
- }
211
-
212
- // Check regex if required
213
- if (p.requireRegex.length > 0) {
214
- const regexes = getCompiledRegex(p);
215
- if (!regexes.some(rx => rx.test(lower))) {
216
- continue;
217
- }
218
- }
219
-
220
- return p;
221
- }
222
-
223
- return null;
224
- }
225
-
226
- /**
227
- * Task interface (from agent.ts, used for pipeline materialization).
228
- */
229
- export interface Task {
230
- id: string;
231
- description: string;
232
- assignedTo: string;
233
- parentId: string | null;
234
- dependsOn: string[];
235
- metadata?: Record<string, any>;
236
- createdAt?: Date;
237
- status?: string;
238
- result?: string;
239
- }
240
-
241
- /**
242
- * Materialize a pipeline into runtime Task objects (full DAG).
243
- */
244
- export function buildTasksFromPipeline(pipeline: Pipeline, goal: string): Task[] {
245
- const tasks: Task[] = [];
246
-
247
- for (const step of pipeline.steps) {
248
- const description = step.descriptionTemplate.replace('{goal}', goal);
249
- const depends = Array.from(step.dependsOn);
250
- const parentId = depends.length > 0 ? depends[0] : null;
251
-
252
- tasks.push({
253
- id: step.id,
254
- description,
255
- assignedTo: step.agent,
256
- parentId,
257
- dependsOn: depends,
258
- metadata: {
259
- goal,
260
- pipeline: pipeline.name,
261
- },
262
- createdAt: new Date(),
263
- status: 'pending',
264
- });
265
- }
266
-
267
- return tasks;
268
- }
269
-
270
- /**
271
- * List all available pipelines for CLI/debug introspection.
272
- */
273
- export function listPipelines(): Record<string, any>[] {
274
- return PIPELINES.map(p => ({
275
- name: p.name,
276
- triggers: Array.from(p.triggers),
277
- steps: p.steps.map(s => ({
278
- id: s.id,
279
- agent: s.agent,
280
- dependsOn: Array.from(s.dependsOn),
281
- })),
282
- }));
283
- }
284
-
285
- /**
286
- * Get a pipeline by name.
287
- */
288
- export function getPipelineByName(name: string): Pipeline | null {
289
- return PIPELINES.find(p => p.name === name) || null;
290
- }
291
-
292
- /**
293
- * Get matching pipelines for a goal (all matches, not just first).
294
- */
295
- export function matchAllPipelines(goal: string): Pipeline[] {
296
- if (!goal) {
297
- return [];
298
- }
299
-
300
- const lower = goal.toLowerCase();
301
- const matches: Pipeline[] = [];
302
-
303
- for (const p of PIPELINES) {
304
- if (!p.triggers.some(tok => lower.includes(tok.toLowerCase()))) {
305
- continue;
306
- }
307
-
308
- if (p.requireRegex.length > 0) {
309
- const regexes = getCompiledRegex(p);
310
- if (!regexes.some(rx => rx.test(lower))) {
311
- continue;
312
- }
313
- }
314
-
315
- matches.push(p);
316
- }
317
-
318
- return matches;
319
- }
320
-
321
- /**
322
- * Validate a DAG for cycles and missing dependencies.
323
- */
324
- export function validateDAG(tasks: Task[]): { valid: boolean; errors: string[] } {
325
- const errors: string[] = [];
326
- const taskIds = new Set(tasks.map(t => t.id));
327
-
328
- // Check for missing dependencies
329
- for (const task of tasks) {
330
- for (const depId of task.dependsOn) {
331
- if (!taskIds.has(depId)) {
332
- errors.push(`Task ${task.id} depends on non-existent task ${depId}`);
333
- }
334
- }
335
- }
336
-
337
- // Simple cycle detection (DFS)
338
- const visited = new Set<string>();
339
- const recursionStack = new Set<string>();
340
-
341
- const hasCycle = (taskId: string): boolean => {
342
- visited.add(taskId);
343
- recursionStack.add(taskId);
344
-
345
- const task = tasks.find(t => t.id === taskId);
346
- if (task) {
347
- for (const depId of task.dependsOn) {
348
- if (!visited.has(depId)) {
349
- if (hasCycle(depId)) {
350
- return true;
351
- }
352
- } else if (recursionStack.has(depId)) {
353
- return true;
354
- }
355
- }
356
- }
357
-
358
- recursionStack.delete(taskId);
359
- return false;
360
- };
361
-
362
- for (const task of tasks) {
363
- if (!visited.has(task.id)) {
364
- if (hasCycle(task.id)) {
365
- errors.push(`Cycle detected involving task ${task.id}`);
366
- }
367
- }
368
- }
369
-
370
- return {
371
- valid: errors.length === 0,
372
- errors,
373
- };
374
- }
375
-
376
- /**
377
- * Topological sort of tasks based on dependencies.
378
- */
379
- export function topologicalSort(tasks: Task[]): Task[] {
380
- const inDegree = new Map<string, number>();
381
- const adjList = new Map<string, string[]>();
382
-
383
- // Initialize
384
- for (const task of tasks) {
385
- inDegree.set(task.id, task.dependsOn.length);
386
- adjList.set(task.id, []);
387
- }
388
-
389
- // Build adjacency list
390
- for (const task of tasks) {
391
- for (const depId of task.dependsOn) {
392
- if (adjList.has(depId)) {
393
- adjList.get(depId)!.push(task.id);
394
- }
395
- }
396
- }
397
-
398
- // Kahn's algorithm
399
- const queue: string[] = [];
400
- for (const [taskId, degree] of inDegree.entries()) {
401
- if (degree === 0) {
402
- queue.push(taskId);
403
- }
404
- }
405
-
406
- const sorted: Task[] = [];
407
- while (queue.length > 0) {
408
- const taskId = queue.shift()!;
409
- const task = tasks.find(t => t.id === taskId);
410
- if (task) {
411
- sorted.push(task);
412
- }
413
-
414
- for (const neighbor of adjList.get(taskId) || []) {
415
- const newDegree = (inDegree.get(neighbor) || 0) - 1;
416
- inDegree.set(neighbor, newDegree);
417
- if (newDegree === 0) {
418
- queue.push(neighbor);
419
- }
420
- }
421
- }
422
-
423
- return sorted;
424
- }
1
+ /**
2
+ * Pipeline templates — predefined multi-agent DAGs.
3
+ *
4
+ * Why: Snow's task-decomposition LLM call costs 2-3k tokens per orchestration.
5
+ * For common, recognizable workflows (code review, research-then-write, etc.),
6
+ * we already know the right shape — paying an LLM to re-derive it every time
7
+ * is pure waste. A pipeline match short-circuits Snow's planner entirely.
8
+ *
9
+ * The match function is rules-only (keyword + regex), runs in microseconds, and
10
+ * falls back gracefully: ``matchPipeline`` returns null when nothing fits, and
11
+ * ``factory.orchestrateTask`` then takes the original LLM-planning path.
12
+ */
13
+
14
+ /**
15
+ * One step in a pipeline. Mirrors Task so we can map 1:1.
16
+ */
17
+ export interface PipelineStep {
18
+ readonly id: string;
19
+ readonly agent: string;
20
+ readonly descriptionTemplate: string; // `{goal}` placeholder gets substituted at build time
21
+ readonly dependsOn: readonly string[];
22
+ }
23
+
24
+ /**
25
+ * A predefined collaboration template.
26
+ */
27
+ export interface Pipeline {
28
+ readonly name: string;
29
+ readonly triggers: readonly string[]; // case-insensitive substring tokens
30
+ readonly steps: readonly PipelineStep[];
31
+ readonly requireRegex: readonly string[]; // optional regex requirement
32
+ }
33
+
34
+ /**
35
+ * Define a pipeline helper function.
36
+ */
37
+ function createPipeline(
38
+ name: string,
39
+ triggers: string[],
40
+ steps: PipelineStep[],
41
+ requireRegex?: string[]
42
+ ): Pipeline {
43
+ return {
44
+ name,
45
+ triggers: Object.freeze(triggers),
46
+ steps: Object.freeze(steps),
47
+ requireRegex: Object.freeze(requireRegex || []),
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Helper to create pipeline steps.
53
+ */
54
+ function createStep(
55
+ id: string,
56
+ agent: string,
57
+ descriptionTemplate: string,
58
+ dependsOn?: string[]
59
+ ): PipelineStep {
60
+ return Object.freeze({
61
+ id,
62
+ agent,
63
+ descriptionTemplate,
64
+ dependsOn: Object.freeze(dependsOn || []),
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Predefined pipeline templates.
70
+ */
71
+ const PIPELINES: readonly Pipeline[] = Object.freeze([
72
+ createPipeline('code_review', ['审查代码', '代码审查', 'code review', 'review my code', '审计安全', '安全审计'], [
73
+ createStep('1', 'frost', '审查代码: {goal}'),
74
+ ]),
75
+
76
+ createPipeline(
77
+ 'research_then_write',
78
+ ['调研后写', 'research and write', '先调研再写', 'research then write', '调研后生成', '调研并撰写'],
79
+ [
80
+ createStep('1', 'fog', '调研: {goal}'),
81
+ createStep('2', 'rain', '基于第 1 步的调研结果撰写: {goal}', ['1']),
82
+ ]
83
+ ),
84
+
85
+ createPipeline(
86
+ 'research_review_write',
87
+ ['调研审查后写', 'research review write', '先调研审查再写', '调研并审查后生成'],
88
+ [
89
+ createStep('1', 'fog', '调研: {goal}'),
90
+ createStep('2', 'frost', '审查第 1 步的调研结果,确认可行性', ['1']),
91
+ createStep('3', 'rain', '基于第 1 步调研和第 2 步审查意见撰写: {goal}', ['2']),
92
+ ]
93
+ ),
94
+
95
+ createPipeline(
96
+ 'implement_and_review',
97
+ ['实现并审查', '写完后审查', 'implement and review', '写代码并 review', '实现并审计'],
98
+ [
99
+ createStep('1', 'rain', '实现: {goal}'),
100
+ createStep('2', 'frost', '审查第 1 步的实现', ['1']),
101
+ ]
102
+ ),
103
+
104
+ createPipeline(
105
+ 'fix_and_verify',
106
+ ['修复并验证', 'fix and verify', '修复bug并验证', '修复后审查', 'bugfix review'],
107
+ [
108
+ createStep('1', 'rain', '修复: {goal}'),
109
+ createStep('2', 'frost', '验证第 1 步的修复是否正确', ['1']),
110
+ ]
111
+ ),
112
+
113
+ createPipeline(
114
+ 'implement_test_deploy',
115
+ ['实现测试部署', '实现并部署', '写完测试再部署', 'implement test deploy'],
116
+ [
117
+ createStep('1', 'rain', '实现: {goal}'),
118
+ createStep('2', 'frost', '审查第 1 步的实现', ['1']),
119
+ createStep('3', 'dew', '部署第 1 步的实现', ['2']),
120
+ ]
121
+ ),
122
+
123
+ createPipeline(
124
+ 'design_implement_review_deploy',
125
+ ['设计实现审查部署', '设计开发测试上线', 'design implement review deploy', '完整开发流程'],
126
+ [
127
+ createStep('1', 'fog', '设计方案: {goal}'),
128
+ createStep('2', 'rain', '根据第 1 步的设计实现: {goal}', ['1']),
129
+ createStep('3', 'frost', '审查第 2 步的实现代码', ['2']),
130
+ createStep('4', 'dew', '部署第 2 步的实现到生产环境', ['3']),
131
+ ]
132
+ ),
133
+
134
+ createPipeline(
135
+ 'investigate_report',
136
+ ['安全审计', 'security audit', '漏洞扫描', 'vulnerability scan', '安全检测'],
137
+ [
138
+ createStep('1', 'fog', '安全调研: {goal}'),
139
+ createStep('2', 'frost', '基于第 1 步的调研生成安全报告', ['1']),
140
+ ]
141
+ ),
142
+
143
+ createPipeline(
144
+ 'debug_and_deploy',
145
+ ['调试部署', 'debug and deploy', '修复并上线', 'hotfix deploy'],
146
+ [
147
+ createStep('1', 'rain', '调试修复: {goal}'),
148
+ createStep('2', 'dew', '部署第 1 步的修复', ['1']),
149
+ ]
150
+ ),
151
+ ]);
152
+
153
+ /**
154
+ * Compiled regex cache for require_regex.
155
+ */
156
+ const REGEX_CACHE: Map<number, RegExp[]> = new Map();
157
+
158
+ /**
159
+ * Compile and cache regex patterns.
160
+ */
161
+ function getCompiledRegex(p: Pipeline): RegExp[] {
162
+ const key = p.requireRegex ? p.requireRegex.join('|').hashCode() : 0;
163
+
164
+ if (REGEX_CACHE.has(key)) {
165
+ return REGEX_CACHE.get(key)!;
166
+ }
167
+
168
+ const compiled = p.requireRegex.map(rx => new RegExp(rx, 'i'));
169
+ REGEX_CACHE.set(key, compiled);
170
+ return compiled;
171
+ }
172
+
173
+ /**
174
+ * Hash function for strings (simple implementation).
175
+ */
176
+ declare global {
177
+ interface String {
178
+ hashCode(): number;
179
+ }
180
+ }
181
+
182
+ String.prototype.hashCode = function (): number {
183
+ let hash = 0;
184
+ if (this.length === 0) return hash;
185
+ for (let i = 0; i < this.length; i++) {
186
+ const char = this.charCodeAt(i);
187
+ hash = (hash << 5) - hash + char;
188
+ hash = hash & hash; // Convert to 32bit integer
189
+ }
190
+ return Math.abs(hash);
191
+ };
192
+
193
+ /**
194
+ * Match a goal string to a pipeline.
195
+ *
196
+ * Matching is case-insensitive substring + optional regex. Fast (~10us).
197
+ * Returns the first matching pipeline, or null if no match.
198
+ */
199
+ export function matchPipeline(goal: string): Pipeline | null {
200
+ if (!goal) {
201
+ return null;
202
+ }
203
+
204
+ const lower = goal.toLowerCase();
205
+
206
+ for (const p of PIPELINES) {
207
+ // Check triggers
208
+ if (!p.triggers.some(tok => lower.includes(tok.toLowerCase()))) {
209
+ continue;
210
+ }
211
+
212
+ // Check regex if required
213
+ if (p.requireRegex.length > 0) {
214
+ const regexes = getCompiledRegex(p);
215
+ if (!regexes.some(rx => rx.test(lower))) {
216
+ continue;
217
+ }
218
+ }
219
+
220
+ return p;
221
+ }
222
+
223
+ return null;
224
+ }
225
+
226
+ /**
227
+ * Task interface (from agent.ts, used for pipeline materialization).
228
+ */
229
+ export interface Task {
230
+ id: string;
231
+ description: string;
232
+ assignedTo: string;
233
+ parentId: string | null;
234
+ dependsOn: string[];
235
+ metadata?: Record<string, any>;
236
+ createdAt?: Date;
237
+ status?: string;
238
+ result?: string;
239
+ }
240
+
241
+ /**
242
+ * Materialize a pipeline into runtime Task objects (full DAG).
243
+ */
244
+ export function buildTasksFromPipeline(pipeline: Pipeline, goal: string): Task[] {
245
+ const tasks: Task[] = [];
246
+
247
+ for (const step of pipeline.steps) {
248
+ const description = step.descriptionTemplate.replace('{goal}', goal);
249
+ const depends = Array.from(step.dependsOn);
250
+ const parentId = depends.length > 0 ? depends[0] : null;
251
+
252
+ tasks.push({
253
+ id: step.id,
254
+ description,
255
+ assignedTo: step.agent,
256
+ parentId,
257
+ dependsOn: depends,
258
+ metadata: {
259
+ goal,
260
+ pipeline: pipeline.name,
261
+ },
262
+ createdAt: new Date(),
263
+ status: 'pending',
264
+ });
265
+ }
266
+
267
+ return tasks;
268
+ }
269
+
270
+ /**
271
+ * List all available pipelines for CLI/debug introspection.
272
+ */
273
+ export function listPipelines(): Record<string, any>[] {
274
+ return PIPELINES.map(p => ({
275
+ name: p.name,
276
+ triggers: Array.from(p.triggers),
277
+ steps: p.steps.map(s => ({
278
+ id: s.id,
279
+ agent: s.agent,
280
+ dependsOn: Array.from(s.dependsOn),
281
+ })),
282
+ }));
283
+ }
284
+
285
+ /**
286
+ * Get a pipeline by name.
287
+ */
288
+ export function getPipelineByName(name: string): Pipeline | null {
289
+ return PIPELINES.find(p => p.name === name) || null;
290
+ }
291
+
292
+ /**
293
+ * Get matching pipelines for a goal (all matches, not just first).
294
+ */
295
+ export function matchAllPipelines(goal: string): Pipeline[] {
296
+ if (!goal) {
297
+ return [];
298
+ }
299
+
300
+ const lower = goal.toLowerCase();
301
+ const matches: Pipeline[] = [];
302
+
303
+ for (const p of PIPELINES) {
304
+ if (!p.triggers.some(tok => lower.includes(tok.toLowerCase()))) {
305
+ continue;
306
+ }
307
+
308
+ if (p.requireRegex.length > 0) {
309
+ const regexes = getCompiledRegex(p);
310
+ if (!regexes.some(rx => rx.test(lower))) {
311
+ continue;
312
+ }
313
+ }
314
+
315
+ matches.push(p);
316
+ }
317
+
318
+ return matches;
319
+ }
320
+
321
+ /**
322
+ * Validate a DAG for cycles and missing dependencies.
323
+ */
324
+ export function validateDAG(tasks: Task[]): { valid: boolean; errors: string[] } {
325
+ const errors: string[] = [];
326
+ const taskIds = new Set(tasks.map(t => t.id));
327
+
328
+ // Check for missing dependencies
329
+ for (const task of tasks) {
330
+ for (const depId of task.dependsOn) {
331
+ if (!taskIds.has(depId)) {
332
+ errors.push(`Task ${task.id} depends on non-existent task ${depId}`);
333
+ }
334
+ }
335
+ }
336
+
337
+ // Simple cycle detection (DFS)
338
+ const visited = new Set<string>();
339
+ const recursionStack = new Set<string>();
340
+
341
+ const hasCycle = (taskId: string): boolean => {
342
+ visited.add(taskId);
343
+ recursionStack.add(taskId);
344
+
345
+ const task = tasks.find(t => t.id === taskId);
346
+ if (task) {
347
+ for (const depId of task.dependsOn) {
348
+ if (!visited.has(depId)) {
349
+ if (hasCycle(depId)) {
350
+ return true;
351
+ }
352
+ } else if (recursionStack.has(depId)) {
353
+ return true;
354
+ }
355
+ }
356
+ }
357
+
358
+ recursionStack.delete(taskId);
359
+ return false;
360
+ };
361
+
362
+ for (const task of tasks) {
363
+ if (!visited.has(task.id)) {
364
+ if (hasCycle(task.id)) {
365
+ errors.push(`Cycle detected involving task ${task.id}`);
366
+ }
367
+ }
368
+ }
369
+
370
+ return {
371
+ valid: errors.length === 0,
372
+ errors,
373
+ };
374
+ }
375
+
376
+ /**
377
+ * Topological sort of tasks based on dependencies.
378
+ */
379
+ export function topologicalSort(tasks: Task[]): Task[] {
380
+ const inDegree = new Map<string, number>();
381
+ const adjList = new Map<string, string[]>();
382
+
383
+ // Initialize
384
+ for (const task of tasks) {
385
+ inDegree.set(task.id, task.dependsOn.length);
386
+ adjList.set(task.id, []);
387
+ }
388
+
389
+ // Build adjacency list
390
+ for (const task of tasks) {
391
+ for (const depId of task.dependsOn) {
392
+ if (adjList.has(depId)) {
393
+ adjList.get(depId)!.push(task.id);
394
+ }
395
+ }
396
+ }
397
+
398
+ // Kahn's algorithm
399
+ const queue: string[] = [];
400
+ for (const [taskId, degree] of inDegree.entries()) {
401
+ if (degree === 0) {
402
+ queue.push(taskId);
403
+ }
404
+ }
405
+
406
+ const sorted: Task[] = [];
407
+ while (queue.length > 0) {
408
+ const taskId = queue.shift()!;
409
+ const task = tasks.find(t => t.id === taskId);
410
+ if (task) {
411
+ sorted.push(task);
412
+ }
413
+
414
+ for (const neighbor of adjList.get(taskId) || []) {
415
+ const newDegree = (inDegree.get(neighbor) || 0) - 1;
416
+ inDegree.set(neighbor, newDegree);
417
+ if (newDegree === 0) {
418
+ queue.push(neighbor);
419
+ }
420
+ }
421
+ }
422
+
423
+ return sorted;
424
+ }