autosnippet 3.2.1 → 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 +245 -46
- 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/infrastructure/config/Paths.js +18 -8
- 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/claude-hooks.yaml +1 -0
- package/templates/copilot-instructions.md +48 -13
- package/templates/cursor-rules/autosnippet-conventions.mdc +11 -0
- package/templates/cursor-rules/autosnippet-workflow.mdc +16 -7
- package/templates/guard-ci.yml +22 -0
- package/templates/pre-commit-guard.sh +2 -1
- package/dashboard/dist/assets/index-BJiuaVPD.css +0 -1
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,27 @@ 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
|
+
this._ensureTaskGraphInstructions();
|
|
447
|
+
if (this._generateAgentInstructionFiles()) configured.push('agent-instructions');
|
|
448
|
+
if (this._copyCursorRules()) configured.push('cursor-rules');
|
|
449
|
+
if (this._copySkillsTemplate()) configured.push('skills-template');
|
|
450
|
+
if (this._copyCursorWorkflow()) configured.push('cursor-workflow');
|
|
451
|
+
if (this._copyClaudeHooks()) configured.push('claude-hooks');
|
|
452
|
+
if (this._copyGuardCI()) configured.push('guard-ci');
|
|
453
|
+
if (this._installPreCommitHook()) configured.push('pre-commit-hook');
|
|
441
454
|
// NOTE: .qoder/ .trae/ 不再自动创建,用户可通过 `asd mirror` 按需生成
|
|
442
455
|
|
|
443
456
|
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
457
|
if (extResult) {
|
|
454
458
|
configured.push(...extResult);
|
|
455
459
|
}
|
|
@@ -711,23 +715,79 @@ export class SetupService {
|
|
|
711
715
|
_copyCopilotInstructions() {
|
|
712
716
|
const src = join(REPO_ROOT, 'templates', 'copilot-instructions.md');
|
|
713
717
|
if (!existsSync(src)) {
|
|
714
|
-
return;
|
|
718
|
+
return false;
|
|
715
719
|
}
|
|
716
720
|
|
|
717
721
|
const destDir = join(this.projectRoot, '.github');
|
|
718
722
|
const dest = join(destDir, 'copilot-instructions.md');
|
|
719
723
|
if (existsSync(dest) && !this.force) {
|
|
720
|
-
return;
|
|
724
|
+
return false;
|
|
721
725
|
}
|
|
722
726
|
|
|
723
727
|
// 即使 --force,也不覆盖用户原有的非 AutoSnippet 文件
|
|
724
728
|
const { canWrite } = checkWriteSafety(dest);
|
|
725
729
|
if (!canWrite) {
|
|
726
|
-
return;
|
|
730
|
+
return false;
|
|
727
731
|
}
|
|
728
732
|
|
|
729
733
|
mkdirSync(destDir, { recursive: true });
|
|
730
734
|
copyFileSync(src, dest);
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** @private TaskGraph 决策记忆激活段落注入 */
|
|
739
|
+
_ensureTaskGraphInstructions() {
|
|
740
|
+
const githubDir = join(this.projectRoot, '.github');
|
|
741
|
+
const filePath = join(githubDir, 'copilot-instructions.md');
|
|
742
|
+
|
|
743
|
+
// 使用 HTML 注释标记界定段落,避免标题被编辑后重复追加(D3)
|
|
744
|
+
const BEGIN_MARKER = '<!-- autosnippet:begin -->';
|
|
745
|
+
const END_MARKER = '<!-- autosnippet:end -->';
|
|
746
|
+
|
|
747
|
+
const autosnippetSection = `
|
|
748
|
+
${BEGIN_MARKER}
|
|
749
|
+
## AutoSnippet
|
|
750
|
+
|
|
751
|
+
This project uses AutoSnippet for knowledge management and decision tracking.
|
|
752
|
+
In VS Code Agent Mode, type \`#asd\` before your message to activate project memory.
|
|
753
|
+
|
|
754
|
+
### ⚠️ MANDATORY — Call \`autosnippet_ready\` FIRST
|
|
755
|
+
|
|
756
|
+
Before doing ANY work, you MUST call \`autosnippet_ready()\` at the start of every conversation.
|
|
757
|
+
This loads **active team decisions** and **pending tasks** that you must respect.
|
|
758
|
+
Skipping this step will cause you to contradict team agreements.
|
|
759
|
+
|
|
760
|
+
\`\`\`
|
|
761
|
+
autosnippet_ready() → Load decisions + context (ALWAYS FIRST)
|
|
762
|
+
autosnippet_decide(record) → Save new team agreement
|
|
763
|
+
autosnippet_task(create/close) → Task lifecycle
|
|
764
|
+
\`\`\`
|
|
765
|
+
${END_MARKER}
|
|
766
|
+
`;
|
|
767
|
+
|
|
768
|
+
if (!existsSync(githubDir)) {
|
|
769
|
+
mkdirSync(githubDir, { recursive: true });
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (existsSync(filePath)) {
|
|
773
|
+
const existing = readFileSync(filePath, 'utf8');
|
|
774
|
+
// 通过 HTML 注释标记检测,不依赖可编辑的标题文本
|
|
775
|
+
if (existing.includes(BEGIN_MARKER)) {
|
|
776
|
+
// 替换现有段落(支持升级)
|
|
777
|
+
const updated = existing.replace(
|
|
778
|
+
new RegExp(`${BEGIN_MARKER}[\\s\\S]*?${END_MARKER}`),
|
|
779
|
+
autosnippetSection.trim()
|
|
780
|
+
);
|
|
781
|
+
writeFileSync(filePath, updated);
|
|
782
|
+
return { action: 'updated' };
|
|
783
|
+
}
|
|
784
|
+
// 追加到末尾
|
|
785
|
+
writeFileSync(filePath, existing + '\n' + autosnippetSection);
|
|
786
|
+
return { action: 'appended' };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
writeFileSync(filePath, autosnippetSection.trimStart());
|
|
790
|
+
return { action: 'created' };
|
|
731
791
|
}
|
|
732
792
|
|
|
733
793
|
/**
|
|
@@ -737,6 +797,7 @@ export class SetupService {
|
|
|
737
797
|
*/
|
|
738
798
|
_generateAgentInstructionFiles() {
|
|
739
799
|
const projectName = this.projectRoot.split('/').pop();
|
|
800
|
+
let wrote = false;
|
|
740
801
|
|
|
741
802
|
// AGENTS.md
|
|
742
803
|
const agentsPath = join(this.projectRoot, 'AGENTS.md');
|
|
@@ -746,31 +807,35 @@ export class SetupService {
|
|
|
746
807
|
const agentsContent = [
|
|
747
808
|
`# ${projectName} — Agent Instructions`,
|
|
748
809
|
'',
|
|
749
|
-
'> Auto-generated by AutoSnippet.
|
|
810
|
+
'> Auto-generated by AutoSnippet.',
|
|
750
811
|
'',
|
|
751
|
-
'##
|
|
812
|
+
'## AutoSnippet Integration',
|
|
752
813
|
'',
|
|
753
|
-
'This project uses **AutoSnippet** for knowledge management.',
|
|
754
|
-
'Run `asd bootstrap` to populate the knowledge base, then this file will be',
|
|
755
|
-
'regenerated with coding standards, patterns, and tool references.',
|
|
814
|
+
'This project uses **AutoSnippet** for knowledge management and decision tracking.',
|
|
756
815
|
'',
|
|
757
|
-
'
|
|
816
|
+
'### MCP Tools',
|
|
758
817
|
'',
|
|
759
|
-
'
|
|
760
|
-
'- `
|
|
761
|
-
'- `
|
|
762
|
-
'- `autosnippet_submit_knowledge` — Submit candidate (strict validation)',
|
|
818
|
+
'- `autosnippet_search` — Search knowledge',
|
|
819
|
+
'- `autosnippet_knowledge` — Browse/get recipes',
|
|
820
|
+
'- `autosnippet_submit_knowledge` — Submit candidate',
|
|
763
821
|
'- `autosnippet_guard` — Code compliance check',
|
|
764
822
|
'- `autosnippet_health` — Service health & KB stats',
|
|
823
|
+
'- `autosnippet_ready` — Session entry point (loads decisions + tasks)',
|
|
824
|
+
'- `autosnippet_decide` — Decision management (record/revise/unpin)',
|
|
825
|
+
'- `autosnippet_task` — Task CRUD (create/claim/close/fail/defer)',
|
|
765
826
|
'',
|
|
766
|
-
'
|
|
827
|
+
'### VS Code Agent Mode',
|
|
767
828
|
'',
|
|
768
|
-
'
|
|
769
|
-
'
|
|
770
|
-
'
|
|
829
|
+
'Type `#asd` before your message in Agent Mode to activate project memory.',
|
|
830
|
+
'',
|
|
831
|
+
'### Constraints',
|
|
832
|
+
'',
|
|
833
|
+
'1. Do NOT modify knowledge base files directly.',
|
|
834
|
+
'2. Create or update knowledge only through MCP tools.',
|
|
771
835
|
'',
|
|
772
836
|
].join('\n');
|
|
773
837
|
writeFileSync(agentsPath, agentsContent);
|
|
838
|
+
wrote = true;
|
|
774
839
|
} else {
|
|
775
840
|
}
|
|
776
841
|
}
|
|
@@ -783,64 +848,198 @@ export class SetupService {
|
|
|
783
848
|
const claudeContent = [
|
|
784
849
|
`# ${projectName} — Claude Code Instructions`,
|
|
785
850
|
'',
|
|
786
|
-
'> Auto-generated by AutoSnippet.
|
|
851
|
+
'> Auto-generated by AutoSnippet.',
|
|
852
|
+
'',
|
|
853
|
+
'## AutoSnippet Integration',
|
|
787
854
|
'',
|
|
788
|
-
'
|
|
855
|
+
'This project uses AutoSnippet for knowledge management and decision tracking.',
|
|
789
856
|
'',
|
|
790
|
-
'
|
|
791
|
-
'
|
|
792
|
-
'- `autosnippet_search` — Search knowledge
|
|
793
|
-
'- `autosnippet_knowledge` — Browse/get recipes
|
|
794
|
-
'- `autosnippet_submit_knowledge` — Submit candidate
|
|
857
|
+
'### MCP Tools',
|
|
858
|
+
'',
|
|
859
|
+
'- `autosnippet_search` — Search knowledge',
|
|
860
|
+
'- `autosnippet_knowledge` — Browse/get recipes',
|
|
861
|
+
'- `autosnippet_submit_knowledge` — Submit candidate',
|
|
795
862
|
'- `autosnippet_guard` — Code compliance check',
|
|
796
863
|
'- `autosnippet_health` — Service health & KB stats',
|
|
864
|
+
'- `autosnippet_ready` — Session entry point (loads decisions + tasks)',
|
|
865
|
+
'- `autosnippet_decide` — Decision management (record/revise/unpin)',
|
|
866
|
+
'- `autosnippet_task` — Task CRUD (create/claim/close/fail/defer)',
|
|
867
|
+
'',
|
|
868
|
+
'### Claude + VS Code',
|
|
869
|
+
'',
|
|
870
|
+
'Claude Code uses MCP tools directly. Run `asd server` to start the API server.',
|
|
871
|
+
'In VS Code Agent Mode, type `#asd` before your message to activate project memory.',
|
|
797
872
|
'',
|
|
798
|
-
'
|
|
873
|
+
'### Constraints',
|
|
799
874
|
'',
|
|
800
|
-
'1.
|
|
801
|
-
'2. Create or update knowledge
|
|
802
|
-
'3. **Prefer Recipes** as project standards; source code is supplementary.',
|
|
875
|
+
'1. Do NOT modify knowledge base files directly.',
|
|
876
|
+
'2. Create or update knowledge only through MCP tools.',
|
|
803
877
|
'',
|
|
804
878
|
].join('\n');
|
|
805
879
|
writeFileSync(claudePath, claudeContent);
|
|
880
|
+
wrote = true;
|
|
806
881
|
} else {
|
|
807
882
|
}
|
|
808
883
|
}
|
|
884
|
+
return wrote;
|
|
809
885
|
}
|
|
810
886
|
|
|
811
887
|
/** @private .cursor/rules/autosnippet-conventions.mdc */
|
|
812
888
|
_copyCursorRules() {
|
|
813
889
|
const src = join(REPO_ROOT, 'templates', 'cursor-rules', 'autosnippet-conventions.mdc');
|
|
814
890
|
if (!existsSync(src)) {
|
|
815
|
-
return;
|
|
891
|
+
return false;
|
|
816
892
|
}
|
|
817
893
|
|
|
818
894
|
const destDir = join(this.projectRoot, '.cursor', 'rules');
|
|
819
895
|
const dest = join(destDir, 'autosnippet-conventions.mdc');
|
|
820
896
|
if (existsSync(dest) && !this.force) {
|
|
821
|
-
return;
|
|
897
|
+
return false;
|
|
822
898
|
}
|
|
823
899
|
|
|
824
900
|
mkdirSync(destDir, { recursive: true });
|
|
825
901
|
copyFileSync(src, dest);
|
|
902
|
+
return true;
|
|
826
903
|
}
|
|
827
904
|
|
|
828
905
|
/** @private .cursor/rules/autosnippet-skills.mdc — Project Skills 索引模板 */
|
|
829
906
|
_copySkillsTemplate() {
|
|
830
907
|
const src = join(REPO_ROOT, 'templates', 'cursor-rules', 'autosnippet-skills.mdc');
|
|
831
908
|
if (!existsSync(src)) {
|
|
832
|
-
return;
|
|
909
|
+
return false;
|
|
833
910
|
}
|
|
834
911
|
|
|
835
912
|
const destDir = join(this.projectRoot, '.cursor', 'rules');
|
|
836
913
|
const dest = join(destDir, 'autosnippet-skills.mdc');
|
|
837
914
|
if (existsSync(dest) && !this.force) {
|
|
838
|
-
return;
|
|
915
|
+
return false;
|
|
839
916
|
}
|
|
840
917
|
|
|
841
918
|
mkdirSync(destDir, { recursive: true });
|
|
842
919
|
copyFileSync(src, dest);
|
|
920
|
+
return true;
|
|
843
921
|
}
|
|
922
|
+
/** @private .cursor/rules/autosnippet-workflow.mdc — TaskGraph & Guard 工作流 */
|
|
923
|
+
_copyCursorWorkflow() {
|
|
924
|
+
const src = join(REPO_ROOT, 'templates', 'cursor-rules', 'autosnippet-workflow.mdc');
|
|
925
|
+
if (!existsSync(src)) {
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const destDir = join(this.projectRoot, '.cursor', 'rules');
|
|
930
|
+
const dest = join(destDir, 'autosnippet-workflow.mdc');
|
|
931
|
+
if (existsSync(dest) && !this.force) {
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
mkdirSync(destDir, { recursive: true });
|
|
936
|
+
copyFileSync(src, dest);
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* @private .claude/hooks.yaml — Claude Code SessionStart hook
|
|
942
|
+
* 会话开始时自动 prime TaskGraph 上下文
|
|
943
|
+
*/
|
|
944
|
+
_copyClaudeHooks() {
|
|
945
|
+
const src = join(REPO_ROOT, 'templates', 'claude-hooks.yaml');
|
|
946
|
+
if (!existsSync(src)) {
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const claudeDir = join(this.projectRoot, '.claude');
|
|
951
|
+
const dest = join(claudeDir, 'hooks.yaml');
|
|
952
|
+
if (existsSync(dest) && !this.force) {
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// 即使 --force,也不覆盖用户自己的 hooks 文件
|
|
957
|
+
const { canWrite } = checkWriteSafety(dest);
|
|
958
|
+
if (!canWrite) {
|
|
959
|
+
return false;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
963
|
+
copyFileSync(src, dest);
|
|
964
|
+
return true;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* @private .github/workflows/autosnippet-guard.yml — Guard CI/CD workflow
|
|
969
|
+
* 在 push/PR 时自动运行 Guard 合规检查
|
|
970
|
+
*/
|
|
971
|
+
_copyGuardCI() {
|
|
972
|
+
const src = join(REPO_ROOT, 'templates', 'guard-ci.yml');
|
|
973
|
+
if (!existsSync(src)) {
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const destDir = join(this.projectRoot, '.github', 'workflows');
|
|
978
|
+
const dest = join(destDir, 'autosnippet-guard.yml');
|
|
979
|
+
if (existsSync(dest) && !this.force) {
|
|
980
|
+
return false;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const { canWrite } = checkWriteSafety(dest);
|
|
984
|
+
if (!canWrite) {
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
mkdirSync(destDir, { recursive: true });
|
|
989
|
+
copyFileSync(src, dest);
|
|
990
|
+
return true;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* @private .git/hooks/pre-commit — Guard pre-commit hook
|
|
995
|
+
* 如果项目使用 husky,则安装到 .husky/pre-commit
|
|
996
|
+
* 否则安装到 .git/hooks/pre-commit
|
|
997
|
+
* 已存在 pre-commit hook 时不覆盖(避免破坏用户现有 hook)
|
|
998
|
+
*/
|
|
999
|
+
_installPreCommitHook() {
|
|
1000
|
+
const src = join(REPO_ROOT, 'templates', 'pre-commit-guard.sh');
|
|
1001
|
+
if (!existsSync(src)) {
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// 优先检查 husky
|
|
1006
|
+
const huskyDir = join(this.projectRoot, '.husky');
|
|
1007
|
+
const gitHooksDir = join(this.projectRoot, '.git', 'hooks');
|
|
1008
|
+
|
|
1009
|
+
let dest;
|
|
1010
|
+
if (existsSync(huskyDir)) {
|
|
1011
|
+
// 项目使用 husky — 安装到 .husky/pre-commit
|
|
1012
|
+
dest = join(huskyDir, 'pre-commit');
|
|
1013
|
+
} else if (existsSync(join(this.projectRoot, '.git'))) {
|
|
1014
|
+
// 普通 git 项目 — 安装到 .git/hooks/pre-commit
|
|
1015
|
+
dest = join(gitHooksDir, 'pre-commit');
|
|
1016
|
+
mkdirSync(gitHooksDir, { recursive: true });
|
|
1017
|
+
} else {
|
|
1018
|
+
// 非 git 项目,跳过
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// 已存在 pre-commit hook 时不覆盖(可能是用户自己的 hook)
|
|
1023
|
+
if (existsSync(dest) && !this.force) {
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// 即使 --force,也不覆盖用户自己的 pre-commit hook
|
|
1028
|
+
const { canWrite } = checkWriteSafety(dest);
|
|
1029
|
+
if (!canWrite) {
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
copyFileSync(src, dest);
|
|
1034
|
+
// 确保可执行
|
|
1035
|
+
try {
|
|
1036
|
+
execSync(`chmod +x "${dest}"`, { stdio: 'pipe' });
|
|
1037
|
+
} catch {
|
|
1038
|
+
/* Windows 不需要 chmod */
|
|
1039
|
+
}
|
|
1040
|
+
return true;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
844
1043
|
/** @private 镜像 .cursor/rules/ 中的 autosnippet-* 文件到目标 IDE 目录(Qoder / Trae 兼容)
|
|
845
1044
|
* 只复制 autosnippet- 前缀的文件,不触碰用户自己创建的规则 */
|
|
846
1045
|
_mirrorCursorToIDE(targetDirName) {
|
|
@@ -203,10 +203,21 @@ export class KnowledgeEntry {
|
|
|
203
203
|
return [...regexRules, ...astRules];
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/* ═══ 系统标签 ═══════════════════════════════════ */
|
|
207
|
+
|
|
208
|
+
/** 系统内部标签前缀 — 内部元数据,不应暴露给最终用户 */
|
|
209
|
+
static SYSTEM_TAG_PREFIXES = ['dimension:', 'bootstrap:', 'internal:', 'system:'];
|
|
210
|
+
|
|
211
|
+
/** 判断是否为系统内部标签 */
|
|
212
|
+
static isSystemTag(tag) {
|
|
213
|
+
return KnowledgeEntry.SYSTEM_TAG_PREFIXES.some((p) => tag.startsWith(p));
|
|
214
|
+
}
|
|
215
|
+
|
|
206
216
|
/* ═══ 序列化 ═══════════════════════════════════════ */
|
|
207
217
|
|
|
208
218
|
/**
|
|
209
219
|
* Domain → JSON (camelCase 直出,全链路统一)
|
|
220
|
+
* 注意: tags 保留原始值(含系统标签),对外 API 使用 sanitizeForAPI() 过滤
|
|
210
221
|
*/
|
|
211
222
|
toJSON() {
|
|
212
223
|
return {
|
package/lib/domain/task/Task.js
CHANGED
|
@@ -60,12 +60,18 @@ export class Task {
|
|
|
60
60
|
if (this.status === 'closed') {
|
|
61
61
|
throw new Error('Cannot claim a closed task');
|
|
62
62
|
}
|
|
63
|
+
if (this.status === 'pinned') {
|
|
64
|
+
throw new Error('Cannot claim a pinned decision. Use unpin_decision or revise_decision instead.');
|
|
65
|
+
}
|
|
63
66
|
this.status = 'in_progress';
|
|
64
67
|
this.assignee = assignee;
|
|
65
68
|
this.updatedAt = Math.floor(Date.now() / 1000);
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
close(reason = 'Completed') {
|
|
72
|
+
if (this.status === 'pinned') {
|
|
73
|
+
throw new Error('Cannot close a pinned decision directly. Use unpin_decision or revise_decision instead.');
|
|
74
|
+
}
|
|
69
75
|
this.status = 'closed';
|
|
70
76
|
this.closeReason = reason;
|
|
71
77
|
this.closedAt = Math.floor(Date.now() / 1000);
|
|
@@ -79,7 +85,28 @@ export class Task {
|
|
|
79
85
|
this.updatedAt = Math.floor(Date.now() / 1000);
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
pin() {
|
|
89
|
+
if (this.status === 'closed') {
|
|
90
|
+
throw new Error('Cannot pin a closed task');
|
|
91
|
+
}
|
|
92
|
+
this.status = 'pinned';
|
|
93
|
+
this.updatedAt = Math.floor(Date.now() / 1000);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
unpin(reason = '') {
|
|
97
|
+
if (this.status !== 'pinned') {
|
|
98
|
+
throw new Error('Can only unpin a pinned task');
|
|
99
|
+
}
|
|
100
|
+
this.status = 'closed';
|
|
101
|
+
this.closeReason = reason || 'Unpinned by user';
|
|
102
|
+
this.closedAt = Math.floor(Date.now() / 1000);
|
|
103
|
+
this.updatedAt = this.closedAt;
|
|
104
|
+
}
|
|
105
|
+
|
|
82
106
|
defer(reason = '') {
|
|
107
|
+
if (this.status === 'pinned') {
|
|
108
|
+
throw new Error('Cannot defer a pinned decision. Use unpin_decision or revise_decision instead.');
|
|
109
|
+
}
|
|
83
110
|
this.status = 'deferred';
|
|
84
111
|
if (reason) {
|
|
85
112
|
this.notes = `[deferred] ${reason}`;
|
|
@@ -91,6 +118,9 @@ export class Task {
|
|
|
91
118
|
if (this.status === 'closed') {
|
|
92
119
|
throw new Error('Cannot fail a closed task');
|
|
93
120
|
}
|
|
121
|
+
if (this.status === 'pinned') {
|
|
122
|
+
throw new Error('Cannot fail a pinned decision. Use unpin_decision or revise_decision instead.');
|
|
123
|
+
}
|
|
94
124
|
this.status = 'open';
|
|
95
125
|
this.failCount += 1;
|
|
96
126
|
this.lastFailReason = reason || 'Unknown failure';
|
|
@@ -120,11 +150,11 @@ export class Task {
|
|
|
120
150
|
if (typeof this.priority !== 'number' || !Number.isInteger(this.priority) || this.priority < 0 || this.priority > 4) {
|
|
121
151
|
throw new Error(`priority must be an integer between 0 and 4 (got ${this.priority})`);
|
|
122
152
|
}
|
|
123
|
-
const validStatuses = ['open', 'in_progress', 'deferred', 'closed'];
|
|
153
|
+
const validStatuses = ['open', 'in_progress', 'deferred', 'closed', 'pinned'];
|
|
124
154
|
if (!validStatuses.includes(this.status)) {
|
|
125
155
|
throw new Error(`invalid status: ${this.status}`);
|
|
126
156
|
}
|
|
127
|
-
const validTypes = ['epic', 'task', 'bug', 'chore'];
|
|
157
|
+
const validTypes = ['epic', 'task', 'bug', 'chore', 'decision'];
|
|
128
158
|
if (!validTypes.includes(this.taskType)) {
|
|
129
159
|
throw new Error(`invalid task type: ${this.taskType}`);
|
|
130
160
|
}
|