autosnippet 3.1.15 → 3.2.2

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 (41) hide show
  1. package/README.md +1 -0
  2. package/bin/cli.js +242 -23
  3. package/dashboard/dist/assets/{icons-CC5R_iwL.js → icons-18VxiaCT.js} +108 -98
  4. package/dashboard/dist/assets/index-BJiuaVPD.css +1 -0
  5. package/dashboard/dist/assets/{index-WmnJCXq4.js → index-CRH5Umim.js} +50 -50
  6. package/dashboard/dist/index.html +3 -3
  7. package/lib/cli/SetupService.js +152 -21
  8. package/lib/domain/task/Task.js +214 -0
  9. package/lib/domain/task/TaskDependency.js +48 -0
  10. package/lib/domain/task/TaskIdGenerator.js +83 -0
  11. package/lib/domain/task/index.js +6 -0
  12. package/lib/external/mcp/McpServer.js +4 -4
  13. package/lib/external/mcp/handlers/task.js +295 -0
  14. package/lib/external/mcp/tools.js +100 -3
  15. package/lib/http/HttpServer.js +8 -0
  16. package/lib/http/routes/guard.js +283 -0
  17. package/lib/http/routes/task.js +282 -0
  18. package/lib/infrastructure/config/Paths.js +18 -8
  19. package/lib/infrastructure/database/migrations/002_add_tasks.js +88 -0
  20. package/lib/injection/ServiceContainer.js +58 -0
  21. package/lib/repository/task/TaskRepository.impl.js +398 -0
  22. package/lib/service/cursor/AgentInstructionsGenerator.js +28 -9
  23. package/lib/service/cursor/CursorDeliveryPipeline.js +42 -20
  24. package/lib/service/cursor/KnowledgeCompressor.js +40 -0
  25. package/lib/service/cursor/TokenBudget.js +2 -2
  26. package/lib/service/guard/GuardFeedbackLoop.js +17 -2
  27. package/lib/service/knowledge/KnowledgeService.js +6 -0
  28. package/lib/service/task/TaskGraphService.js +410 -0
  29. package/lib/service/task/TaskKnowledgeBridge.js +86 -0
  30. package/lib/service/task/TaskReadyEngine.js +127 -0
  31. package/lib/shared/constants.js +3 -3
  32. package/package.json +1 -1
  33. package/skills/autosnippet-intent/SKILL.md +4 -1
  34. package/skills/autosnippet-recipes/SKILL.md +17 -2
  35. package/templates/claude-hooks.yaml +19 -0
  36. package/templates/copilot-instructions.md +33 -1
  37. package/templates/cursor-rules/autosnippet-conventions.mdc +12 -0
  38. package/templates/cursor-rules/autosnippet-workflow.mdc +43 -0
  39. package/templates/guard-ci.yml +1 -0
  40. package/templates/pre-commit-guard.sh +2 -1
  41. package/dashboard/dist/assets/index-6iola4rb.css +0 -1
