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.
- package/README.md +1 -0
- package/bin/cli.js +242 -23
- package/dashboard/dist/assets/{icons-CC5R_iwL.js → icons-18VxiaCT.js} +108 -98
- package/dashboard/dist/assets/index-BJiuaVPD.css +1 -0
- package/dashboard/dist/assets/{index-WmnJCXq4.js → index-CRH5Umim.js} +50 -50
- package/dashboard/dist/index.html +3 -3
- package/lib/cli/SetupService.js +152 -21
- package/lib/domain/task/Task.js +214 -0
- package/lib/domain/task/TaskDependency.js +48 -0
- package/lib/domain/task/TaskIdGenerator.js +83 -0
- package/lib/domain/task/index.js +6 -0
- package/lib/external/mcp/McpServer.js +4 -4
- package/lib/external/mcp/handlers/task.js +295 -0
- package/lib/external/mcp/tools.js +100 -3
- package/lib/http/HttpServer.js +8 -0
- package/lib/http/routes/guard.js +283 -0
- package/lib/http/routes/task.js +282 -0
- package/lib/infrastructure/config/Paths.js +18 -8
- package/lib/infrastructure/database/migrations/002_add_tasks.js +88 -0
- package/lib/injection/ServiceContainer.js +58 -0
- package/lib/repository/task/TaskRepository.impl.js +398 -0
- package/lib/service/cursor/AgentInstructionsGenerator.js +28 -9
- package/lib/service/cursor/CursorDeliveryPipeline.js +42 -20
- package/lib/service/cursor/KnowledgeCompressor.js +40 -0
- package/lib/service/cursor/TokenBudget.js +2 -2
- package/lib/service/guard/GuardFeedbackLoop.js +17 -2
- package/lib/service/knowledge/KnowledgeService.js +6 -0
- package/lib/service/task/TaskGraphService.js +410 -0
- package/lib/service/task/TaskKnowledgeBridge.js +86 -0
- package/lib/service/task/TaskReadyEngine.js +127 -0
- package/lib/shared/constants.js +3 -3
- package/package.json +1 -1
- package/skills/autosnippet-intent/SKILL.md +4 -1
- package/skills/autosnippet-recipes/SKILL.md +17 -2
- package/templates/claude-hooks.yaml +19 -0
- package/templates/copilot-instructions.md +33 -1
- package/templates/cursor-rules/autosnippet-conventions.mdc +12 -0
- package/templates/cursor-rules/autosnippet-workflow.mdc +43 -0
- package/templates/guard-ci.yml +1 -0
- package/templates/pre-commit-guard.sh +2 -1
- 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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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;
|