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
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>AutoSnippet Dashboard</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-CRH5Umim.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/yaml-qRaU8Ldn.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-Ck-HBmg5.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/axios-C0Zqfgkc.js">
|
|
12
|
-
<link rel="modulepreload" crossorigin href="/assets/icons-
|
|
12
|
+
<link rel="modulepreload" crossorigin href="/assets/icons-18VxiaCT.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/framer-motion-DOATyqla.js">
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BJiuaVPD.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
package/lib/cli/SetupService.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Step 1 .autosnippet/ 运行时目录 + config.json + .gitignore
|
|
7
7
|
* Step 2 AutoSnippet/ 子仓库(核心数据 + 权限能力 + skills/)
|
|
8
|
-
* Step 3 IDE 集成(VSCode MCP + Cursor MCP + copilot-instructions + cursor-rules
|
|
8
|
+
* Step 3 IDE 集成(VSCode MCP + Cursor MCP + copilot-instructions + cursor-rules
|
|
9
|
+
* + skills-template + cursor-workflow + claude-hooks + guard-ci + pre-commit-hook)
|
|
9
10
|
* Step 4 SQLite 数据库 + V1 数据迁移
|
|
10
11
|
* Step 5 平台相关初始化(macOS → Xcode Snippets)
|
|
11
12
|
*
|
|
@@ -432,24 +433,26 @@ export class SetupService {
|
|
|
432
433
|
/* ═══ Step 3: IDE 集成 ═══════════════════════════════ */
|
|
433
434
|
|
|
434
435
|
stepIDE() {
|
|
436
|
+
const configured = [];
|
|
437
|
+
|
|
438
|
+
// MCP 配置(总是写入/合并,不会跳过)
|
|
435
439
|
this._configureVSCodeMCP();
|
|
440
|
+
configured.push('vscode-mcp');
|
|
436
441
|
this._configureCursorMCP();
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
this.
|
|
442
|
+
configured.push('cursor-mcp');
|
|
443
|
+
|
|
444
|
+
// 模板文件(可能因已存在/用户文件而跳过)
|
|
445
|
+
if (this._copyCopilotInstructions()) configured.push('copilot-instructions');
|
|
446
|
+
if (this._generateAgentInstructionFiles()) configured.push('agent-instructions');
|
|
447
|
+
if (this._copyCursorRules()) configured.push('cursor-rules');
|
|
448
|
+
if (this._copySkillsTemplate()) configured.push('skills-template');
|
|
449
|
+
if (this._copyCursorWorkflow()) configured.push('cursor-workflow');
|
|
450
|
+
if (this._copyClaudeHooks()) configured.push('claude-hooks');
|
|
451
|
+
if (this._copyGuardCI()) configured.push('guard-ci');
|
|
452
|
+
if (this._installPreCommitHook()) configured.push('pre-commit-hook');
|
|
441
453
|
// NOTE: .qoder/ .trae/ 不再自动创建,用户可通过 `asd mirror` 按需生成
|
|
442
454
|
|
|
443
455
|
const extResult = this._installVSCodeExtension();
|
|
444
|
-
|
|
445
|
-
const configured = [
|
|
446
|
-
'vscode-mcp',
|
|
447
|
-
'cursor-mcp',
|
|
448
|
-
'copilot-instructions',
|
|
449
|
-
'agent-instructions',
|
|
450
|
-
'cursor-rules',
|
|
451
|
-
'skills-template',
|
|
452
|
-
];
|
|
453
456
|
if (extResult) {
|
|
454
457
|
configured.push(...extResult);
|
|
455
458
|
}
|
|
@@ -711,23 +714,24 @@ export class SetupService {
|
|
|
711
714
|
_copyCopilotInstructions() {
|
|
712
715
|
const src = join(REPO_ROOT, 'templates', 'copilot-instructions.md');
|
|
713
716
|
if (!existsSync(src)) {
|
|
714
|
-
return;
|
|
717
|
+
return false;
|
|
715
718
|
}
|
|
716
719
|
|
|
717
720
|
const destDir = join(this.projectRoot, '.github');
|
|
718
721
|
const dest = join(destDir, 'copilot-instructions.md');
|
|
719
722
|
if (existsSync(dest) && !this.force) {
|
|
720
|
-
return;
|
|
723
|
+
return false;
|
|
721
724
|
}
|
|
722
725
|
|
|
723
726
|
// 即使 --force,也不覆盖用户原有的非 AutoSnippet 文件
|
|
724
727
|
const { canWrite } = checkWriteSafety(dest);
|
|
725
728
|
if (!canWrite) {
|
|
726
|
-
return;
|
|
729
|
+
return false;
|
|
727
730
|
}
|
|
728
731
|
|
|
729
732
|
mkdirSync(destDir, { recursive: true });
|
|
730
733
|
copyFileSync(src, dest);
|
|
734
|
+
return true;
|
|
731
735
|
}
|
|
732
736
|
|
|
733
737
|
/**
|
|
@@ -737,6 +741,7 @@ export class SetupService {
|
|
|
737
741
|
*/
|
|
738
742
|
_generateAgentInstructionFiles() {
|
|
739
743
|
const projectName = this.projectRoot.split('/').pop();
|
|
744
|
+
let wrote = false;
|
|
740
745
|
|
|
741
746
|
// AGENTS.md
|
|
742
747
|
const agentsPath = join(this.projectRoot, 'AGENTS.md');
|
|
@@ -771,6 +776,7 @@ export class SetupService {
|
|
|
771
776
|
'',
|
|
772
777
|
].join('\n');
|
|
773
778
|
writeFileSync(agentsPath, agentsContent);
|
|
779
|
+
wrote = true;
|
|
774
780
|
} else {
|
|
775
781
|
}
|
|
776
782
|
}
|
|
@@ -803,44 +809,169 @@ export class SetupService {
|
|
|
803
809
|
'',
|
|
804
810
|
].join('\n');
|
|
805
811
|
writeFileSync(claudePath, claudeContent);
|
|
812
|
+
wrote = true;
|
|
806
813
|
} else {
|
|
807
814
|
}
|
|
808
815
|
}
|
|
816
|
+
return wrote;
|
|
809
817
|
}
|
|
810
818
|
|
|
811
819
|
/** @private .cursor/rules/autosnippet-conventions.mdc */
|
|
812
820
|
_copyCursorRules() {
|
|
813
821
|
const src = join(REPO_ROOT, 'templates', 'cursor-rules', 'autosnippet-conventions.mdc');
|
|
814
822
|
if (!existsSync(src)) {
|
|
815
|
-
return;
|
|
823
|
+
return false;
|
|
816
824
|
}
|
|
817
825
|
|
|
818
826
|
const destDir = join(this.projectRoot, '.cursor', 'rules');
|
|
819
827
|
const dest = join(destDir, 'autosnippet-conventions.mdc');
|
|
820
828
|
if (existsSync(dest) && !this.force) {
|
|
821
|
-
return;
|
|
829
|
+
return false;
|
|
822
830
|
}
|
|
823
831
|
|
|
824
832
|
mkdirSync(destDir, { recursive: true });
|
|
825
833
|
copyFileSync(src, dest);
|
|
834
|
+
return true;
|
|
826
835
|
}
|
|
827
836
|
|
|
828
837
|
/** @private .cursor/rules/autosnippet-skills.mdc — Project Skills 索引模板 */
|
|
829
838
|
_copySkillsTemplate() {
|
|
830
839
|
const src = join(REPO_ROOT, 'templates', 'cursor-rules', 'autosnippet-skills.mdc');
|
|
831
840
|
if (!existsSync(src)) {
|
|
832
|
-
return;
|
|
841
|
+
return false;
|
|
833
842
|
}
|
|
834
843
|
|
|
835
844
|
const destDir = join(this.projectRoot, '.cursor', 'rules');
|
|
836
845
|
const dest = join(destDir, 'autosnippet-skills.mdc');
|
|
837
846
|
if (existsSync(dest) && !this.force) {
|
|
838
|
-
return;
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
mkdirSync(destDir, { recursive: true });
|
|
851
|
+
copyFileSync(src, dest);
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
/** @private .cursor/rules/autosnippet-workflow.mdc — TaskGraph & Guard 工作流 */
|
|
855
|
+
_copyCursorWorkflow() {
|
|
856
|
+
const src = join(REPO_ROOT, 'templates', 'cursor-rules', 'autosnippet-workflow.mdc');
|
|
857
|
+
if (!existsSync(src)) {
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const destDir = join(this.projectRoot, '.cursor', 'rules');
|
|
862
|
+
const dest = join(destDir, 'autosnippet-workflow.mdc');
|
|
863
|
+
if (existsSync(dest) && !this.force) {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
mkdirSync(destDir, { recursive: true });
|
|
868
|
+
copyFileSync(src, dest);
|
|
869
|
+
return true;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* @private .claude/hooks.yaml — Claude Code SessionStart hook
|
|
874
|
+
* 会话开始时自动 prime TaskGraph 上下文
|
|
875
|
+
*/
|
|
876
|
+
_copyClaudeHooks() {
|
|
877
|
+
const src = join(REPO_ROOT, 'templates', 'claude-hooks.yaml');
|
|
878
|
+
if (!existsSync(src)) {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const claudeDir = join(this.projectRoot, '.claude');
|
|
883
|
+
const dest = join(claudeDir, 'hooks.yaml');
|
|
884
|
+
if (existsSync(dest) && !this.force) {
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// 即使 --force,也不覆盖用户自己的 hooks 文件
|
|
889
|
+
const { canWrite } = checkWriteSafety(dest);
|
|
890
|
+
if (!canWrite) {
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
895
|
+
copyFileSync(src, dest);
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* @private .github/workflows/autosnippet-guard.yml — Guard CI/CD workflow
|
|
901
|
+
* 在 push/PR 时自动运行 Guard 合规检查
|
|
902
|
+
*/
|
|
903
|
+
_copyGuardCI() {
|
|
904
|
+
const src = join(REPO_ROOT, 'templates', 'guard-ci.yml');
|
|
905
|
+
if (!existsSync(src)) {
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const destDir = join(this.projectRoot, '.github', 'workflows');
|
|
910
|
+
const dest = join(destDir, 'autosnippet-guard.yml');
|
|
911
|
+
if (existsSync(dest) && !this.force) {
|
|
912
|
+
return false;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const { canWrite } = checkWriteSafety(dest);
|
|
916
|
+
if (!canWrite) {
|
|
917
|
+
return false;
|
|
839
918
|
}
|
|
840
919
|
|
|
841
920
|
mkdirSync(destDir, { recursive: true });
|
|
842
921
|
copyFileSync(src, dest);
|
|
922
|
+
return true;
|
|
843
923
|
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* @private .git/hooks/pre-commit — Guard pre-commit hook
|
|
927
|
+
* 如果项目使用 husky,则安装到 .husky/pre-commit
|
|
928
|
+
* 否则安装到 .git/hooks/pre-commit
|
|
929
|
+
* 已存在 pre-commit hook 时不覆盖(避免破坏用户现有 hook)
|
|
930
|
+
*/
|
|
931
|
+
_installPreCommitHook() {
|
|
932
|
+
const src = join(REPO_ROOT, 'templates', 'pre-commit-guard.sh');
|
|
933
|
+
if (!existsSync(src)) {
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// 优先检查 husky
|
|
938
|
+
const huskyDir = join(this.projectRoot, '.husky');
|
|
939
|
+
const gitHooksDir = join(this.projectRoot, '.git', 'hooks');
|
|
940
|
+
|
|
941
|
+
let dest;
|
|
942
|
+
if (existsSync(huskyDir)) {
|
|
943
|
+
// 项目使用 husky — 安装到 .husky/pre-commit
|
|
944
|
+
dest = join(huskyDir, 'pre-commit');
|
|
945
|
+
} else if (existsSync(join(this.projectRoot, '.git'))) {
|
|
946
|
+
// 普通 git 项目 — 安装到 .git/hooks/pre-commit
|
|
947
|
+
dest = join(gitHooksDir, 'pre-commit');
|
|
948
|
+
mkdirSync(gitHooksDir, { recursive: true });
|
|
949
|
+
} else {
|
|
950
|
+
// 非 git 项目,跳过
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// 已存在 pre-commit hook 时不覆盖(可能是用户自己的 hook)
|
|
955
|
+
if (existsSync(dest) && !this.force) {
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// 即使 --force,也不覆盖用户自己的 pre-commit hook
|
|
960
|
+
const { canWrite } = checkWriteSafety(dest);
|
|
961
|
+
if (!canWrite) {
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
copyFileSync(src, dest);
|
|
966
|
+
// 确保可执行
|
|
967
|
+
try {
|
|
968
|
+
execSync(`chmod +x "${dest}"`, { stdio: 'pipe' });
|
|
969
|
+
} catch {
|
|
970
|
+
/* Windows 不需要 chmod */
|
|
971
|
+
}
|
|
972
|
+
return true;
|
|
973
|
+
}
|
|
974
|
+
|
|
844
975
|
/** @private 镜像 .cursor/rules/ 中的 autosnippet-* 文件到目标 IDE 目录(Qoder / Trae 兼容)
|
|
845
976
|
* 只复制 autosnippet- 前缀的文件,不触碰用户自己创建的规则 */
|
|
846
977
|
_mirrorCursorToIDE(targetDirName) {
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Task — 任务实体
|
|
5
|
+
*
|
|
6
|
+
* 参考 Beads Issue 结构,裁剪为 AutoSnippet 所需的核心字段。
|
|
7
|
+
*
|
|
8
|
+
* 字段分组:
|
|
9
|
+
* Core Identification → id, parentId, contentHash
|
|
10
|
+
* Content → title, description, design, acceptance
|
|
11
|
+
* Status & Workflow → status, priority, taskType
|
|
12
|
+
* Assignment → assignee, claimedAt
|
|
13
|
+
* Timestamps → createdAt, updatedAt, closedAt
|
|
14
|
+
* Knowledge Bridge → knowledgeContext (运行时,不持久化)
|
|
15
|
+
*/
|
|
16
|
+
export class Task {
|
|
17
|
+
constructor(props = {}) {
|
|
18
|
+
// ── Core Identification ──
|
|
19
|
+
this.id = props.id || null;
|
|
20
|
+
this.parentId = props.parentId || null;
|
|
21
|
+
this.childSeq = props.childSeq || 0;
|
|
22
|
+
|
|
23
|
+
// ── Content ──
|
|
24
|
+
this.title = props.title || '';
|
|
25
|
+
this.description = props.description || '';
|
|
26
|
+
this.design = props.design || '';
|
|
27
|
+
this.acceptance = props.acceptance || '';
|
|
28
|
+
this.notes = props.notes || '';
|
|
29
|
+
|
|
30
|
+
// ── Status & Workflow ──
|
|
31
|
+
this.status = props.status || 'open';
|
|
32
|
+
this.priority = props.priority ?? 2;
|
|
33
|
+
this.taskType = props.taskType || 'task';
|
|
34
|
+
this.closeReason = props.closeReason || '';
|
|
35
|
+
this.failCount = props.failCount || 0;
|
|
36
|
+
this.lastFailReason = props.lastFailReason || '';
|
|
37
|
+
|
|
38
|
+
// ── Content Hash (去重) ──
|
|
39
|
+
this.contentHash = props.contentHash || null;
|
|
40
|
+
|
|
41
|
+
// ── Assignment ──
|
|
42
|
+
this.assignee = props.assignee || '';
|
|
43
|
+
this.createdBy = props.createdBy || 'agent';
|
|
44
|
+
|
|
45
|
+
// ── Timestamps ──
|
|
46
|
+
this.createdAt = props.createdAt || Math.floor(Date.now() / 1000);
|
|
47
|
+
this.updatedAt = props.updatedAt || Math.floor(Date.now() / 1000);
|
|
48
|
+
this.closedAt = props.closedAt || null;
|
|
49
|
+
|
|
50
|
+
// ── Knowledge Bridge (运行时填充, 不持久化) ──
|
|
51
|
+
this.knowledgeContext = props.knowledgeContext || null;
|
|
52
|
+
|
|
53
|
+
// ── Metadata (可扩展 JSON) ──
|
|
54
|
+
this.metadata = props.metadata || {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── 生命周期方法 ──
|
|
58
|
+
|
|
59
|
+
claim(assignee = 'agent') {
|
|
60
|
+
if (this.status === 'closed') {
|
|
61
|
+
throw new Error('Cannot claim a closed task');
|
|
62
|
+
}
|
|
63
|
+
this.status = 'in_progress';
|
|
64
|
+
this.assignee = assignee;
|
|
65
|
+
this.updatedAt = Math.floor(Date.now() / 1000);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
close(reason = 'Completed') {
|
|
69
|
+
this.status = 'closed';
|
|
70
|
+
this.closeReason = reason;
|
|
71
|
+
this.closedAt = Math.floor(Date.now() / 1000);
|
|
72
|
+
this.updatedAt = this.closedAt;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
reopen() {
|
|
76
|
+
this.status = 'open';
|
|
77
|
+
this.closedAt = null;
|
|
78
|
+
this.closeReason = '';
|
|
79
|
+
this.updatedAt = Math.floor(Date.now() / 1000);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
defer(reason = '') {
|
|
83
|
+
this.status = 'deferred';
|
|
84
|
+
if (reason) {
|
|
85
|
+
this.notes = `[deferred] ${reason}`;
|
|
86
|
+
}
|
|
87
|
+
this.updatedAt = Math.floor(Date.now() / 1000);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fail(reason) {
|
|
91
|
+
if (this.status === 'closed') {
|
|
92
|
+
throw new Error('Cannot fail a closed task');
|
|
93
|
+
}
|
|
94
|
+
this.status = 'open';
|
|
95
|
+
this.failCount += 1;
|
|
96
|
+
this.lastFailReason = reason || 'Unknown failure';
|
|
97
|
+
this.assignee = '';
|
|
98
|
+
this.updatedAt = Math.floor(Date.now() / 1000);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 计算内容哈希(用于去重检测)
|
|
103
|
+
* 相同标题+描述+类型 = 重复任务
|
|
104
|
+
*/
|
|
105
|
+
computeContentHash() {
|
|
106
|
+
const content = `${this.title}|${this.description}|${this.taskType}`;
|
|
107
|
+
this.contentHash = createHash('sha256').update(content).digest('hex').substring(0, 12);
|
|
108
|
+
return this.contentHash;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── 校验 ──
|
|
112
|
+
|
|
113
|
+
validate() {
|
|
114
|
+
if (!this.title || this.title.length === 0) {
|
|
115
|
+
throw new Error('title is required');
|
|
116
|
+
}
|
|
117
|
+
if (this.title.length > 500) {
|
|
118
|
+
throw new Error(`title must be 500 characters or less (got ${this.title.length})`);
|
|
119
|
+
}
|
|
120
|
+
if (typeof this.priority !== 'number' || !Number.isInteger(this.priority) || this.priority < 0 || this.priority > 4) {
|
|
121
|
+
throw new Error(`priority must be an integer between 0 and 4 (got ${this.priority})`);
|
|
122
|
+
}
|
|
123
|
+
const validStatuses = ['open', 'in_progress', 'deferred', 'closed'];
|
|
124
|
+
if (!validStatuses.includes(this.status)) {
|
|
125
|
+
throw new Error(`invalid status: ${this.status}`);
|
|
126
|
+
}
|
|
127
|
+
const validTypes = ['epic', 'task', 'bug', 'chore'];
|
|
128
|
+
if (!validTypes.includes(this.taskType)) {
|
|
129
|
+
throw new Error(`invalid task type: ${this.taskType}`);
|
|
130
|
+
}
|
|
131
|
+
if (this.status === 'closed' && !this.closedAt) {
|
|
132
|
+
throw new Error('closed tasks must have closedAt timestamp');
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 是否有效(快捷校验)
|
|
139
|
+
*/
|
|
140
|
+
isValid() {
|
|
141
|
+
try {
|
|
142
|
+
this.validate();
|
|
143
|
+
return true;
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
toJSON() {
|
|
150
|
+
return {
|
|
151
|
+
id: this.id,
|
|
152
|
+
parentId: this.parentId,
|
|
153
|
+
title: this.title,
|
|
154
|
+
description: this.description,
|
|
155
|
+
design: this.design,
|
|
156
|
+
acceptance: this.acceptance,
|
|
157
|
+
notes: this.notes,
|
|
158
|
+
status: this.status,
|
|
159
|
+
priority: this.priority,
|
|
160
|
+
taskType: this.taskType,
|
|
161
|
+
closeReason: this.closeReason,
|
|
162
|
+
assignee: this.assignee,
|
|
163
|
+
createdBy: this.createdBy,
|
|
164
|
+
createdAt: this.createdAt,
|
|
165
|
+
updatedAt: this.updatedAt,
|
|
166
|
+
closedAt: this.closedAt,
|
|
167
|
+
contentHash: this.contentHash,
|
|
168
|
+
failCount: this.failCount,
|
|
169
|
+
lastFailReason: this.lastFailReason,
|
|
170
|
+
metadata: this.metadata,
|
|
171
|
+
...(this.knowledgeContext ? { knowledgeContext: this.knowledgeContext } : {}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
static fromJSON(data) {
|
|
176
|
+
if (!data) return new Task();
|
|
177
|
+
return new Task(data);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 从数据库行构造 Task(snake_case → camelCase)
|
|
182
|
+
*/
|
|
183
|
+
static fromRow(row) {
|
|
184
|
+
if (!row) return null;
|
|
185
|
+
return new Task({
|
|
186
|
+
id: row.id,
|
|
187
|
+
parentId: row.parent_id,
|
|
188
|
+
childSeq: row.child_seq,
|
|
189
|
+
title: row.title,
|
|
190
|
+
description: row.description,
|
|
191
|
+
design: row.design,
|
|
192
|
+
acceptance: row.acceptance,
|
|
193
|
+
notes: row.notes,
|
|
194
|
+
status: row.status,
|
|
195
|
+
priority: row.priority,
|
|
196
|
+
taskType: row.task_type,
|
|
197
|
+
closeReason: row.close_reason,
|
|
198
|
+
contentHash: row.content_hash,
|
|
199
|
+
failCount: row.fail_count,
|
|
200
|
+
lastFailReason: row.last_fail_reason,
|
|
201
|
+
assignee: row.assignee,
|
|
202
|
+
createdBy: row.created_by,
|
|
203
|
+
createdAt: row.created_at,
|
|
204
|
+
updatedAt: row.updated_at,
|
|
205
|
+
closedAt: row.closed_at,
|
|
206
|
+
metadata: (() => {
|
|
207
|
+
if (typeof row.metadata !== 'string') return row.metadata || {};
|
|
208
|
+
try { return JSON.parse(row.metadata); } catch { return {}; }
|
|
209
|
+
})(),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export default Task;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 任务依赖类型
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 引入 6 种核心依赖类型,按 "是否影响就绪" 分为两组。
|
|
5
|
+
*
|
|
6
|
+
* 阻塞型: blocks, waits-for → 影响 ready 计算
|
|
7
|
+
* 结构型: parent-child → 仅层次关系
|
|
8
|
+
* 关联型: discovered-from, related, knowledge-ref → 不影响 ready
|
|
9
|
+
*/
|
|
10
|
+
export const DepType = Object.freeze({
|
|
11
|
+
// ── 阻塞型(影响 ready 计算)──
|
|
12
|
+
BLOCKS: 'blocks',
|
|
13
|
+
WAITS_FOR: 'waits-for',
|
|
14
|
+
|
|
15
|
+
// ── 结构型(建立层次,不影响 ready)──
|
|
16
|
+
PARENT_CHILD: 'parent-child',
|
|
17
|
+
|
|
18
|
+
// ── 关联型(不影响 ready,构建知识/因果图谱)──
|
|
19
|
+
DISCOVERED_FROM: 'discovered-from',
|
|
20
|
+
RELATED: 'related',
|
|
21
|
+
KNOWLEDGE_REF: 'knowledge-ref', // AutoSnippet 独有:关联知识条目
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/** 所有合法的依赖类型值列表 */
|
|
25
|
+
const ALL_DEP_TYPES = Object.values(DepType);
|
|
26
|
+
|
|
27
|
+
/** 影响就绪计算的依赖类型集合 */
|
|
28
|
+
const BLOCKING_TYPES = new Set([DepType.BLOCKS, DepType.WAITS_FOR]);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 判断依赖类型是否影响就绪计算
|
|
32
|
+
* @param {string} depType
|
|
33
|
+
* @returns {boolean}
|
|
34
|
+
*/
|
|
35
|
+
export function affectsReadyWork(depType) {
|
|
36
|
+
return BLOCKING_TYPES.has(depType);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 判断依赖类型是否合法
|
|
41
|
+
* @param {string} depType
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
export function isValidDepType(depType) {
|
|
45
|
+
return ALL_DEP_TYPES.includes(depType);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default DepType;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TaskIdGenerator — 短 Hash ID 生成器
|
|
6
|
+
*
|
|
7
|
+
* 渐进式长度扩展(随任务数增长自动加长):
|
|
8
|
+
* 0-500 tasks → 4 字符 (16^4 = 65,536 组合)
|
|
9
|
+
* 500-1500 → 5 字符 (16^5 = 1,048,576)
|
|
10
|
+
* 1500+ → 6 字符 (16^6 = 16,777,216)
|
|
11
|
+
*
|
|
12
|
+
* 前缀 'asd-',与 Beads 的 'bd-' 区分。
|
|
13
|
+
*/
|
|
14
|
+
export class TaskIdGenerator {
|
|
15
|
+
/**
|
|
16
|
+
* @param {import('better-sqlite3').Database} db — raw SQLite handle
|
|
17
|
+
*/
|
|
18
|
+
constructor(db) {
|
|
19
|
+
this._db = db;
|
|
20
|
+
this._prefix = 'asd';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 生成新的短 Hash ID
|
|
25
|
+
* @returns {string} 如 'asd-a1b2'
|
|
26
|
+
*/
|
|
27
|
+
generate() {
|
|
28
|
+
const taskCount = this._getTaskCount();
|
|
29
|
+
const hashLen = taskCount < 500 ? 4 : taskCount < 1500 ? 5 : 6;
|
|
30
|
+
|
|
31
|
+
// 尝试生成无冲突 ID(最多 10 次)
|
|
32
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
33
|
+
const uuid = uuidv4();
|
|
34
|
+
const hash = createHash('sha256').update(uuid).digest('hex');
|
|
35
|
+
const shortHash = hash.substring(0, hashLen);
|
|
36
|
+
const id = `${this._prefix}-${shortHash}`;
|
|
37
|
+
|
|
38
|
+
if (!this._exists(id)) {
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 回退到 6 位 + 冲突检查 + 终极 8 位兜底
|
|
44
|
+
const uuid = uuidv4();
|
|
45
|
+
const hash = createHash('sha256').update(uuid).digest('hex');
|
|
46
|
+
const fallbackId = `${this._prefix}-${hash.substring(0, 6)}`;
|
|
47
|
+
if (!this._exists(fallbackId)) return fallbackId;
|
|
48
|
+
return `${this._prefix}-${hash.substring(0, 8)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 生成子任务 ID
|
|
53
|
+
* asd-a3f8 → asd-a3f8.1, asd-a3f8.2, ...
|
|
54
|
+
* @param {string} parentId
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
generateChild(parentId) {
|
|
58
|
+
const parent = this._db.prepare('SELECT child_seq FROM tasks WHERE id = ?').get(parentId);
|
|
59
|
+
|
|
60
|
+
if (!parent) {
|
|
61
|
+
throw new Error(`Parent task not found: ${parentId}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const nextSeq = (parent.child_seq || 0) + 1;
|
|
65
|
+
this._db.prepare('UPDATE tasks SET child_seq = ? WHERE id = ?').run(nextSeq, parentId);
|
|
66
|
+
|
|
67
|
+
return `${parentId}.${nextSeq}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @private */
|
|
71
|
+
_getTaskCount() {
|
|
72
|
+
const row = this._db.prepare('SELECT COUNT(*) as cnt FROM tasks').get();
|
|
73
|
+
return row?.cnt || 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @private */
|
|
77
|
+
_exists(id) {
|
|
78
|
+
const row = this._db.prepare('SELECT 1 FROM tasks WHERE id = ?').get(id);
|
|
79
|
+
return !!row;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default TaskIdGenerator;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Model Context Protocol (stdio transport)
|
|
5
5
|
* 提供给 IDE AI Agent (Cursor/VSCode Copilot) 的工具集
|
|
6
6
|
*
|
|
7
|
-
* V3.1 整合:39 →
|
|
7
|
+
* V3.1 整合:39 → 20 工具(16 agent + 4 admin)
|
|
8
8
|
* 通过 ASD_MCP_TIER 环境变量控制可见工具集(agent/admin)
|
|
9
9
|
*
|
|
10
10
|
* 冷启动双路径:
|
|
@@ -38,8 +38,7 @@ import * as systemHandlers from './handlers/system.js';
|
|
|
38
38
|
// ─── External Agent Bootstrap 新 handler ──────────────────────
|
|
39
39
|
|
|
40
40
|
import { bootstrapExternal } from './handlers/bootstrap-external.js';
|
|
41
|
-
import { dimensionComplete } from './handlers/dimension-complete-external.js';
|
|
42
|
-
import { wikiFinalize, wikiPlan } from './handlers/wiki-external.js';
|
|
41
|
+
import { dimensionComplete } from './handlers/dimension-complete-external.js';import { taskHandler } from './handlers/task.js';import { wikiFinalize, wikiPlan } from './handlers/wiki-external.js';
|
|
43
42
|
|
|
44
43
|
// ─── McpServer 类 ─────────────────────────────────────────────
|
|
45
44
|
|
|
@@ -180,7 +179,7 @@ export class McpServer {
|
|
|
180
179
|
*/
|
|
181
180
|
_resolveHandler(name) {
|
|
182
181
|
const HANDLER_MAP = {
|
|
183
|
-
// ── Agent 层 (
|
|
182
|
+
// ── Agent 层 (16) ──
|
|
184
183
|
autosnippet_health: (ctx) => systemHandlers.health(ctx),
|
|
185
184
|
autosnippet_capabilities: () => systemHandlers.capabilities(),
|
|
186
185
|
autosnippet_search: (ctx, args) => consolidated.consolidatedSearch(ctx, args),
|
|
@@ -193,6 +192,7 @@ export class McpServer {
|
|
|
193
192
|
knowledgeHandlers.submitKnowledgeBatch(ctx, args),
|
|
194
193
|
autosnippet_save_document: (ctx, args) => knowledgeHandlers.saveDocument(ctx, args),
|
|
195
194
|
autosnippet_skill: (ctx, args) => consolidated.consolidatedSkill(ctx, args),
|
|
195
|
+
autosnippet_task: (ctx, args) => taskHandler(ctx, args),
|
|
196
196
|
// ── External Agent Bootstrap (v3.1) ──
|
|
197
197
|
autosnippet_bootstrap: (ctx, _args) => bootstrapExternal(ctx),
|
|
198
198
|
autosnippet_dimension_complete: (ctx, args) => dimensionComplete(ctx, args),
|