@@ -71,9 +71,9 @@ export class CursorDeliveryPipeline {
71
71
  this.logger.info?.(`[CursorDelivery] Loaded ${entries.length} knowledge entries`);
72
72
 
73
73
  // 2. 分类:rules vs patterns vs facts vs documents
74
- const { rules, patterns, documents } = this._classify(entries);
74
+ const { rules, patterns, facts, documents } = this._classify(entries);
75
75
  this.logger.info?.(
76
- `[CursorDelivery] Classified: ${rules.length} rules, ${patterns.length} patterns, ${documents.length} documents`
76
+ `[CursorDelivery] Classified: ${rules.length} rules, ${patterns.length} patterns, ${facts.length} facts, ${documents.length} documents`
77
77
  );
78
78
 
79
79
  // 3. 清理旧的动态生成文件
@@ -83,8 +83,8 @@ export class CursorDeliveryPipeline {
83
83
  const channelA = this._generateChannelA(rules);
84
84
  stats.channelA = channelA;
85
85
 
86
- // ── Channel B: Smart Rules (by topic) ──
87
- const channelB = this._generateChannelB(patterns);
86
+ // ── Channel B: Smart Rules (by topic) + Facts ──
87
+ const channelB = this._generateChannelB(patterns, facts);
88
88
  stats.channelB = channelB;
89
89
 
90
90
  // ── Channel C: Skills Sync ──
@@ -255,49 +255,71 @@ export class CursorDeliveryPipeline {
255
255
  }
256
256
 
257
257
  /**
258
- * Channel B 生成
258
+ * Channel B 生成(patterns + facts)
259
+ * @param {Array} patterns - kind='pattern' 的知识条目
260
+ * @param {Array} [facts=[]] - kind='fact' 的知识条目
259
261
  * @private
260
262
  */
261
- _generateChannelB(patterns) {
262
- const result = { topicCount: 0, patternsCount: 0, totalTokens: 0, topics: {} };
263
+ _generateChannelB(patterns, facts = []) {
264
+ const result = { topicCount: 0, patternsCount: 0, factsCount: 0, totalTokens: 0, topics: {} };
263
265
 
264
- if (patterns.length === 0) {
265
- this.logger.info?.('[CursorDelivery] Channel B: No patterns to generate');
266
+ if (patterns.length === 0 && facts.length === 0) {
267
+ this.logger.info?.('[CursorDelivery] Channel B: No patterns or facts to generate');
266
268
  return result;
267
269
  }
268
270
 
269
- // 按主题分组
271
+ // 按主题分组 patterns
270
272
  const grouped = this.topicClassifier.group(patterns);
271
273
 
272
- for (const [topic, topicPatterns] of Object.entries(grouped)) {
273
- // 排序并取 Top N
274
- const top = this._rank(topicPatterns).slice(0, BUDGET.CHANNEL_B_MAX_PATTERNS);
274
+ // 按主题分组 facts(复用同一分类器)
275
+ const groupedFacts = facts.length > 0 ? this.topicClassifier.group(facts) : {};
276
+
277
+ // 合并所有主题(patterns + facts 的并集)
278
+ const allTopics = new Set([...Object.keys(grouped), ...Object.keys(groupedFacts)]);
275
279
 
276
- // 压缩为 When/Do/Don't
280
+ for (const topic of allTopics) {
281
+ const topicPatterns = grouped[topic] || [];
282
+ const topicFacts = groupedFacts[topic] || [];
283
+
284
+ // 压缩 patterns 为 When/Do/Don't
285
+ const top = this._rank(topicPatterns).slice(0, BUDGET.CHANNEL_B_MAX_PATTERNS);
277
286
  const compressed = this.compressor.compressToWhenDoDont(top);
278
- if (compressed.length === 0) {
287
+
288
+ // 压缩 facts 为 Know 行
289
+ const factLines = this.compressor.compressToFactLines(topicFacts);
290
+
291
+ if (compressed.length === 0 && factLines.length === 0) {
279
292
  continue;
280
293
  }
281
294
 
282
- // 格式化为 Markdown
283
- const body = this.compressor.formatWhenDoDont(compressed);
295
+ // 格式化为 Markdown(patterns + facts)
296
+ let body = '';
297
+ if (compressed.length > 0) {
298
+ body += this.compressor.formatWhenDoDont(compressed);
299
+ }
300
+ if (factLines.length > 0) {
301
+ body += this.compressor.formatFactLines(factLines);
302
+ }
284
303
 
285
- // 构建 description
286
- const description = this.topicClassifier.buildDescription(topic, topicPatterns);
304
+ // 构建 description(合并 patterns 和 facts 条目用于关键词提取)
305
+ const allEntries = [...topicPatterns, ...topicFacts];
306
+ const description = this.topicClassifier.buildDescription(topic, allEntries);
287
307
 
288
308
  // 写入 .mdc
289
309
  const writeResult = this.rulesGenerator.writeSmartRules(topic, body, description);
290
310
 
291
311
  result.topicCount++;
292
312
  result.patternsCount += compressed.length;
313
+ result.factsCount += factLines.length;
293
314
  result.totalTokens += writeResult.tokensUsed;
294
315
  result.topics[topic] = {
295
316
  patternsCount: compressed.length,
317
+ factsCount: factLines.length,
296
318
  tokensUsed: writeResult.tokensUsed,
297
319
  };
298
320
 
299
321
  this.logger.info?.(
300
- `[CursorDelivery] Channel B: ${topic} — ${compressed.length} patterns → ${writeResult.filePath}`
322
+ `[CursorDelivery] Channel B: ${topic} — ${compressed.length} patterns + ${factLines.length} facts → ${writeResult.filePath}`
301
323
  );
302
324
  }
303
325
 
@@ -144,6 +144,46 @@ export class KnowledgeCompressor {
144
144
  })
145
145
  .join('\n\n');
146
146
  }
147
+
148
+ /**
149
+ * Channel B — Fact 条目压缩为 "Know" 行
150
+ *
151
+ * fact 类型没有 trigger/whenClause/doClause 结构,
152
+ * 采用 "Know: {title} — {description}" 的简洁格式,
153
+ * 让 Agent 获取项目事实性知识(技术选型、架构决策等)。
154
+ *
155
+ * @param {Array<Object>} facts - KnowledgeEntry 数组 (kind='fact')
156
+ * @returns {Array<{ title: string, summary: string }>}
157
+ */
158
+ compressToFactLines(facts) {
159
+ return facts
160
+ .filter((e) => e.title)
161
+ .map((e) => {
162
+ const summary = e.description || e.content?.markdown || '';
163
+ const shortSummary = summary.length > 150
164
+ ? `${summary.slice(0, 147)}...`
165
+ : summary;
166
+ return { title: e.title, summary: shortSummary };
167
+ });
168
+ }
169
+
170
+ /**
171
+ * 将 Fact 压缩结果格式化为 Markdown 字符串
172
+ * @param {Array<{ title: string, summary: string }>} factLines
173
+ * @returns {string}
174
+ */
175
+ formatFactLines(factLines) {
176
+ if (factLines.length === 0) return '';
177
+ const lines = ['', '## Context Facts', ''];
178
+ for (const f of factLines) {
179
+ if (f.summary) {
180
+ lines.push(`- **${f.title}**: ${f.summary}`);
181
+ } else {
182
+ lines.push(`- **${f.title}**`);
183
+ }
184
+ }
185
+ return lines.join('\n');
186
+ }
147
187
  }
148
188
 
149
189
  export default KnowledgeCompressor;
@@ -10,10 +10,10 @@ export { estimateTokens };
10
10
 
11
11
  /** 默认预算配置 */
12
12
  export const BUDGET = {
13
- CHANNEL_A_MAX: 400, // Always-On Rules 最大 token
13
+ CHANNEL_A_MAX: 800, // Always-On Rules 最大 token (8→15条扩容)
14
14
  CHANNEL_B_MAX_PER_FILE: 750, // Smart Rules 每个主题文件最大 token
15
15
  CHANNEL_B_MAX_PATTERNS: 5, // Smart Rules 每个主题最多模式数
16
- CHANNEL_A_MAX_RULES: 8, // Always-On Rules 最多规则数
16
+ CHANNEL_A_MAX_RULES: 15, // Always-On Rules 最多规则数 (8→15扩容)
17
17
  };
18
18
 
19
19
  /**
@@ -91,7 +91,7 @@ export class GuardFeedbackLoop {
91
91
 
92
92
  /**
93
93
  * 一站式处理:检测修复 + 自动确认
94
- * 供 MCP handler GuardHandler 集成调用
94
+ * 供 MCP handler、GuardHandler、HTTP guard/file 端点集成调用
95
95
  * @param {{ violations: Array }} currentResult
96
96
  * @param {string} filePath
97
97
  */
@@ -106,8 +106,22 @@ export class GuardFeedbackLoop {
106
106
  return fixed;
107
107
  }
108
108
 
109
+ /**
110
+ * 获取闭环统计数据
111
+ * @returns {{ totalFixDetected: number, totalConfirmed: number }}
112
+ */
113
+ getStats() {
114
+ return {
115
+ hasViolationsStore: !!this.violationsStore,
116
+ hasFeedbackCollector: !!this.feedbackCollector,
117
+ hasGuardCheckEngine: !!this.guardCheckEngine,
118
+ };
119
+ }
120
+
109
121
  /**
110
122
  * 从 violation 或 GuardCheckEngine 查找 fixRecipeId
123
+ * 增强:当无显式 fixSuggestion 时,以 ruleId 本身作为 fallback recipeId
124
+ * 这允许 Knowledge Base 中以 ruleId 命名的条目自动关联
111
125
  */
112
126
  _findFixRecipe(ruleId, violations) {
113
127
  // 先从 violation 本身的 fixSuggestion 查找
@@ -130,7 +144,8 @@ export class GuardFeedbackLoop {
130
144
  }
131
145
  }
132
146
 
133
- return null;
147
+ // fallback: 用 ruleId 本身作为 recipeId — 允许知识库按规则 ID 索引
148
+ return ruleId || null;
134
149
  }
135
150
  }
136
151
 
@@ -687,6 +687,8 @@ export class KnowledgeService {
687
687
  * QualityScorer 需要: title, trigger, code, language, category, summary, usageGuide, headers, tags
688
688
  */
689
689
  _adaptForScorer(entry) {
690
+ // 从 Stats 值对象提取 engagement 指标,映射到 QualityScorer 期望的 views/clicks/rating
691
+ const stats = entry.stats && typeof entry.stats === 'object' ? entry.stats : {};
690
692
  return {
691
693
  title: entry.title,
692
694
  trigger: entry.trigger,
@@ -697,6 +699,10 @@ export class KnowledgeService {
697
699
  usageGuide: entry.content?.markdown || entry.doClause || '',
698
700
  headers: entry.headers || [],
699
701
  tags: entry.tags || [],
702
+ // engagement: views → views, adoptions+applications → clicks, authority → rating
703
+ views: (stats.views ?? 0) + (stats.searchHits ?? 0),
704
+ clicks: (stats.adoptions ?? 0) + (stats.applications ?? 0) + (stats.guardHits ?? 0),
705
+ rating: stats.authority ?? 0,
700
706
  };
701
707
  }
702
708
 
@@ -0,0 +1,410 @@
1
+ import { Task } from '../../domain/task/Task.js';
2
+ import { DepType, affectsReadyWork, isValidDepType } from '../../domain/task/TaskDependency.js';
3
+ import Logger from '../../infrastructure/logging/Logger.js';
4
+
5
+ /**
6
+ * TaskGraphService — 任务图核心服务
7
+ *
8
+ * 编排 TaskRepository / TaskReadyEngine / TaskKnowledgeBridge / AuditLog / IdGenerator。
9
+ * 对外提供完整的任务生命周期管理。
10
+ *
11
+ * 设计原则:
12
+ * - 所有写操作记录审计事件 (task_events 表)
13
+ * - claim 是原子操作 (status + assignee 一起更新)
14
+ * - close 时验证不变量 (closedAt 必须存在)
15
+ * - 子任务 ID 自动递增 (parent.1, parent.2, ...)
16
+ *
17
+ * AutoSnippet 增强:
18
+ * - ready 返回带知识上下文的任务
19
+ * - close 返回 newlyReady 列表(减少 Agent MCP 调用次数)
20
+ * - prime 支持会话恢复(幂等)
21
+ */
22
+ export class TaskGraphService {
23
+ /**
24
+ * @param {import('../../repository/task/TaskRepository.impl.js').TaskRepositoryImpl} repository
25
+ * @param {import('./TaskReadyEngine.js').TaskReadyEngine} readyEngine
26
+ * @param {import('./TaskKnowledgeBridge.js').TaskKnowledgeBridge} [knowledgeBridge]
27
+ * @param {object} [auditLogger]
28
+ * @param {import('../../domain/task/TaskIdGenerator.js').TaskIdGenerator} idGenerator
29
+ */
30
+ constructor(repository, readyEngine, knowledgeBridge, auditLogger, idGenerator) {
31
+ this.repo = repository;
32
+ this.readyEngine = readyEngine;
33
+ this.bridge = knowledgeBridge || null;
34
+ this.auditLogger = auditLogger || null;
35
+ this.idGen = idGenerator;
36
+ this.logger = Logger.getInstance();
37
+ }
38
+
39
+ // ═══ 创建 ═══════════════════════════════════════════
40
+
41
+ /**
42
+ * 创建任务
43
+ * @param {object} data — { title, description, design, acceptance, priority, taskType, parentId }
44
+ * @returns {{ task: Task, isDuplicate: boolean }}
45
+ */
46
+ async create(data) {
47
+ const task = new Task(data);
48
+ task.computeContentHash();
49
+ task.validate();
50
+
51
+ // 去重检测(findByContentHash 已排除 closed 状态)
52
+ const duplicate = this.repo.findByContentHash(task.contentHash);
53
+ if (duplicate) {
54
+ return { task: duplicate, isDuplicate: true };
55
+ }
56
+
57
+ // 生成 ID
58
+ if (data.parentId) {
59
+ task.id = this.idGen.generateChild(data.parentId);
60
+ task.parentId = data.parentId;
61
+ } else {
62
+ task.id = this.idGen.generate();
63
+ }
64
+
65
+ // 事务:创建任务 + 子任务自动依赖
66
+ const saved = this.repo.inTransaction(() => {
67
+ const created = this.repo.create(task);
68
+
69
+ if (data.parentId) {
70
+ this.repo.addDependency(created.id, data.parentId, DepType.PARENT_CHILD);
71
+ }
72
+
73
+ return created;
74
+ });
75
+
76
+ this._logEvent(saved.id, 'created', null, saved.title);
77
+ return { task: saved, isDuplicate: false };
78
+ }
79
+
80
+ /**
81
+ * 批量拆解 Epic
82
+ * 一次性创建多个子任务 + 依赖关系,减少 Agent 的 MCP 调用次数
83
+ *
84
+ * @param {string} epicId
85
+ * @param {Array<object>} subtasks — [{ title, description, priority, taskType, blockedByIndex }]
86
+ * @returns {Task[]}
87
+ */
88
+ async decompose(epicId, subtasks) {
89
+ const epic = this.repo.findById(epicId);
90
+ if (!epic) throw new Error(`Epic not found: ${epicId}`);
91
+
92
+ const results = this.repo.inTransaction(() => {
93
+ const created = [];
94
+
95
+ for (let i = 0; i < subtasks.length; i++) {
96
+ const sub = subtasks[i];
97
+ const task = new Task({
98
+ ...sub,
99
+ parentId: epicId,
100
+ });
101
+ task.computeContentHash();
102
+ task.validate();
103
+ task.id = this.idGen.generateChild(epicId);
104
+ task.parentId = epicId;
105
+
106
+ const saved = this.repo.create(task);
107
+ this.repo.addDependency(saved.id, epicId, DepType.PARENT_CHILD);
108
+ created.push(saved);
109
+ }
110
+
111
+ // 子任务间的依赖(通过 index 引用,支持单个数字或数组)
112
+ for (let i = 0; i < subtasks.length; i++) {
113
+ const sub = subtasks[i];
114
+ if (sub.blockedByIndex == null) continue;
115
+
116
+ // 统一为数组处理
117
+ const indices = Array.isArray(sub.blockedByIndex)
118
+ ? sub.blockedByIndex
119
+ : [sub.blockedByIndex];
120
+
121
+ for (const idx of indices) {
122
+ if (typeof idx === 'number' && idx >= 0 && idx < created.length && idx !== i) {
123
+ this.repo.addDependency(created[i].id, created[idx].id, DepType.BLOCKS);
124
+ }
125
+ }
126
+ }
127
+
128
+ // Epic 等待所有子任务完成
129
+ for (const c of created) {
130
+ this.repo.addDependency(epicId, c.id, DepType.WAITS_FOR);
131
+ }
132
+
133
+ return created;
134
+ });
135
+
136
+ this._logEvent(epicId, 'decomposed', null, `${results.length} subtasks`);
137
+ return results;
138
+ }
139
+
140
+ // ═══ 工作流操作 ═══════════════════════════════════════
141
+
142
+ /**
143
+ * 认领任务
144
+ * @param {string} id
145
+ * @param {string} [assignee='agent']
146
+ * @returns {Task}
147
+ */
148
+ async claim(id, assignee = 'agent') {
149
+ const task = this.repo.findById(id);
150
+ if (!task) throw new Error(`Task not found: ${id}`);
151
+
152
+ const oldStatus = task.status;
153
+ task.claim(assignee);
154
+ const saved = this.repo.update(id, {
155
+ status: 'in_progress',
156
+ assignee,
157
+ updatedAt: Math.floor(Date.now() / 1000),
158
+ });
159
+
160
+ this._logEvent(id, 'status_changed', oldStatus, 'in_progress');
161
+ return saved;
162
+ }
163
+
164
+ /**
165
+ * 关闭任务
166
+ * 返回因此解除阻塞的新就绪任务列表
167
+ *
168
+ * @param {string} id
169
+ * @param {string} [reason='Completed']
170
+ * @returns {{ task: Task, newlyReady: string[] }}
171
+ */
172
+ async close(id, reason = 'Completed') {
173
+ const task = this.repo.findById(id);
174
+ if (!task) throw new Error(`Task not found: ${id}`);
175
+
176
+ const oldStatus = task.status;
177
+ task.close(reason);
178
+ const saved = this.repo.update(id, {
179
+ status: 'closed',
180
+ closeReason: reason,
181
+ closedAt: Math.floor(Date.now() / 1000),
182
+ updatedAt: Math.floor(Date.now() / 1000),
183
+ });
184
+
185
+ this._logEvent(id, 'closed', oldStatus, `closed: ${reason}`);
186
+
187
+ // 查找因此解除阻塞的任务
188
+ const newlyReady = this._checkNewlyUnblocked(id);
189
+ return { task: saved, newlyReady };
190
+ }
191
+
192
+ /**
193
+ * 标记任务失败
194
+ * 释放认领、递增失败计数、回退到 open
195
+ *
196
+ * @param {string} id
197
+ * @param {string} reason
198
+ * @returns {Task}
199
+ */
200
+ async fail(id, reason) {
201
+ const task = this.repo.findById(id);
202
+ if (!task) throw new Error(`Task not found: ${id}`);
203
+
204
+ task.fail(reason);
205
+ const saved = this.repo.update(id, {
206
+ status: 'open',
207
+ assignee: '',
208
+ failCount: task.failCount,
209
+ lastFailReason: task.lastFailReason,
210
+ updatedAt: Math.floor(Date.now() / 1000),
211
+ });
212
+
213
+ this._logEvent(id, 'failed', 'in_progress', reason);
214
+ return saved;
215
+ }
216
+
217
+ /**
218
+ * 推迟任务
219
+ * @param {string} id
220
+ * @param {string} [reason='']
221
+ * @returns {Task}
222
+ */
223
+ async defer(id, reason = '') {
224
+ const task = this.repo.findById(id);
225
+ if (!task) throw new Error(`Task not found: ${id}`);
226
+
227
+ const oldStatus = task.status;
228
+ task.defer(reason);
229
+ const saved = this.repo.update(id, {
230
+ status: 'deferred',
231
+ notes: task.notes,
232
+ updatedAt: Math.floor(Date.now() / 1000),
233
+ });
234
+
235
+ this._logEvent(id, 'deferred', oldStatus, reason);
236
+ return saved;
237
+ }
238
+
239
+ /**
240
+ * 上报进度(长任务的中间状态更新)
241
+ * @param {string} id
242
+ * @param {string} note
243
+ * @returns {Task}
244
+ */
245
+ async progress(id, note) {
246
+ const task = this.repo.findById(id);
247
+ if (!task) throw new Error(`Task not found: ${id}`);
248
+ if (task.status !== 'in_progress') {
249
+ throw new Error(`Cannot update progress: task ${id} is ${task.status}, expected in_progress`);
250
+ }
251
+
252
+ const saved = this.repo.update(id, {
253
+ notes: note,
254
+ updatedAt: Math.floor(Date.now() / 1000),
255
+ });
256
+
257
+ this._logEvent(id, 'progress', null, note);
258
+ return saved;
259
+ }
260
+
261
+ // ═══ 依赖管理 ═══════════════════════════════════════
262
+
263
+ /**
264
+ * 添加依赖
265
+ * @param {string} taskId
266
+ * @param {string} dependsOnId
267
+ * @param {string} [depType='blocks']
268
+ */
269
+ async addDependency(taskId, dependsOnId, depType = 'blocks') {
270
+ if (taskId === dependsOnId) {
271
+ throw new Error('Self-dependency is not allowed');
272
+ }
273
+ if (!isValidDepType(depType)) {
274
+ throw new Error(`Invalid dependency type: ${depType}`);
275
+ }
276
+
277
+ // 阻塞型依赖需要环检测
278
+ if (affectsReadyWork(depType)) {
279
+ const hasCycle = this.repo.hasReachablePath(dependsOnId, taskId);
280
+ if (hasCycle) {
281
+ throw new Error(`Cycle detected: ${taskId} → ${dependsOnId}`);
282
+ }
283
+ }
284
+
285
+ this.repo.addDependency(taskId, dependsOnId, depType);
286
+ this._logEvent(taskId, 'dependency_added', null, `${depType}: ${dependsOnId}`);
287
+ }
288
+
289
+ // ═══ 查询操作 ═══════════════════════════════════════
290
+
291
+ /**
292
+ * 获取就绪任务 + 知识上下文
293
+ * @param {object} [options] — { limit, withKnowledge }
294
+ * @returns {Promise<Task[]>}
295
+ */
296
+ async ready(options = {}) {
297
+ const tasks = this.readyEngine.getReadyWork(options);
298
+
299
+ if (this.bridge && options.withKnowledge !== false) {
300
+ return this.bridge.enrichWithKnowledge(tasks);
301
+ }
302
+
303
+ return tasks;
304
+ }
305
+
306
+ /**
307
+ * 获取被阻塞的任务
308
+ */
309
+ async blocked() {
310
+ return this.readyEngine.getBlockedWork();
311
+ }
312
+
313
+ /**
314
+ * 获取单个任务详情
315
+ * @param {string} id
316
+ * @returns {Task|null}
317
+ */
318
+ async show(id) {
319
+ return this.repo.findById(id);
320
+ }
321
+
322
+ /**
323
+ * 列表查询
324
+ * @param {object} filters — { status, taskType, assignee, parentId }
325
+ * @param {object} options — { limit }
326
+ * @returns {Task[]}
327
+ */
328
+ async list(filters = {}, options = {}) {
329
+ return this.repo.findAll(filters, options);
330
+ }
331
+
332
+ /**
333
+ * 依赖树
334
+ */
335
+ async depTree(taskId) {
336
+ return this.readyEngine.getDependencyTree(taskId);
337
+ }
338
+
339
+ /**
340
+ * 统计信息
341
+ */
342
+ async stats() {
343
+ return this.repo.getStatistics();
344
+ }
345
+
346
+ /**
347
+ * Prime — 会话恢复(幂等)
348
+ *
349
+ * 返回当前进行中的任务 + 就绪任务 + 统计信息。
350
+ * Agent 在新会话开始或上下文压缩后调用。
351
+ *
352
+ * @param {object} [options] — { withKnowledge }
353
+ * @returns {{ inProgress: Task[], ready: Task[], stats: object }}
354
+ */
355
+ async prime(options = {}) {
356
+ const inProgress = this.repo.findAll({ status: 'in_progress' }, { limit: 10 });
357
+ const readyTasks = await this.ready({
358
+ limit: options.limit || 5,
359
+ withKnowledge: options.withKnowledge !== false,
360
+ });
361
+ const statistics = await this.stats();
362
+
363
+ return {
364
+ inProgress: inProgress.map((t) => t.toJSON()),
365
+ ready: readyTasks.map((t) => (t.toJSON ? t.toJSON() : t)),
366
+ stats: statistics,
367
+ };
368
+ }
369
+
370
+ // ═══ 私有方法 ═══════════════════════════════════════
371
+
372
+ /**
373
+ * 查找因 closedTaskId 完成而新解除阻塞的任务
374
+ * @private
375
+ */
376
+ _checkNewlyUnblocked(closedTaskId) {
377
+ const dependents = this.repo.getDependents(closedTaskId);
378
+ const newlyReady = [];
379
+
380
+ for (const dep of dependents) {
381
+ // 只关注阻塞型依赖
382
+ if (dep.dep_type !== 'blocks' && dep.dep_type !== 'waits-for') continue;
383
+
384
+ // getBlockers 返回"尚未关闭的阻塞者",空 = 全部完成
385
+ const pendingBlockers = this.repo.getBlockers(dep.task_id);
386
+ if (pendingBlockers.length === 0) {
387
+ // 还要检查任务本身状态是 open
388
+ const task = this.repo.findById(dep.task_id);
389
+ if (task && task.status === 'open') {
390
+ newlyReady.push(dep.task_id);
391
+ }
392
+ }
393
+ }
394
+
395
+ return newlyReady;
396
+ }
397
+
398
+ /**
399
+ * @private
400
+ */
401
+ _logEvent(taskId, eventType, oldValue, newValue) {
402
+ try {
403
+ this.repo.logEvent(taskId, eventType, oldValue, newValue);
404
+ } catch (err) {
405
+ this.logger.debug('TaskGraphService._logEvent error', { error: err.message });
406
+ }
407
+ }
408
+ }
409
+
410
+ export default TaskGraphService;