skyloom 1.14.6 → 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 (157) hide show
  1. package/.github/workflows/ci.yml +2 -2
  2. package/.github/workflows/publish.yml +74 -0
  3. package/CONVERSION_PLAN.md +191 -191
  4. package/README.md +523 -220
  5. package/config/default.yaml +46 -43
  6. package/config/models.yaml +928 -155
  7. package/config/providers.yaml +109 -6
  8. package/dist/agents/snow.d.ts +2 -0
  9. package/dist/agents/snow.d.ts.map +1 -1
  10. package/dist/agents/snow.js +36 -5
  11. package/dist/agents/snow.js.map +1 -1
  12. package/dist/cli/loom_chat.d.ts.map +1 -1
  13. package/dist/cli/loom_chat.js +207 -1
  14. package/dist/cli/loom_chat.js.map +1 -1
  15. package/dist/cli/main.js +190 -40
  16. package/dist/cli/main.js.map +1 -1
  17. package/dist/cli/tui.d.ts.map +1 -1
  18. package/dist/cli/tui.js +6 -31
  19. package/dist/cli/tui.js.map +1 -1
  20. package/dist/core/agent.d.ts +6 -4
  21. package/dist/core/agent.d.ts.map +1 -1
  22. package/dist/core/agent.js +61 -20
  23. package/dist/core/agent.js.map +1 -1
  24. package/dist/core/catalog.d.ts.map +1 -1
  25. package/dist/core/catalog.js +30 -9
  26. package/dist/core/catalog.js.map +1 -1
  27. package/dist/core/commands.d.ts +110 -0
  28. package/dist/core/commands.d.ts.map +1 -0
  29. package/dist/core/commands.js +633 -0
  30. package/dist/core/commands.js.map +1 -0
  31. package/dist/core/concurrency.d.ts +38 -0
  32. package/dist/core/concurrency.d.ts.map +1 -0
  33. package/dist/core/concurrency.js +65 -0
  34. package/dist/core/concurrency.js.map +1 -0
  35. package/dist/core/factory.js +16 -16
  36. package/dist/core/file_checkpoint.d.ts +9 -0
  37. package/dist/core/file_checkpoint.d.ts.map +1 -1
  38. package/dist/core/file_checkpoint.js +33 -1
  39. package/dist/core/file_checkpoint.js.map +1 -1
  40. package/dist/core/llm.d.ts.map +1 -1
  41. package/dist/core/llm.js +66 -13
  42. package/dist/core/llm.js.map +1 -1
  43. package/dist/core/memory.js +51 -51
  44. package/dist/core/schemas.d.ts +16 -0
  45. package/dist/core/schemas.d.ts.map +1 -1
  46. package/dist/core/schemas.js +32 -0
  47. package/dist/core/schemas.js.map +1 -1
  48. package/dist/core/security.d.ts.map +1 -1
  49. package/dist/core/security.js +27 -0
  50. package/dist/core/security.js.map +1 -1
  51. package/dist/core/skymd.js +14 -14
  52. package/dist/core/trace.d.ts +105 -0
  53. package/dist/core/trace.d.ts.map +1 -0
  54. package/dist/core/trace.js +213 -0
  55. package/dist/core/trace.js.map +1 -0
  56. package/dist/tools/builtin.d.ts +2 -6
  57. package/dist/tools/builtin.d.ts.map +1 -1
  58. package/dist/tools/builtin.js +180 -125
  59. package/dist/tools/builtin.js.map +1 -1
  60. package/dist/tools/extra.d.ts +13 -0
  61. package/dist/tools/extra.d.ts.map +1 -0
  62. package/dist/tools/extra.js +827 -0
  63. package/dist/tools/extra.js.map +1 -0
  64. package/dist/tools/guards.d.ts +12 -0
  65. package/dist/tools/guards.d.ts.map +1 -0
  66. package/dist/tools/guards.js +143 -0
  67. package/dist/tools/guards.js.map +1 -0
  68. package/dist/tools/model_tool.d.ts.map +1 -1
  69. package/dist/tools/model_tool.js +24 -4
  70. package/dist/tools/model_tool.js.map +1 -1
  71. package/dist/web/markdown.d.ts +32 -0
  72. package/dist/web/markdown.d.ts.map +1 -0
  73. package/dist/web/markdown.js +202 -0
  74. package/dist/web/markdown.js.map +1 -0
  75. package/dist/web/server.d.ts +4 -0
  76. package/dist/web/server.d.ts.map +1 -1
  77. package/dist/web/server.js +14 -582
  78. package/dist/web/server.js.map +1 -1
  79. package/dist/web/ui.d.ts +31 -0
  80. package/dist/web/ui.d.ts.map +1 -0
  81. package/dist/web/ui.js +1009 -0
  82. package/dist/web/ui.js.map +1 -0
  83. package/docs/AESTHETIC_DESIGN.md +152 -152
  84. package/docs/OPTIMIZATION_PLAN.md +178 -178
  85. package/package.json +68 -68
  86. package/src/agents/snow.ts +38 -5
  87. package/src/cli/commands_md.ts +112 -112
  88. package/src/cli/input_macros.ts +83 -83
  89. package/src/cli/loom.ts +1041 -1041
  90. package/src/cli/loom_chat.ts +772 -603
  91. package/src/cli/main.ts +853 -723
  92. package/src/cli/tui.ts +264 -289
  93. package/src/core/agent/guard.ts +133 -133
  94. package/src/core/agent/task.ts +100 -100
  95. package/src/core/agent.ts +1630 -1590
  96. package/src/core/agent_helpers.ts +500 -500
  97. package/src/core/bus.ts +221 -221
  98. package/src/core/cache.ts +153 -153
  99. package/src/core/catalog.ts +199 -178
  100. package/src/core/circuit_breaker.ts +119 -119
  101. package/src/core/commands.ts +704 -0
  102. package/src/core/concurrency.ts +73 -0
  103. package/src/core/config.ts +365 -365
  104. package/src/core/constants.ts +95 -95
  105. package/src/core/factory.ts +656 -656
  106. package/src/core/file_checkpoint.ts +163 -136
  107. package/src/core/hooks.ts +126 -126
  108. package/src/core/llm.ts +972 -915
  109. package/src/core/logger.ts +143 -143
  110. package/src/core/mcp.ts +1001 -1001
  111. package/src/core/memory.ts +1201 -1201
  112. package/src/core/middleware.ts +350 -350
  113. package/src/core/model_config.ts +159 -159
  114. package/src/core/pipelines.ts +424 -424
  115. package/src/core/schemas.ts +319 -282
  116. package/src/core/security.ts +27 -0
  117. package/src/core/semantic.ts +211 -211
  118. package/src/core/skill.ts +384 -384
  119. package/src/core/skymd.ts +143 -143
  120. package/src/core/theme.ts +65 -65
  121. package/src/core/tool.ts +457 -457
  122. package/src/core/trace.ts +236 -0
  123. package/src/core/verify.ts +71 -71
  124. package/src/plugins/loader.ts +91 -91
  125. package/src/skills/loader.ts +75 -75
  126. package/src/tools/builtin.ts +571 -493
  127. package/src/tools/computer.ts +279 -279
  128. package/src/tools/extra.ts +662 -0
  129. package/src/tools/guards.ts +82 -0
  130. package/src/tools/model_tool.ts +93 -74
  131. package/src/tools/todo.ts +76 -76
  132. package/src/web/markdown.ts +193 -0
  133. package/src/web/server.ts +117 -693
  134. package/src/web/ui.ts +949 -0
  135. package/tests/agent.test.ts +211 -159
  136. package/tests/agent_helpers.test.ts +48 -48
  137. package/tests/catalog.test.ts +86 -86
  138. package/tests/checkpoint_commands.test.ts +124 -124
  139. package/tests/claude_compat.test.ts +110 -110
  140. package/tests/commands.test.ts +103 -0
  141. package/tests/concurrency.test.ts +102 -0
  142. package/tests/config.test.ts +41 -41
  143. package/tests/extra_tools.test.ts +212 -0
  144. package/tests/fence_plugin.test.ts +52 -52
  145. package/tests/guard.test.ts +75 -75
  146. package/tests/loom.test.ts +337 -337
  147. package/tests/memory.test.ts +170 -170
  148. package/tests/model_config.test.ts +109 -109
  149. package/tests/skymd.test.ts +146 -146
  150. package/tests/ssrf.test.ts +38 -38
  151. package/tests/structured_retry.test.ts +87 -0
  152. package/tests/task.test.ts +60 -60
  153. package/tests/todo_toolstats.test.ts +94 -94
  154. package/tests/trace.test.ts +128 -0
  155. package/tests/tui.test.ts +67 -67
  156. package/tests/web.test.ts +169 -0
  157. 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
+ }