autosnippet 3.2.2 → 3.2.3
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 +23 -0
- package/config/default.json +7 -0
- package/dashboard/dist/assets/{icons-18VxiaCT.js → icons-pSac4wYO.js} +101 -96
- package/dashboard/dist/assets/{index-CRH5Umim.js → index-6itPuGFl.js} +45 -45
- package/dashboard/dist/assets/index-DNOHYBhy.css +1 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/cli/SetupService.js +93 -25
- package/lib/domain/knowledge/KnowledgeEntry.js +11 -0
- package/lib/domain/task/Task.js +32 -2
- package/lib/domain/task/TaskDependency.js +1 -0
- package/lib/external/mcp/McpServer.js +180 -6
- package/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +2 -1
- package/lib/external/mcp/handlers/decide.js +109 -0
- package/lib/external/mcp/handlers/ready.js +42 -0
- package/lib/external/mcp/handlers/system.js +12 -0
- package/lib/external/mcp/handlers/task.js +7 -19
- package/lib/external/mcp/tools.js +83 -42
- package/lib/http/routes/knowledge.js +10 -10
- package/lib/http/routes/task.js +81 -1
- package/lib/http/utils/routeHelpers.js +30 -0
- package/lib/repository/task/TaskRepository.impl.js +3 -1
- package/lib/service/cursor/AgentInstructionsGenerator.js +6 -4
- package/lib/service/knowledge/KnowledgeService.js +12 -1
- package/lib/service/task/TaskGraphService.js +243 -3
- package/package.json +1 -1
- package/skills/autosnippet-intent/SKILL.md +3 -1
- package/skills/autosnippet-recipes/SKILL.md +3 -1
- package/templates/copilot-instructions.md +47 -14
- package/templates/cursor-rules/autosnippet-conventions.mdc +11 -0
- package/templates/cursor-rules/autosnippet-workflow.mdc +16 -7
- package/templates/guard-ci.yml +21 -0
- package/dashboard/dist/assets/index-BJiuaVPD.css +0 -1
|
@@ -9,7 +9,7 @@ import Logger from '../../infrastructure/logging/Logger.js';
|
|
|
9
9
|
import { getServiceContainer } from '../../injection/ServiceContainer.js';
|
|
10
10
|
import { ValidationError } from '../../shared/errors/index.js';
|
|
11
11
|
import { asyncHandler } from '../middleware/errorHandler.js';
|
|
12
|
-
import { getContext, safeInt } from '../utils/routeHelpers.js';
|
|
12
|
+
import { getContext, safeInt, sanitizeForAPI, sanitizePaginatedForAPI } from '../utils/routeHelpers.js';
|
|
13
13
|
|
|
14
14
|
const _logger = Logger.getInstance();
|
|
15
15
|
const router = express.Router();
|
|
@@ -35,7 +35,7 @@ router.get(
|
|
|
35
35
|
|
|
36
36
|
if (keyword) {
|
|
37
37
|
const result = await knowledgeService.search(keyword, { page, pageSize });
|
|
38
|
-
return res.json({ success: true, data: result });
|
|
38
|
+
return res.json({ success: true, data: sanitizePaginatedForAPI(result) });
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const filters = {};
|
|
@@ -65,7 +65,7 @@ router.get(
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
const result = await knowledgeService.list(filters, { page, pageSize });
|
|
68
|
-
res.json({ success: true, data: result });
|
|
68
|
+
res.json({ success: true, data: sanitizePaginatedForAPI(result) });
|
|
69
69
|
})
|
|
70
70
|
);
|
|
71
71
|
|
|
@@ -94,7 +94,7 @@ router.get(
|
|
|
94
94
|
const container = getServiceContainer();
|
|
95
95
|
const knowledgeService = container.get('knowledgeService');
|
|
96
96
|
const entry = await knowledgeService.get(id);
|
|
97
|
-
res.json({ success: true, data: entry
|
|
97
|
+
res.json({ success: true, data: sanitizeForAPI(entry) });
|
|
98
98
|
})
|
|
99
99
|
);
|
|
100
100
|
|
|
@@ -120,7 +120,7 @@ router.post(
|
|
|
120
120
|
const entry = await knowledgeService.create(data, context);
|
|
121
121
|
res.status(201).json({
|
|
122
122
|
success: true,
|
|
123
|
-
data: entry
|
|
123
|
+
data: sanitizeForAPI(entry),
|
|
124
124
|
});
|
|
125
125
|
})
|
|
126
126
|
);
|
|
@@ -138,7 +138,7 @@ router.patch(
|
|
|
138
138
|
const context = getContext(req);
|
|
139
139
|
|
|
140
140
|
const entry = await knowledgeService.update(id, req.body, context);
|
|
141
|
-
res.json({ success: true, data: entry
|
|
141
|
+
res.json({ success: true, data: sanitizeForAPI(entry) });
|
|
142
142
|
})
|
|
143
143
|
);
|
|
144
144
|
|
|
@@ -174,7 +174,7 @@ router.patch(
|
|
|
174
174
|
const context = getContext(req);
|
|
175
175
|
|
|
176
176
|
const entry = await knowledgeService.publish(id, context);
|
|
177
|
-
res.json({ success: true, data: entry
|
|
177
|
+
res.json({ success: true, data: sanitizeForAPI(entry) });
|
|
178
178
|
})
|
|
179
179
|
);
|
|
180
180
|
|
|
@@ -197,7 +197,7 @@ router.patch(
|
|
|
197
197
|
const context = getContext(req);
|
|
198
198
|
|
|
199
199
|
const entry = await knowledgeService.deprecate(id, reason, context);
|
|
200
|
-
res.json({ success: true, data: entry
|
|
200
|
+
res.json({ success: true, data: sanitizeForAPI(entry) });
|
|
201
201
|
})
|
|
202
202
|
);
|
|
203
203
|
|
|
@@ -214,7 +214,7 @@ router.patch(
|
|
|
214
214
|
const context = getContext(req);
|
|
215
215
|
|
|
216
216
|
const entry = await knowledgeService.reactivate(id, context);
|
|
217
|
-
res.json({ success: true, data: entry
|
|
217
|
+
res.json({ success: true, data: sanitizeForAPI(entry) });
|
|
218
218
|
})
|
|
219
219
|
);
|
|
220
220
|
|
|
@@ -245,7 +245,7 @@ router.post(
|
|
|
245
245
|
ids.map((id) => knowledgeService.publish(id, context))
|
|
246
246
|
);
|
|
247
247
|
|
|
248
|
-
const published = results.filter((r) => r.status === 'fulfilled').map((r) => r.value
|
|
248
|
+
const published = results.filter((r) => r.status === 'fulfilled').map((r) => sanitizeForAPI(r.value));
|
|
249
249
|
const failed = results
|
|
250
250
|
.map((r, i) => (r.status === 'rejected' ? { id: ids[i], error: r.reason?.message } : null))
|
|
251
251
|
.filter(Boolean);
|
package/lib/http/routes/task.js
CHANGED
|
@@ -105,6 +105,12 @@ async function _dispatch(svc, operation, params) {
|
|
|
105
105
|
return _depTree(svc, params);
|
|
106
106
|
case 'stats':
|
|
107
107
|
return _stats(svc);
|
|
108
|
+
case 'record_decision':
|
|
109
|
+
return _recordDecision(svc, params);
|
|
110
|
+
case 'revise_decision':
|
|
111
|
+
return _reviseDecision(svc, params);
|
|
112
|
+
case 'unpin_decision':
|
|
113
|
+
return _unpinDecision(svc, params);
|
|
108
114
|
default:
|
|
109
115
|
return { success: false, message: `Unknown operation: ${operation}` };
|
|
110
116
|
}
|
|
@@ -197,7 +203,24 @@ async function _progress(svc, args) {
|
|
|
197
203
|
|
|
198
204
|
async function _prime(svc) {
|
|
199
205
|
const result = await svc.prime({ withKnowledge: true });
|
|
200
|
-
|
|
206
|
+
const decisionCount = (result.decisions || []).length;
|
|
207
|
+
const staleCount = (result.staleDecisions || []).length;
|
|
208
|
+
const decisionTitles = (result.decisions || []).map((d) => d.title).join('; ');
|
|
209
|
+
const statsLine = `${result.inProgress.length} in-progress, ${result.ready.length} ready, ${result.stats.total} total`;
|
|
210
|
+
|
|
211
|
+
let message;
|
|
212
|
+
if (decisionCount > 0) {
|
|
213
|
+
const stalePart = staleCount > 0 ? ` ${staleCount} stale.` : '';
|
|
214
|
+
message = `⚠️ ${decisionCount} ACTIVE DECISION(S): [${decisionTitles}].${stalePart} ${statsLine}.`;
|
|
215
|
+
} else {
|
|
216
|
+
message = `${statsLine}.`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
success: true,
|
|
221
|
+
data: result,
|
|
222
|
+
message,
|
|
223
|
+
};
|
|
201
224
|
}
|
|
202
225
|
|
|
203
226
|
// ── decompose ──
|
|
@@ -279,4 +302,61 @@ async function _stats(svc) {
|
|
|
279
302
|
return { success: true, data: stats };
|
|
280
303
|
}
|
|
281
304
|
|
|
305
|
+
// ── record_decision ──
|
|
306
|
+
|
|
307
|
+
async function _recordDecision(svc, args) {
|
|
308
|
+
if (!args.title) return { success: false, message: 'title is required' };
|
|
309
|
+
if (!args.description) return { success: false, message: 'description is required' };
|
|
310
|
+
const { task, isDuplicate } = await svc.recordDecision({
|
|
311
|
+
title: args.title,
|
|
312
|
+
description: args.description,
|
|
313
|
+
rationale: args.rationale || '',
|
|
314
|
+
tags: args.tags || [],
|
|
315
|
+
relatedTaskId: args.relatedTaskId || null,
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
success: true,
|
|
319
|
+
data: task.toJSON(),
|
|
320
|
+
message: isDuplicate
|
|
321
|
+
? `Decision already recorded: ${task.id}`
|
|
322
|
+
: `Decision pinned: ${task.id} — "${args.title}"`,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ── revise_decision ──
|
|
327
|
+
|
|
328
|
+
async function _reviseDecision(svc, args) {
|
|
329
|
+
if (!args.id) return { success: false, message: 'id of old decision is required' };
|
|
330
|
+
if (!args.title) return { success: false, message: 'title of new decision is required' };
|
|
331
|
+
if (!args.description)
|
|
332
|
+
return { success: false, message: 'description of new decision is required' };
|
|
333
|
+
const result = await svc.reviseDecision({
|
|
334
|
+
oldDecisionId: args.id,
|
|
335
|
+
title: args.title,
|
|
336
|
+
description: args.description,
|
|
337
|
+
rationale: args.rationale || '',
|
|
338
|
+
reason: args.reason || '',
|
|
339
|
+
});
|
|
340
|
+
return {
|
|
341
|
+
success: true,
|
|
342
|
+
data: {
|
|
343
|
+
newDecision: result.newDecision.toJSON(),
|
|
344
|
+
superseded: result.oldDecisionId,
|
|
345
|
+
},
|
|
346
|
+
message: `Decision revised: ${result.oldDecisionId} → ${result.newDecision.id}`,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── unpin_decision ──
|
|
351
|
+
|
|
352
|
+
async function _unpinDecision(svc, args) {
|
|
353
|
+
if (!args.id) return { success: false, message: 'id is required' };
|
|
354
|
+
const task = await svc.unpinDecision(args.id, args.reason || '');
|
|
355
|
+
return {
|
|
356
|
+
success: true,
|
|
357
|
+
data: task.toJSON(),
|
|
358
|
+
message: `Decision ${args.id} unpinned and closed`,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
282
362
|
export default router;
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* 提取自各路由文件中的重复实现
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { KnowledgeEntry } from '../../domain/knowledge/KnowledgeEntry.js';
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* 从请求中提取操作上下文(用户身份、IP、UA)
|
|
8
10
|
* @param {import('express').Request} req
|
|
@@ -31,3 +33,31 @@ export function safeInt(value, defaultValue, min = 1, max = 1000) {
|
|
|
31
33
|
}
|
|
32
34
|
return Math.max(min, Math.min(max, parsed));
|
|
33
35
|
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 将 KnowledgeEntry(或其 toJSON 输出)转换为对外 API 安全的格式
|
|
39
|
+
* - 过滤系统内部标签(dimension:* / bootstrap:* 等)
|
|
40
|
+
*
|
|
41
|
+
* @param {KnowledgeEntry|Object} entryOrJson 实体或 toJSON 输出
|
|
42
|
+
* @returns {Object} 过滤后的 JSON
|
|
43
|
+
*/
|
|
44
|
+
export function sanitizeForAPI(entryOrJson) {
|
|
45
|
+
const json = typeof entryOrJson?.toJSON === 'function' ? entryOrJson.toJSON() : { ...entryOrJson };
|
|
46
|
+
if (Array.isArray(json.tags)) {
|
|
47
|
+
json.tags = json.tags.filter((t) => !KnowledgeEntry.isSystemTag(t));
|
|
48
|
+
}
|
|
49
|
+
return json;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 将分页结果中的 data 数组批量过滤系统标签
|
|
54
|
+
* @param {{ data: Array, pagination: Object }} result
|
|
55
|
+
* @returns {{ data: Array, pagination: Object }}
|
|
56
|
+
*/
|
|
57
|
+
export function sanitizePaginatedForAPI(result) {
|
|
58
|
+
if (!result?.data) return result;
|
|
59
|
+
return {
|
|
60
|
+
...result,
|
|
61
|
+
data: result.data.map(sanitizeForAPI),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -108,7 +108,8 @@ export class TaskRepositoryImpl {
|
|
|
108
108
|
SUM(CASE WHEN status = 'open' THEN 1 ELSE 0 END) as open,
|
|
109
109
|
SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
|
|
110
110
|
SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END) as closed,
|
|
111
|
-
SUM(CASE WHEN status = 'deferred' THEN 1 ELSE 0 END) as deferred
|
|
111
|
+
SUM(CASE WHEN status = 'deferred' THEN 1 ELSE 0 END) as deferred,
|
|
112
|
+
SUM(CASE WHEN status = 'pinned' THEN 1 ELSE 0 END) as pinned
|
|
112
113
|
FROM tasks
|
|
113
114
|
`);
|
|
114
115
|
}
|
|
@@ -318,6 +319,7 @@ export class TaskRepositoryImpl {
|
|
|
318
319
|
in_progress: row.in_progress || 0,
|
|
319
320
|
closed: row.closed || 0,
|
|
320
321
|
deferred: row.deferred || 0,
|
|
322
|
+
pinned: row.pinned || 0,
|
|
321
323
|
};
|
|
322
324
|
}
|
|
323
325
|
|
|
@@ -55,7 +55,9 @@ const MCP_TOOLS_SUMMARY = [
|
|
|
55
55
|
{ name: 'autosnippet_skill', desc: 'Skill management (list/load/create/update/delete/suggest)' },
|
|
56
56
|
{ name: 'autosnippet_save_document', desc: 'Save development document (auto-publish)' },
|
|
57
57
|
{ name: 'autosnippet_bootstrap', desc: 'Project cold-start & scan (knowledge/refine/scan)' },
|
|
58
|
-
{ name: '
|
|
58
|
+
{ name: 'autosnippet_ready', desc: 'Session entry point — loads decisions + ready tasks (call FIRST)' },
|
|
59
|
+
{ name: 'autosnippet_decide', desc: 'Decision management (record/revise/unpin/list)' },
|
|
60
|
+
{ name: 'autosnippet_task', desc: 'Task CRUD (create/claim/close/fail/defer/progress/decompose)' },
|
|
59
61
|
{ name: 'autosnippet_health', desc: 'Service health & KB statistics' },
|
|
60
62
|
{ name: 'autosnippet_capabilities', desc: 'List all available MCP tools (self-discovery)' },
|
|
61
63
|
];
|
|
@@ -279,7 +281,7 @@ export class AgentInstructionsGenerator {
|
|
|
279
281
|
// Key tools highlight for Claude
|
|
280
282
|
lines.push('### Recommended Workflow', '');
|
|
281
283
|
lines.push(
|
|
282
|
-
'1. **Session start**: `
|
|
284
|
+
'1. **Session start**: `autosnippet_ready()` — Restore task context and decisions'
|
|
283
285
|
);
|
|
284
286
|
lines.push(
|
|
285
287
|
'2. **Before writing code**: `autosnippet_search({ query: "<topic>" })` to find relevant patterns'
|
|
@@ -405,7 +407,7 @@ export class AgentInstructionsGenerator {
|
|
|
405
407
|
'## Recommended Workflow',
|
|
406
408
|
'',
|
|
407
409
|
'### Session Start (ALWAYS)',
|
|
408
|
-
'1. `
|
|
410
|
+
'1. `autosnippet_ready()` — Restore task context and decisions',
|
|
409
411
|
'',
|
|
410
412
|
'### Task Lifecycle',
|
|
411
413
|
'2. `autosnippet_task({ operation: "claim", id: "asd-xxx" })` — Start working',
|
|
@@ -423,7 +425,7 @@ export class AgentInstructionsGenerator {
|
|
|
423
425
|
'',
|
|
424
426
|
'### Context Pressure',
|
|
425
427
|
'- `_contextHint: CONTEXT_PRESSURE:WARNING` → Summarize completed work, then continue',
|
|
426
|
-
'- `_contextHint: CONTEXT_PRESSURE:CRITICAL` → Call `
|
|
428
|
+
'- `_contextHint: CONTEXT_PRESSURE:CRITICAL` → Call `autosnippet_ready()` immediately',
|
|
427
429
|
'',
|
|
428
430
|
];
|
|
429
431
|
}
|
|
@@ -225,11 +225,22 @@ export class KnowledgeService {
|
|
|
225
225
|
case 'relations':
|
|
226
226
|
case 'constraints':
|
|
227
227
|
case 'reasoning':
|
|
228
|
-
case 'tags':
|
|
229
228
|
case 'headers':
|
|
230
229
|
case 'headerPaths':
|
|
231
230
|
dbUpdates[key] = data[key];
|
|
232
231
|
break;
|
|
232
|
+
|
|
233
|
+
// tags 需要特殊处理:API 返回时已过滤系统标签,保存时需要合并回来
|
|
234
|
+
case 'tags': {
|
|
235
|
+
const existingSystemTags = (_entry.tags || []).filter((t) =>
|
|
236
|
+
KnowledgeEntry.isSystemTag(t)
|
|
237
|
+
);
|
|
238
|
+
const incomingUserTags = (data.tags || []).filter(
|
|
239
|
+
(t) => !KnowledgeEntry.isSystemTag(t)
|
|
240
|
+
);
|
|
241
|
+
dbUpdates.tags = [...incomingUserTags, ...existingSystemTags];
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
233
244
|
}
|
|
234
245
|
}
|
|
235
246
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Task } from '../../domain/task/Task.js';
|
|
2
|
-
import {
|
|
2
|
+
import { affectsReadyWork, DepType, isValidDepType } from '../../domain/task/TaskDependency.js';
|
|
3
3
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -88,6 +88,13 @@ export class TaskGraphService {
|
|
|
88
88
|
async decompose(epicId, subtasks) {
|
|
89
89
|
const epic = this.repo.findById(epicId);
|
|
90
90
|
if (!epic) throw new Error(`Epic not found: ${epicId}`);
|
|
91
|
+
// 决策不可拆解(C6)
|
|
92
|
+
if (epic.taskType === 'decision') {
|
|
93
|
+
throw new Error('Cannot decompose a decision. Decisions are atomic records.');
|
|
94
|
+
}
|
|
95
|
+
if (epic.status === 'pinned') {
|
|
96
|
+
throw new Error('Cannot decompose a pinned task.');
|
|
97
|
+
}
|
|
91
98
|
|
|
92
99
|
const results = this.repo.inTransaction(() => {
|
|
93
100
|
const created = [];
|
|
@@ -359,16 +366,249 @@ export class TaskGraphService {
|
|
|
359
366
|
withKnowledge: options.withKnowledge !== false,
|
|
360
367
|
});
|
|
361
368
|
const statistics = await this.stats();
|
|
362
|
-
|
|
363
|
-
|
|
369
|
+
// 按创建时间降序,确保超出 limit 时保留最新决策(C5)
|
|
370
|
+
// 双重过滤 status+taskType,避免 pinned 语义过载(D1)
|
|
371
|
+
const pinnedDecisions = this.repo.findAll(
|
|
372
|
+
{ status: 'pinned', taskType: 'decision' },
|
|
373
|
+
{ limit: 50, orderBy: 'created_at DESC' }
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const result = {
|
|
364
377
|
inProgress: inProgress.map((t) => t.toJSON()),
|
|
365
378
|
ready: readyTasks.map((t) => (t.toJSON ? t.toJSON() : t)),
|
|
366
379
|
stats: statistics,
|
|
367
380
|
};
|
|
381
|
+
|
|
382
|
+
if (pinnedDecisions.length > 0) {
|
|
383
|
+
// P2: Stale detection — 超过阈值的决策标记为 stale
|
|
384
|
+
const staleThresholdSec = this._getDecisionStaleThreshold();
|
|
385
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
386
|
+
const activeDecisions = [];
|
|
387
|
+
const staleDecisions = [];
|
|
388
|
+
|
|
389
|
+
for (const t of pinnedDecisions) {
|
|
390
|
+
const isStale =
|
|
391
|
+
t.metadata?.staleSince ||
|
|
392
|
+
(staleThresholdSec > 0 && t.createdAt && nowSec - t.createdAt > staleThresholdSec);
|
|
393
|
+
if (isStale) {
|
|
394
|
+
staleDecisions.push(t);
|
|
395
|
+
} else {
|
|
396
|
+
activeDecisions.push(t);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// P1 Result Compaction: compact 模式 — description 截断到 120 chars
|
|
401
|
+
// Agent 需要完整内容时调用 autosnippet_decide({ operation: 'list' })
|
|
402
|
+
result.decisions = activeDecisions.map((t) => ({
|
|
403
|
+
id: t.id,
|
|
404
|
+
title: t.title,
|
|
405
|
+
summary:
|
|
406
|
+
typeof t.description === 'string'
|
|
407
|
+
? t.description
|
|
408
|
+
.replace(/^## Decision\n/, '')
|
|
409
|
+
.split(/\n\n## /)[0]
|
|
410
|
+
.slice(0, 120)
|
|
411
|
+
: '',
|
|
412
|
+
createdAt: t.createdAt,
|
|
413
|
+
}));
|
|
414
|
+
|
|
415
|
+
// P2: stale decisions 单独返回(轻量格式,仅供提示)
|
|
416
|
+
if (staleDecisions.length > 0) {
|
|
417
|
+
result.staleDecisions = staleDecisions.map((t) => ({
|
|
418
|
+
id: t.id,
|
|
419
|
+
title: t.title,
|
|
420
|
+
createdAt: t.createdAt,
|
|
421
|
+
ageDays: Math.floor((nowSec - (t.createdAt || nowSec)) / 86400),
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
result._decisionHint =
|
|
426
|
+
'These are team-agreed decisions (compact view). Respect them in your response. ' +
|
|
427
|
+
'Call autosnippet_decide({ operation: "list" }) for full details. ' +
|
|
428
|
+
"If the user's request conflicts with a decision, point out the conflict and ask whether to revise_decision.";
|
|
429
|
+
|
|
430
|
+
if (staleDecisions.length > 0) {
|
|
431
|
+
result._staleHint =
|
|
432
|
+
`${staleDecisions.length} decision(s) are stale (>${Math.floor(staleThresholdSec / 86400)} days). ` +
|
|
433
|
+
'Consider reviewing with autosnippet_decide({ operation: "list" }) and unpin outdated ones.';
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── 完整协议指令(从 copilot-instructions.md 移到 prime 返回值)──
|
|
438
|
+
result._protocol = [
|
|
439
|
+
'Respect decisions above. Record new decisions when user agrees/disagrees on something.',
|
|
440
|
+
'Use create/close to track multi-step work.',
|
|
441
|
+
].join('\n');
|
|
442
|
+
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ═══ 决策管理 ═══════════════════════════════════════
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 记录决策 — 直接以 pinned 状态创建,避免 open→pinned 的幽灵窗口(C1)
|
|
450
|
+
* @param {object} params — { title, description, rationale, tags, relatedTaskId }
|
|
451
|
+
* @returns {{ task: Task, isDuplicate: boolean }}
|
|
452
|
+
*/
|
|
453
|
+
async recordDecision({ title, description, rationale, tags, relatedTaskId }) {
|
|
454
|
+
if (!title) throw new Error('Decision title is required');
|
|
455
|
+
if (!description) throw new Error('Decision description is required');
|
|
456
|
+
|
|
457
|
+
let formattedDesc = `## Decision\n${description}`;
|
|
458
|
+
if (rationale) {
|
|
459
|
+
formattedDesc += `\n\n## Rationale\n${rationale}`;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// 直接以 pinned 状态构建 Task,避免 open→pinned 的幽灵窗口
|
|
463
|
+
const task = new Task({
|
|
464
|
+
title,
|
|
465
|
+
description: formattedDesc,
|
|
466
|
+
taskType: 'decision',
|
|
467
|
+
status: 'pinned',
|
|
468
|
+
priority: 0,
|
|
469
|
+
// C8: 传递普通对象,_entityToRow() 负责 JSON.stringify
|
|
470
|
+
metadata: {
|
|
471
|
+
tags: tags || [],
|
|
472
|
+
source: 'agent-user-agreement',
|
|
473
|
+
rationale: rationale || '',
|
|
474
|
+
recordedAt: new Date().toISOString(),
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
task.computeContentHash();
|
|
478
|
+
task.validate();
|
|
479
|
+
|
|
480
|
+
// 去重检测(复用 create 的逻辑)
|
|
481
|
+
const duplicate = this.repo.findByContentHash(task.contentHash);
|
|
482
|
+
if (duplicate) {
|
|
483
|
+
return { task: duplicate, isDuplicate: true };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
task.id = this.idGen.generate();
|
|
487
|
+
|
|
488
|
+
// 单事务:创建 + 关联依赖
|
|
489
|
+
const saved = this.repo.inTransaction(() => {
|
|
490
|
+
const created = this.repo.create(task);
|
|
491
|
+
if (relatedTaskId) {
|
|
492
|
+
try {
|
|
493
|
+
this.repo.addDependency(created.id, relatedTaskId, 'related');
|
|
494
|
+
} catch (err) {
|
|
495
|
+
this.logger.debug('recordDecision: dependency add failed', { error: err.message });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return created;
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
this._logEvent(saved.id, 'decision_recorded', null, title);
|
|
502
|
+
return { task: saved, isDuplicate: false };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* 修订决策 — 原子事务:创建新决策 + 关闭旧决策 + 建立 supersedes 链(C2+C7)
|
|
507
|
+
* @param {object} params — { oldDecisionId, title, description, rationale, reason }
|
|
508
|
+
* @returns {{ newDecision: Task, oldDecisionId: string }}
|
|
509
|
+
*/
|
|
510
|
+
async reviseDecision({ oldDecisionId, title, description, rationale, reason }) {
|
|
511
|
+
// 事务前验证
|
|
512
|
+
const oldDecision = this.repo.findById(oldDecisionId);
|
|
513
|
+
if (!oldDecision) throw new Error(`Decision not found: ${oldDecisionId}`);
|
|
514
|
+
if (oldDecision.status !== 'pinned') {
|
|
515
|
+
throw new Error(`Can only revise pinned decisions (current: ${oldDecision.status})`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
let formattedDesc = `## Decision\n${description}`;
|
|
519
|
+
if (rationale) {
|
|
520
|
+
formattedDesc += `\n\n## Rationale\n${rationale}`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// 构建新决策 Task
|
|
524
|
+
const newTask = new Task({
|
|
525
|
+
title,
|
|
526
|
+
description: formattedDesc,
|
|
527
|
+
taskType: 'decision',
|
|
528
|
+
status: 'pinned',
|
|
529
|
+
priority: 0,
|
|
530
|
+
metadata: {
|
|
531
|
+
tags: [],
|
|
532
|
+
source: 'agent-user-agreement',
|
|
533
|
+
rationale: rationale || '',
|
|
534
|
+
recordedAt: new Date().toISOString(),
|
|
535
|
+
supersedes: oldDecisionId,
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
newTask.computeContentHash();
|
|
539
|
+
newTask.validate();
|
|
540
|
+
newTask.id = this.idGen.generate();
|
|
541
|
+
|
|
542
|
+
// 原子事务:创建新决策 + 关闭旧决策 + 建立 supersedes 链
|
|
543
|
+
const newDecision = this.repo.inTransaction(() => {
|
|
544
|
+
const created = this.repo.create(newTask);
|
|
545
|
+
|
|
546
|
+
this.repo.update(oldDecisionId, {
|
|
547
|
+
status: 'closed',
|
|
548
|
+
closeReason: `Superseded by ${created.id}: ${reason || 'Revised'}`,
|
|
549
|
+
closedAt: Math.floor(Date.now() / 1000),
|
|
550
|
+
updatedAt: Math.floor(Date.now() / 1000),
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
this.repo.addDependency(created.id, oldDecisionId, 'supersedes');
|
|
554
|
+
return created;
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
this._logEvent(oldDecisionId, 'superseded', 'pinned', `by ${newDecision.id}`);
|
|
558
|
+
this._logEvent(newDecision.id, 'supersedes', null, oldDecisionId);
|
|
559
|
+
|
|
560
|
+
return { newDecision, oldDecisionId };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* 取消固定决策
|
|
565
|
+
* @param {string} id
|
|
566
|
+
* @param {string} [reason='']
|
|
567
|
+
* @returns {Task}
|
|
568
|
+
*/
|
|
569
|
+
async unpinDecision(id, reason = '') {
|
|
570
|
+
const task = this.repo.findById(id);
|
|
571
|
+
if (!task) throw new Error(`Decision not found: ${id}`);
|
|
572
|
+
if (task.status !== 'pinned') {
|
|
573
|
+
throw new Error(`Can only unpin pinned decisions (current: ${task.status})`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const saved = this.repo.update(id, {
|
|
577
|
+
status: 'closed',
|
|
578
|
+
closeReason: reason || 'Unpinned',
|
|
579
|
+
closedAt: Math.floor(Date.now() / 1000),
|
|
580
|
+
updatedAt: Math.floor(Date.now() / 1000),
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
this._logEvent(id, 'unpinned', 'pinned', reason);
|
|
584
|
+
return saved;
|
|
368
585
|
}
|
|
369
586
|
|
|
370
587
|
// ═══ 私有方法 ═══════════════════════════════════════
|
|
371
588
|
|
|
589
|
+
/**
|
|
590
|
+
* P2: 获取决策过期阈值(秒)
|
|
591
|
+
* 默认 30 天 = 2592000 秒。可通过容器内 'config' 服务配置。
|
|
592
|
+
* 返回 0 表示禁用过期检测。
|
|
593
|
+
* @private
|
|
594
|
+
*/
|
|
595
|
+
_getDecisionStaleThreshold() {
|
|
596
|
+
try {
|
|
597
|
+
const config = this.repo?.db
|
|
598
|
+
? null // 通过构造函数传入的 config 优先
|
|
599
|
+
: null;
|
|
600
|
+
// 暂无 config 注入路径,使用环境变量或硬编码默认值
|
|
601
|
+
const envDays = process.env.ASD_DECISION_STALE_DAYS;
|
|
602
|
+
if (envDays !== undefined) {
|
|
603
|
+
const days = Number.parseInt(envDays, 10);
|
|
604
|
+
if (Number.isFinite(days) && days >= 0) return days * 86400;
|
|
605
|
+
}
|
|
606
|
+
return 30 * 86400; // 默认 30 天
|
|
607
|
+
} catch {
|
|
608
|
+
return 30 * 86400;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
372
612
|
/**
|
|
373
613
|
* 查找因 closedTaskId 完成而新解除阻塞的任务
|
|
374
614
|
* @private
|
package/package.json
CHANGED
|
@@ -46,7 +46,9 @@ Use this skill when the user's intent is unclear or overlaps multiple capabiliti
|
|
|
46
46
|
- Guard: `autosnippet_guard` (code 单文件 / files[] 批量)
|
|
47
47
|
- Bootstrap: `autosnippet_bootstrap` (no params — Mission Briefing) + `autosnippet_dimension_complete`
|
|
48
48
|
- Wiki: `autosnippet_wiki_plan` (topic planning) + `autosnippet_wiki_finalize` (meta.json + dedup)
|
|
49
|
-
-
|
|
49
|
+
- Ready: `autosnippet_ready` (session entry — loads decisions + tasks)
|
|
50
|
+
- Decide: `autosnippet_decide` (operation: record / revise / unpin / list)
|
|
51
|
+
- Task: `autosnippet_task` (operation: create / ready / claim / close / fail / defer / progress / stats / decompose)
|
|
50
52
|
- Skills: `autosnippet_skill` (operation: list / load / create / update / delete / suggest)
|
|
51
53
|
- Admin: `autosnippet_enrich_candidates`, `autosnippet_knowledge_lifecycle`, `autosnippet_validate_candidate`, `autosnippet_check_duplicate`
|
|
52
54
|
|
|
@@ -118,7 +118,9 @@ Recipe is the core knowledge unit. V3 uses a unified structured model:
|
|
|
118
118
|
|
|
119
119
|
| Tool | Description |
|
|
120
120
|
|------|-------------|
|
|
121
|
-
| `
|
|
121
|
+
| `autosnippet_ready` | Session entry point — loads decisions + ready tasks (call FIRST) |
|
|
122
|
+
| `autosnippet_decide` | Decision management (`operation`: record/revise/unpin/list) |
|
|
123
|
+
| `autosnippet_task` | Task CRUD (`operation`: create/ready/claim/close/fail/defer/progress/stats/decompose) |
|
|
122
124
|
|
|
123
125
|
### Skills Management
|
|
124
126
|
|