specdo 1.0.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/CHANGELOG.md +139 -0
- package/README.md +308 -0
- package/README.zh-CN.md +306 -0
- package/bin/specdo.js +3 -0
- package/dist/cli/index.d.ts +15 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +297 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/commands/_shared.d.ts +45 -0
- package/dist/commands/_shared.d.ts.map +1 -0
- package/dist/commands/_shared.js +124 -0
- package/dist/commands/_shared.js.map +1 -0
- package/dist/commands/apply.d.ts +30 -0
- package/dist/commands/apply.d.ts.map +1 -0
- package/dist/commands/apply.js +393 -0
- package/dist/commands/apply.js.map +1 -0
- package/dist/commands/archive.d.ts +25 -0
- package/dist/commands/archive.d.ts.map +1 -0
- package/dist/commands/archive.js +362 -0
- package/dist/commands/archive.js.map +1 -0
- package/dist/commands/doctor.d.ts +21 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +180 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/domains.d.ts +14 -0
- package/dist/commands/domains.d.ts.map +1 -0
- package/dist/commands/domains.js +107 -0
- package/dist/commands/domains.js.map +1 -0
- package/dist/commands/explore.d.ts +48 -0
- package/dist/commands/explore.d.ts.map +1 -0
- package/dist/commands/explore.js +378 -0
- package/dist/commands/explore.js.map +1 -0
- package/dist/commands/init.d.ts +45 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +243 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/list.d.ts +23 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +135 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/propose.d.ts +22 -0
- package/dist/commands/propose.d.ts.map +1 -0
- package/dist/commands/propose.js +316 -0
- package/dist/commands/propose.js.map +1 -0
- package/dist/commands/show.d.ts +15 -0
- package/dist/commands/show.d.ts.map +1 -0
- package/dist/commands/show.js +214 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/commands/status.d.ts +17 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +146 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/sync.d.ts +21 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +113 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/validate.d.ts +117 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +446 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/core/apply-brief-renderer.d.ts +35 -0
- package/dist/core/apply-brief-renderer.d.ts.map +1 -0
- package/dist/core/apply-brief-renderer.js +242 -0
- package/dist/core/apply-brief-renderer.js.map +1 -0
- package/dist/core/config-store.d.ts +190 -0
- package/dist/core/config-store.d.ts.map +1 -0
- package/dist/core/config-store.js +280 -0
- package/dist/core/config-store.js.map +1 -0
- package/dist/core/context-store.d.ts +96 -0
- package/dist/core/context-store.d.ts.map +1 -0
- package/dist/core/context-store.js +426 -0
- package/dist/core/context-store.js.map +1 -0
- package/dist/core/json-schemas.d.ts +349 -0
- package/dist/core/json-schemas.d.ts.map +1 -0
- package/dist/core/json-schemas.js +125 -0
- package/dist/core/json-schemas.js.map +1 -0
- package/dist/core/skill-content/cross-domain.d.ts +12 -0
- package/dist/core/skill-content/cross-domain.d.ts.map +1 -0
- package/dist/core/skill-content/cross-domain.js +291 -0
- package/dist/core/skill-content/cross-domain.js.map +1 -0
- package/dist/core/skill-content/protocol-examples.d.ts +13 -0
- package/dist/core/skill-content/protocol-examples.d.ts.map +1 -0
- package/dist/core/skill-content/protocol-examples.js +190 -0
- package/dist/core/skill-content/protocol-examples.js.map +1 -0
- package/dist/core/skill-content/workflow-content.d.ts +25 -0
- package/dist/core/skill-content/workflow-content.d.ts.map +1 -0
- package/dist/core/skill-content/workflow-content.js +1572 -0
- package/dist/core/skill-content/workflow-content.js.map +1 -0
- package/dist/core/skill-exporter.d.ts +186 -0
- package/dist/core/skill-exporter.d.ts.map +1 -0
- package/dist/core/skill-exporter.js +922 -0
- package/dist/core/skill-exporter.js.map +1 -0
- package/dist/core/spec-sync.d.ts +65 -0
- package/dist/core/spec-sync.d.ts.map +1 -0
- package/dist/core/spec-sync.js +226 -0
- package/dist/core/spec-sync.js.map +1 -0
- package/dist/core/task-parser.d.ts +58 -0
- package/dist/core/task-parser.d.ts.map +1 -0
- package/dist/core/task-parser.js +244 -0
- package/dist/core/task-parser.js.map +1 -0
- package/dist/core/template-renderer.d.ts +51 -0
- package/dist/core/template-renderer.d.ts.map +1 -0
- package/dist/core/template-renderer.js +362 -0
- package/dist/core/template-renderer.js.map +1 -0
- package/dist/domains/architecture.d.ts +34 -0
- package/dist/domains/architecture.d.ts.map +1 -0
- package/dist/domains/architecture.js +341 -0
- package/dist/domains/architecture.js.map +1 -0
- package/dist/domains/backend.d.ts +35 -0
- package/dist/domains/backend.d.ts.map +1 -0
- package/dist/domains/backend.js +367 -0
- package/dist/domains/backend.js.map +1 -0
- package/dist/domains/frontend.d.ts +36 -0
- package/dist/domains/frontend.d.ts.map +1 -0
- package/dist/domains/frontend.js +373 -0
- package/dist/domains/frontend.js.map +1 -0
- package/dist/domains/index.d.ts +49 -0
- package/dist/domains/index.d.ts.map +1 -0
- package/dist/domains/index.js +255 -0
- package/dist/domains/index.js.map +1 -0
- package/dist/domains/operations.d.ts +37 -0
- package/dist/domains/operations.d.ts.map +1 -0
- package/dist/domains/operations.js +344 -0
- package/dist/domains/operations.js.map +1 -0
- package/dist/domains/pool-ranking.d.ts +43 -0
- package/dist/domains/pool-ranking.d.ts.map +1 -0
- package/dist/domains/pool-ranking.js +153 -0
- package/dist/domains/pool-ranking.js.map +1 -0
- package/dist/domains/quality.d.ts +45 -0
- package/dist/domains/quality.d.ts.map +1 -0
- package/dist/domains/quality.js +368 -0
- package/dist/domains/quality.js.map +1 -0
- package/dist/domains/security.d.ts +19 -0
- package/dist/domains/security.d.ts.map +1 -0
- package/dist/domains/security.js +364 -0
- package/dist/domains/security.js.map +1 -0
- package/dist/domains/signal-match.d.ts +25 -0
- package/dist/domains/signal-match.d.ts.map +1 -0
- package/dist/domains/signal-match.js +67 -0
- package/dist/domains/signal-match.js.map +1 -0
- package/dist/domains/types.d.ts +354 -0
- package/dist/domains/types.d.ts.map +1 -0
- package/dist/domains/types.js +12 -0
- package/dist/domains/types.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/protocols/index.d.ts +36 -0
- package/dist/protocols/index.d.ts.map +1 -0
- package/dist/protocols/index.js +85 -0
- package/dist/protocols/index.js.map +1 -0
- package/dist/protocols/review-to-solid.d.ts +32 -0
- package/dist/protocols/review-to-solid.d.ts.map +1 -0
- package/dist/protocols/review-to-solid.js +309 -0
- package/dist/protocols/review-to-solid.js.map +1 -0
- package/dist/utils/prompt.d.ts +37 -0
- package/dist/utils/prompt.d.ts.map +1 -0
- package/dist/utils/prompt.js +81 -0
- package/dist/utils/prompt.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,922 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Exporter — OpenSpec core-compatible progressive disclosure
|
|
3
|
+
*
|
|
4
|
+
* 把 specdo 的 core 工作流导出为 progressive-disclosure SKILL.md 包,
|
|
5
|
+
* 投递到各 AI 工具的 skill 目录。
|
|
6
|
+
*
|
|
7
|
+
* 每个 skill 包结构:
|
|
8
|
+
* <name>/
|
|
9
|
+
* ├── SKILL.md # 精炼指引(agent 默认加载)
|
|
10
|
+
* └── references/ # 按需加载的详细参考
|
|
11
|
+
* ├── REFERENCE.md # workflow / protocol
|
|
12
|
+
* ├── EXAMPLES.md # workflow / protocol
|
|
13
|
+
* ├── DOMAIN_*.md # specdo 增强的 domain guidance
|
|
14
|
+
* ├── PROTOCOL_*.md # specdo 增强的 protocol guidance
|
|
15
|
+
* └── ARCHIVE_*.md # 收尾/归档 gate
|
|
16
|
+
*
|
|
17
|
+
* 关键不变量:
|
|
18
|
+
* - 导出入口始终保持 OpenSpec core 范式:workflow skill 是一等公民,
|
|
19
|
+
* domain/protocol 仅作为 workflow references 的增强材料
|
|
20
|
+
* - 内容来源全部为运行时数据(DomainModule / Protocol / WORKFLOW_SKILLS
|
|
21
|
+
* + 手写的 CROSS_DOMAIN / PROTOCOL_EXAMPLES),不依赖模板文件
|
|
22
|
+
* - SKILL.md 严格遵循 Anthropic 标准:YAML frontmatter(name + description)
|
|
23
|
+
* + 正文 markdown
|
|
24
|
+
* - skill name ≤ 64 字符,仅 lowercase 字母数字短横线(Anthropic 规范)
|
|
25
|
+
* - SKILL.md 推荐 ≤ 500 行(agentskills.io);progressive disclosure 让
|
|
26
|
+
* 大量内容下沉 references/,单文件保持精炼
|
|
27
|
+
*
|
|
28
|
+
* 输出 5 个 core workflow skill:
|
|
29
|
+
* - specdo-explore
|
|
30
|
+
* - specdo-propose
|
|
31
|
+
* - specdo-apply
|
|
32
|
+
* - specdo-sync
|
|
33
|
+
* - specdo-archive
|
|
34
|
+
*
|
|
35
|
+
* domain / protocol 不再作为独立 skill 导出,避免 agent 从工作流主轴偏航。
|
|
36
|
+
*/
|
|
37
|
+
import * as fs from 'node:fs';
|
|
38
|
+
import * as os from 'node:os';
|
|
39
|
+
import * as path from 'node:path';
|
|
40
|
+
import { ALL_DOMAINS, extractQuestions, isQuestionPool } from '../domains/index.js';
|
|
41
|
+
import { ALL_PROTOCOLS } from '../protocols/index.js';
|
|
42
|
+
import { WORKFLOW_SKILLS as WORKFLOW_CONTENT } from './skill-content/workflow-content.js';
|
|
43
|
+
import { CROSS_DOMAIN_NOTES } from './skill-content/cross-domain.js';
|
|
44
|
+
import { PROTOCOL_EXAMPLES } from './skill-content/protocol-examples.js';
|
|
45
|
+
const AGENT_REGISTRY = {
|
|
46
|
+
claude: { name: 'claude', homeSubdir: '.claude/skills', projectSubdir: '.claude/skills' },
|
|
47
|
+
qoder: { name: 'qoder', homeSubdir: '.qoder/skills', projectSubdir: '.qoder/skills' },
|
|
48
|
+
cursor: { name: 'cursor', homeSubdir: '.cursor/skills', projectSubdir: '.cursor/skills' },
|
|
49
|
+
cline: { name: 'cline', homeSubdir: '.cline/skills', projectSubdir: '.cline/skills' },
|
|
50
|
+
codex: { name: 'codex', homeSubdir: '.codex/skills', projectSubdir: '.codex/skills' },
|
|
51
|
+
goose: { name: 'goose', homeSubdir: '.goose/skills', projectSubdir: '.goose/skills' },
|
|
52
|
+
kilo: { name: 'kilo', homeSubdir: '.kilo/skills', projectSubdir: '.kilo/skills' },
|
|
53
|
+
opencode: { name: 'opencode', homeSubdir: '.opencode/skills', projectSubdir: '.opencode/skills' },
|
|
54
|
+
// 'universal' 是 escape hatch:写入社区开放标准 .agents/skills/
|
|
55
|
+
universal: { name: 'universal', homeSubdir: '.agents/skills', projectSubdir: '.agents/skills' },
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* 8 家 canonical AI 工具(不含 universal escape hatch)。
|
|
59
|
+
* 供文档、测试、拼写纠错限定在已审计的工具列表上。
|
|
60
|
+
*/
|
|
61
|
+
export const ALL_AGENTS = [
|
|
62
|
+
'claude',
|
|
63
|
+
'qoder',
|
|
64
|
+
'cursor',
|
|
65
|
+
'cline',
|
|
66
|
+
'codex',
|
|
67
|
+
'goose',
|
|
68
|
+
'kilo',
|
|
69
|
+
'opencode',
|
|
70
|
+
];
|
|
71
|
+
/** Universal escape hatch 单独曝露,需要与 ALL_AGENTS 区分对待。 */
|
|
72
|
+
export const UNIVERSAL_AGENT = 'universal';
|
|
73
|
+
export function isAgentName(value) {
|
|
74
|
+
return Object.prototype.hasOwnProperty.call(AGENT_REGISTRY, value);
|
|
75
|
+
}
|
|
76
|
+
// ─── Suggest helper:为 CLI 提供拼写纠错 ───────────────────────────
|
|
77
|
+
/**
|
|
78
|
+
* 计算两个字符串的 Levenshtein 距离。用于拼写纠错提示。
|
|
79
|
+
*/
|
|
80
|
+
export function levenshtein(a, b) {
|
|
81
|
+
if (a === b)
|
|
82
|
+
return 0;
|
|
83
|
+
if (a.length === 0)
|
|
84
|
+
return b.length;
|
|
85
|
+
if (b.length === 0)
|
|
86
|
+
return a.length;
|
|
87
|
+
const m = a.length;
|
|
88
|
+
const n = b.length;
|
|
89
|
+
const prev = new Array(n + 1);
|
|
90
|
+
const curr = new Array(n + 1);
|
|
91
|
+
for (let j = 0; j <= n; j++)
|
|
92
|
+
prev[j] = j;
|
|
93
|
+
for (let i = 1; i <= m; i++) {
|
|
94
|
+
curr[0] = i;
|
|
95
|
+
for (let j = 1; j <= n; j++) {
|
|
96
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
97
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
98
|
+
}
|
|
99
|
+
for (let j = 0; j <= n; j++)
|
|
100
|
+
prev[j] = curr[j];
|
|
101
|
+
}
|
|
102
|
+
return prev[n];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 在 ALL_AGENTS 中寻找与输入最接近的 canonical agent 名。
|
|
106
|
+
* 近似阈值:Levenshtein 距离 ≤ 1。
|
|
107
|
+
* 不返回 'universal':它是 escape hatch,不适合作为拼写纠错候选。
|
|
108
|
+
*/
|
|
109
|
+
export function suggestAgents(input) {
|
|
110
|
+
if (!input || input.length < 2)
|
|
111
|
+
return [];
|
|
112
|
+
const lower = input.toLowerCase();
|
|
113
|
+
const candidates = ALL_AGENTS.map((name) => ({
|
|
114
|
+
name,
|
|
115
|
+
distance: levenshtein(lower, name),
|
|
116
|
+
}));
|
|
117
|
+
const minDistance = Math.min(...candidates.map((c) => c.distance));
|
|
118
|
+
if (minDistance > 1)
|
|
119
|
+
return [];
|
|
120
|
+
return candidates
|
|
121
|
+
.filter((c) => c.distance === minDistance)
|
|
122
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
123
|
+
.map((c) => c.name);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 渲染 SKILL.md 完整内容(含 frontmatter)。
|
|
127
|
+
*
|
|
128
|
+
* description 内的双引号被替换为单引号,避免破坏 YAML 字符串;
|
|
129
|
+
* 空白序列被压缩,让 YAML 解析器 / agent 列表视图更友好。
|
|
130
|
+
*/
|
|
131
|
+
export function renderSkillFile(doc) {
|
|
132
|
+
const safeDescription = doc.description.replace(/"/g, "'").replace(/\s+/g, ' ').trim();
|
|
133
|
+
const frontmatter = ['---', `name: ${doc.name}`, `description: "${safeDescription}"`, '---', ''];
|
|
134
|
+
return `${frontmatter.join('\n')}\n${doc.body.trimEnd()}\n`;
|
|
135
|
+
}
|
|
136
|
+
// ─── Builders ─────────────────────────────────────────────────
|
|
137
|
+
function renderDomainDiscoveryReference() {
|
|
138
|
+
const lines = [];
|
|
139
|
+
lines.push('# Domain discovery guidance');
|
|
140
|
+
lines.push('');
|
|
141
|
+
lines.push('SpecDo keeps the OpenSpec-style workflow entrypoint at `explore`, then enriches it with domain scoring, clarifying questions, and cross-domain concerns.');
|
|
142
|
+
lines.push('');
|
|
143
|
+
lines.push('## How to use this reference');
|
|
144
|
+
lines.push('');
|
|
145
|
+
lines.push('1. Read the user\'s request and scan each domain\'s **Signals** list');
|
|
146
|
+
lines.push(' below for relevant keywords and concepts.');
|
|
147
|
+
lines.push('2. For each domain whose signals overlap with the user\'s intent, review');
|
|
148
|
+
lines.push(' the **Clarifying questions**.');
|
|
149
|
+
lines.push('3. **Select 1-4 domains** that best cover the user\'s needs. More domains');
|
|
150
|
+
lines.push(' mean broader coverage but also more complexity — prefer focused selection.');
|
|
151
|
+
lines.push('4. **Ask the user** the clarifying questions from selected domains. Record');
|
|
152
|
+
lines.push(' the answers.');
|
|
153
|
+
lines.push('5. Pass the selected domains to `specdo explore --domains <list>` and');
|
|
154
|
+
lines.push(' your collected answers via `--answers` or a summary via `--context`.');
|
|
155
|
+
lines.push('6. Set `--depth standard` (or `deep` if all questions were answered).');
|
|
156
|
+
lines.push('');
|
|
157
|
+
lines.push('**Important**: Do not rely on the automatic `scoreDomains()` signal matching');
|
|
158
|
+
lines.push('to select domains for you. The signal matching uses English keyword heuristics');
|
|
159
|
+
lines.push('and cannot understand user intent the way you can. You are the intelligent');
|
|
160
|
+
lines.push('agent — you decide which domains apply.');
|
|
161
|
+
lines.push('');
|
|
162
|
+
lines.push('## Available domains (6 total)');
|
|
163
|
+
lines.push('');
|
|
164
|
+
for (const domain of ALL_DOMAINS) {
|
|
165
|
+
lines.push(`## ${domain.name}`);
|
|
166
|
+
lines.push('');
|
|
167
|
+
lines.push(domain.description);
|
|
168
|
+
lines.push('');
|
|
169
|
+
const signals = domain.explore?.signals ?? [];
|
|
170
|
+
if (signals.length > 0) {
|
|
171
|
+
lines.push('**Signals**');
|
|
172
|
+
lines.push('');
|
|
173
|
+
for (const signal of signals) {
|
|
174
|
+
lines.push(`- \`${signal}\``);
|
|
175
|
+
}
|
|
176
|
+
lines.push('');
|
|
177
|
+
}
|
|
178
|
+
const questionsRaw = domain.explore?.questions ?? [];
|
|
179
|
+
const questions = extractQuestions(questionsRaw);
|
|
180
|
+
if (questions.length > 0) {
|
|
181
|
+
if (isQuestionPool(questionsRaw)) {
|
|
182
|
+
// Pool format: show with metadata for agent LLM generation
|
|
183
|
+
lines.push(`**Question pool** (${questionsRaw.items.length} items, default top-${questionsRaw.defaultCount})`);
|
|
184
|
+
lines.push('');
|
|
185
|
+
lines.push('Each item includes signals for relevance matching, priority (1-10),');
|
|
186
|
+
lines.push('and optional dependency chains. Agents can use this taxonomy to generate');
|
|
187
|
+
lines.push('LLM-tailored questions via `--llm` / `--llm-questions`.');
|
|
188
|
+
lines.push('');
|
|
189
|
+
// Show top 10 items with metadata as examples
|
|
190
|
+
for (const item of questionsRaw.items.slice(0, 10)) {
|
|
191
|
+
const meta = [
|
|
192
|
+
`priority=${item.priority}`,
|
|
193
|
+
item.requiresAnswer?.length ? `requires=[${item.requiresAnswer.join(',')}]` : null,
|
|
194
|
+
item.conditional ? `conditional=${JSON.stringify(item.conditional)}` : null,
|
|
195
|
+
].filter(Boolean).join(', ');
|
|
196
|
+
lines.push(`- ${item.text}`);
|
|
197
|
+
lines.push(` _signals: ${item.signals.slice(0, 5).join(', ')}${item.signals.length > 5 ? ', …' : ''}_`);
|
|
198
|
+
lines.push(` _${meta}_`);
|
|
199
|
+
}
|
|
200
|
+
if (questionsRaw.items.length > 10) {
|
|
201
|
+
lines.push(`- _…and ${questionsRaw.items.length - 10} more items (see references/CHECKLIST.md for full list)_`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
// Legacy format: show as before
|
|
206
|
+
lines.push('**Clarifying questions**');
|
|
207
|
+
lines.push('');
|
|
208
|
+
for (const question of questions) {
|
|
209
|
+
lines.push(`- ${question}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
lines.push('');
|
|
213
|
+
}
|
|
214
|
+
const crossDomain = CROSS_DOMAIN_NOTES[domain.name];
|
|
215
|
+
if (crossDomain) {
|
|
216
|
+
const summary = crossDomain
|
|
217
|
+
.split('\n')
|
|
218
|
+
.filter((line) => line.startsWith('- '))
|
|
219
|
+
.slice(0, 3);
|
|
220
|
+
if (summary.length > 0) {
|
|
221
|
+
lines.push('**Cross-domain coordination**');
|
|
222
|
+
lines.push('');
|
|
223
|
+
lines.push(...summary);
|
|
224
|
+
lines.push('');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return lines.join('\n').trimEnd() + '\n';
|
|
229
|
+
}
|
|
230
|
+
function renderDomainDesignReference() {
|
|
231
|
+
const lines = [];
|
|
232
|
+
lines.push('# Domain design guidance');
|
|
233
|
+
lines.push('');
|
|
234
|
+
lines.push('OpenSpec core keeps `propose` responsible for planning artifacts. SpecDo augments that step with the domain checklists below so the generated proposal/specs/design/tasks stay grounded in the right concerns.');
|
|
235
|
+
lines.push('');
|
|
236
|
+
for (const domain of ALL_DOMAINS) {
|
|
237
|
+
lines.push(`## ${domain.name}`);
|
|
238
|
+
lines.push('');
|
|
239
|
+
if (domain.design?.checklist?.length) {
|
|
240
|
+
lines.push('### Design checklist');
|
|
241
|
+
lines.push('');
|
|
242
|
+
for (const item of domain.design.checklist) {
|
|
243
|
+
lines.push(`- ${item}`);
|
|
244
|
+
}
|
|
245
|
+
lines.push('');
|
|
246
|
+
}
|
|
247
|
+
if (domain.design?.patterns && Object.keys(domain.design.patterns).length > 0) {
|
|
248
|
+
lines.push('### Recommended patterns');
|
|
249
|
+
lines.push('');
|
|
250
|
+
for (const [name, desc] of Object.entries(domain.design.patterns)) {
|
|
251
|
+
lines.push(`- **${name}** — ${desc}`);
|
|
252
|
+
}
|
|
253
|
+
lines.push('');
|
|
254
|
+
}
|
|
255
|
+
if (domain.design?.antiPatterns?.length) {
|
|
256
|
+
lines.push('### Anti-patterns');
|
|
257
|
+
lines.push('');
|
|
258
|
+
for (const item of domain.design.antiPatterns) {
|
|
259
|
+
lines.push(`- ${item}`);
|
|
260
|
+
}
|
|
261
|
+
lines.push('');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return lines.join('\n').trimEnd() + '\n';
|
|
265
|
+
}
|
|
266
|
+
function renderDomainImplementationReference() {
|
|
267
|
+
const lines = [];
|
|
268
|
+
lines.push('# Domain implementation guidance');
|
|
269
|
+
lines.push('');
|
|
270
|
+
lines.push('`apply` remains the OpenSpec execution step. SpecDo strengthens it with domain-specific implementation focus and verification obligations instead of splitting execution into extra top-level skills.');
|
|
271
|
+
lines.push('');
|
|
272
|
+
for (const domain of ALL_DOMAINS) {
|
|
273
|
+
lines.push(`## ${domain.name}`);
|
|
274
|
+
lines.push('');
|
|
275
|
+
if (domain.implement?.focusAreas?.length) {
|
|
276
|
+
lines.push('### Implementation focus');
|
|
277
|
+
lines.push('');
|
|
278
|
+
for (const item of domain.implement.focusAreas) {
|
|
279
|
+
lines.push(`- ${item}`);
|
|
280
|
+
}
|
|
281
|
+
lines.push('');
|
|
282
|
+
}
|
|
283
|
+
if (domain.implement?.patterns && Object.keys(domain.implement.patterns).length > 0) {
|
|
284
|
+
lines.push('### Implementation patterns');
|
|
285
|
+
lines.push('');
|
|
286
|
+
for (const [name, desc] of Object.entries(domain.implement.patterns)) {
|
|
287
|
+
lines.push(`- **${name}** — ${desc}`);
|
|
288
|
+
}
|
|
289
|
+
lines.push('');
|
|
290
|
+
}
|
|
291
|
+
if (domain.implement?.antiPatterns?.length) {
|
|
292
|
+
lines.push('### Implementation anti-patterns');
|
|
293
|
+
lines.push('');
|
|
294
|
+
for (const item of domain.implement.antiPatterns) {
|
|
295
|
+
lines.push(`- ${item}`);
|
|
296
|
+
}
|
|
297
|
+
lines.push('');
|
|
298
|
+
}
|
|
299
|
+
if (domain.verify?.checklist?.length) {
|
|
300
|
+
lines.push('### Verification gates');
|
|
301
|
+
lines.push('');
|
|
302
|
+
for (const item of domain.verify.checklist) {
|
|
303
|
+
lines.push(`- ${item}`);
|
|
304
|
+
}
|
|
305
|
+
lines.push('');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return lines.join('\n').trimEnd() + '\n';
|
|
309
|
+
}
|
|
310
|
+
function renderDomainSyncChecksReference() {
|
|
311
|
+
const lines = [];
|
|
312
|
+
lines.push('# Domain sync checks');
|
|
313
|
+
lines.push('');
|
|
314
|
+
lines.push('`sync` keeps OpenSpec\'s canonical-spec promotion step intact. SpecDo adds domain-aware checks for whether a per-change spec is ready to replace the canonical copy, without turning sync into another implementation phase.');
|
|
315
|
+
lines.push('');
|
|
316
|
+
lines.push('## Promotion checks');
|
|
317
|
+
lines.push('');
|
|
318
|
+
lines.push('- Confirm each promoted delta spec reflects what actually landed in code, docs, and runbooks.');
|
|
319
|
+
lines.push('- Resolve same-path canonical conflicts intentionally; `--force` is an overwrite decision, not a merge strategy.');
|
|
320
|
+
lines.push('- Keep the canonical library coherent: no mixed legacy/capability layout and no stale capability names.');
|
|
321
|
+
lines.push('- Treat unresolved sync conflicts as a release gate; do not archive through them.');
|
|
322
|
+
lines.push('');
|
|
323
|
+
lines.push('## Domain verification before promotion');
|
|
324
|
+
lines.push('');
|
|
325
|
+
for (const domain of ALL_DOMAINS) {
|
|
326
|
+
const items = domain.verify?.checklist ?? [];
|
|
327
|
+
if (items.length === 0)
|
|
328
|
+
continue;
|
|
329
|
+
lines.push(`### ${domain.name}`);
|
|
330
|
+
lines.push('');
|
|
331
|
+
for (const item of items) {
|
|
332
|
+
lines.push(`- ${item}`);
|
|
333
|
+
}
|
|
334
|
+
lines.push('');
|
|
335
|
+
}
|
|
336
|
+
return lines.join('\n').trimEnd() + '\n';
|
|
337
|
+
}
|
|
338
|
+
function renderProtocolWorkflowReference(protocol) {
|
|
339
|
+
const lines = [];
|
|
340
|
+
lines.push(`# Protocol enhancement — ${protocol.name}`);
|
|
341
|
+
lines.push('');
|
|
342
|
+
lines.push(protocol.description);
|
|
343
|
+
lines.push('');
|
|
344
|
+
lines.push(`**Goal:** ${protocol.goal}`);
|
|
345
|
+
lines.push('');
|
|
346
|
+
lines.push('## Trigger phrases');
|
|
347
|
+
lines.push('');
|
|
348
|
+
for (const phrase of protocol.triggerPhrases) {
|
|
349
|
+
lines.push(`- \`${phrase}\``);
|
|
350
|
+
}
|
|
351
|
+
lines.push('');
|
|
352
|
+
if (protocol.suppressionPhrases?.length) {
|
|
353
|
+
lines.push('## Suppression phrases');
|
|
354
|
+
lines.push('');
|
|
355
|
+
for (const phrase of protocol.suppressionPhrases) {
|
|
356
|
+
lines.push(`- \`${phrase}\``);
|
|
357
|
+
}
|
|
358
|
+
lines.push('');
|
|
359
|
+
}
|
|
360
|
+
lines.push('## Workflow');
|
|
361
|
+
lines.push('');
|
|
362
|
+
for (const step of protocol.workflow) {
|
|
363
|
+
lines.push(`### Step ${step.step} — ${step.title}`);
|
|
364
|
+
lines.push('');
|
|
365
|
+
lines.push(`**Goal:** ${step.goal}`);
|
|
366
|
+
lines.push('');
|
|
367
|
+
for (const action of step.actions) {
|
|
368
|
+
lines.push(`- ${action}`);
|
|
369
|
+
}
|
|
370
|
+
lines.push('');
|
|
371
|
+
}
|
|
372
|
+
lines.push('## Stop conditions');
|
|
373
|
+
lines.push('');
|
|
374
|
+
for (const item of protocol.stopConditions) {
|
|
375
|
+
lines.push(`- ${item}`);
|
|
376
|
+
}
|
|
377
|
+
lines.push('');
|
|
378
|
+
return lines.join('\n').trimEnd() + '\n';
|
|
379
|
+
}
|
|
380
|
+
function renderProtocolContractReference(protocol) {
|
|
381
|
+
const lines = [];
|
|
382
|
+
lines.push(`# Protocol output contract — ${protocol.name}`);
|
|
383
|
+
lines.push('');
|
|
384
|
+
lines.push('## Required sections');
|
|
385
|
+
lines.push('');
|
|
386
|
+
for (const section of protocol.outputContract) {
|
|
387
|
+
lines.push(`- **${section.name}** (${section.required ? 'required' : 'optional'}, ${section.format}) — ${section.description}`);
|
|
388
|
+
}
|
|
389
|
+
lines.push('');
|
|
390
|
+
if (protocol.severityLevels && Object.keys(protocol.severityLevels).length > 0) {
|
|
391
|
+
lines.push('## Severity levels');
|
|
392
|
+
lines.push('');
|
|
393
|
+
for (const [level, desc] of Object.entries(protocol.severityLevels)) {
|
|
394
|
+
lines.push(`- **${level}** — ${desc}`);
|
|
395
|
+
}
|
|
396
|
+
lines.push('');
|
|
397
|
+
}
|
|
398
|
+
lines.push('## Prohibitions');
|
|
399
|
+
lines.push('');
|
|
400
|
+
for (const item of protocol.prohibitions) {
|
|
401
|
+
lines.push(`- ${item}`);
|
|
402
|
+
}
|
|
403
|
+
lines.push('');
|
|
404
|
+
if (protocol.generalConstraints?.length) {
|
|
405
|
+
lines.push('## General constraints');
|
|
406
|
+
lines.push('');
|
|
407
|
+
for (const item of protocol.generalConstraints) {
|
|
408
|
+
lines.push(`- ${item}`);
|
|
409
|
+
}
|
|
410
|
+
lines.push('');
|
|
411
|
+
}
|
|
412
|
+
if (protocol.artifactChecklists && Object.keys(protocol.artifactChecklists).length > 0) {
|
|
413
|
+
lines.push('## Artifact-specific checks');
|
|
414
|
+
lines.push('');
|
|
415
|
+
for (const [artifact, checks] of Object.entries(protocol.artifactChecklists)) {
|
|
416
|
+
lines.push(`### ${artifact}`);
|
|
417
|
+
lines.push('');
|
|
418
|
+
for (const item of checks) {
|
|
419
|
+
lines.push(`- ${item}`);
|
|
420
|
+
}
|
|
421
|
+
lines.push('');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return lines.join('\n').trimEnd() + '\n';
|
|
425
|
+
}
|
|
426
|
+
function renderArchiveGatesReference() {
|
|
427
|
+
const lines = [];
|
|
428
|
+
lines.push('# Archive gates');
|
|
429
|
+
lines.push('');
|
|
430
|
+
lines.push('Archive stays the final OpenSpec core step. SpecDo adds explicit production gates before a change can be considered closed.');
|
|
431
|
+
lines.push('');
|
|
432
|
+
lines.push('## Required before archive');
|
|
433
|
+
lines.push('');
|
|
434
|
+
lines.push('- `specdo sync --change <name>` completed against the current working tree');
|
|
435
|
+
lines.push('- `context.apply` evidence still matches `tasks.md` and no task was silently skipped');
|
|
436
|
+
lines.push('- delta specs accurately represent the implementation that landed');
|
|
437
|
+
lines.push('- open risks, rollout notes, and follow-up items are either resolved or explicitly recorded');
|
|
438
|
+
lines.push('');
|
|
439
|
+
lines.push('## Domain closure scan');
|
|
440
|
+
lines.push('');
|
|
441
|
+
for (const domain of ALL_DOMAINS) {
|
|
442
|
+
lines.push(`### ${domain.name}`);
|
|
443
|
+
lines.push('');
|
|
444
|
+
const items = domain.verify?.checklist ?? [];
|
|
445
|
+
for (const item of items) {
|
|
446
|
+
lines.push(`- ${item}`);
|
|
447
|
+
}
|
|
448
|
+
lines.push('');
|
|
449
|
+
}
|
|
450
|
+
return lines.join('\n').trimEnd() + '\n';
|
|
451
|
+
}
|
|
452
|
+
function buildWorkflowAugmentedReferences(slug) {
|
|
453
|
+
const references = {};
|
|
454
|
+
if (slug === 'specdo-explore') {
|
|
455
|
+
references['DOMAIN_DISCOVERY.md'] = renderDomainDiscoveryReference();
|
|
456
|
+
}
|
|
457
|
+
if (slug === 'specdo-propose') {
|
|
458
|
+
references['DOMAIN_DESIGN.md'] = renderDomainDesignReference();
|
|
459
|
+
}
|
|
460
|
+
if (slug === 'specdo-apply') {
|
|
461
|
+
const protocol = ALL_PROTOCOLS.find((item) => item.name === 'review-to-solid');
|
|
462
|
+
references['DOMAIN_IMPLEMENTATION.md'] = renderDomainImplementationReference();
|
|
463
|
+
if (protocol) {
|
|
464
|
+
references['PROTOCOL_REVIEW_TO_SOLID.md'] = renderProtocolWorkflowReference(protocol);
|
|
465
|
+
references['PROTOCOL_OUTPUT_CONTRACT.md'] = renderProtocolContractReference(protocol);
|
|
466
|
+
references['PROTOCOL_EXAMPLES.md'] =
|
|
467
|
+
PROTOCOL_EXAMPLES[protocol.name] ??
|
|
468
|
+
`# ${protocol.name} — Examples\n\n_(No code examples authored yet for this protocol.)_\n`;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (slug === 'specdo-sync') {
|
|
472
|
+
references['DOMAIN_SYNC_CHECKS.md'] = renderDomainSyncChecksReference();
|
|
473
|
+
}
|
|
474
|
+
if (slug === 'specdo-archive') {
|
|
475
|
+
references['ARCHIVE_GATES.md'] = renderArchiveGatesReference();
|
|
476
|
+
}
|
|
477
|
+
return references;
|
|
478
|
+
}
|
|
479
|
+
/** 构造工作流 skill 文档(直接取自 workflow-content.ts,并注入 core-compatible 增强 references) */
|
|
480
|
+
export function buildWorkflowSkill(slug) {
|
|
481
|
+
const entry = WORKFLOW_CONTENT.find((s) => s.name === slug);
|
|
482
|
+
if (!entry) {
|
|
483
|
+
throw new Error(`Unknown workflow skill: ${slug}`);
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
name: entry.name,
|
|
487
|
+
description: entry.description,
|
|
488
|
+
body: entry.body,
|
|
489
|
+
references: {
|
|
490
|
+
...entry.references,
|
|
491
|
+
...buildWorkflowAugmentedReferences(slug),
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* 构造领域 skill 文档(progressive disclosure)。
|
|
497
|
+
*
|
|
498
|
+
* SKILL.md:领域描述 + 全部 signals(agent auto-trigger 需要)+ 各阶段前若干条精华
|
|
499
|
+
* references/CHECKLIST.md:完整 design.checklist + implement.focusAreas + verify.checklist
|
|
500
|
+
* references/PATTERNS.md:完整 patterns / antiPatterns(设计阶段 + 实施阶段)
|
|
501
|
+
* references/CROSS_DOMAIN.md:手写的跨领域协同说明(取自 cross-domain.ts)
|
|
502
|
+
*/
|
|
503
|
+
export function buildDomainSkill(domain) {
|
|
504
|
+
const signals = domain.explore?.signals ?? [];
|
|
505
|
+
const teaser = signals.slice(0, 8).join(', ');
|
|
506
|
+
const description = `Auto-invoke during specdo workflows touching ${teaser}` +
|
|
507
|
+
(signals.length > 8 ? ', etc' : '') +
|
|
508
|
+
`. Provides domain-specific design checklist, implementation focus areas, anti-patterns, and verification clauses for the ${domain.name} domain. See references/ for full lists, patterns, and cross-domain collaboration notes.`;
|
|
509
|
+
// ── SKILL.md body (concise) ────────────────────────────────
|
|
510
|
+
const body = [];
|
|
511
|
+
body.push(`# specdo-domain-${domain.name}`);
|
|
512
|
+
body.push('');
|
|
513
|
+
body.push(domain.description);
|
|
514
|
+
body.push('');
|
|
515
|
+
if (signals.length > 0) {
|
|
516
|
+
body.push('## Trigger signals');
|
|
517
|
+
body.push('');
|
|
518
|
+
body.push('When the user input matches any of these, this domain is scored and its checklists are injected:');
|
|
519
|
+
body.push('');
|
|
520
|
+
// 全部 signals 留在 SKILL.md(auto-trigger 召回需要)
|
|
521
|
+
body.push(signals.map((s) => `\`${s}\``).join(', '));
|
|
522
|
+
body.push('');
|
|
523
|
+
}
|
|
524
|
+
const exploreQuestions = domain.explore ? extractQuestions(domain.explore.questions) : [];
|
|
525
|
+
if (exploreQuestions.length > 0) {
|
|
526
|
+
body.push('## Top clarifying questions (explore stage)');
|
|
527
|
+
body.push('');
|
|
528
|
+
for (const q of exploreQuestions.slice(0, 4)) {
|
|
529
|
+
body.push(`- ${q}`);
|
|
530
|
+
}
|
|
531
|
+
if (exploreQuestions.length > 4) {
|
|
532
|
+
body.push(`- _…and ${exploreQuestions.length - 4} more (see references/CHECKLIST.md)_`);
|
|
533
|
+
}
|
|
534
|
+
body.push('');
|
|
535
|
+
}
|
|
536
|
+
if (domain.design && domain.design.checklist.length > 0) {
|
|
537
|
+
body.push('## Top design checks');
|
|
538
|
+
body.push('');
|
|
539
|
+
for (const item of domain.design.checklist.slice(0, 4)) {
|
|
540
|
+
body.push(`- ${item}`);
|
|
541
|
+
}
|
|
542
|
+
if (domain.design.checklist.length > 4) {
|
|
543
|
+
body.push(`- _…full ${domain.design.checklist.length}-item list in references/CHECKLIST.md_`);
|
|
544
|
+
}
|
|
545
|
+
body.push('');
|
|
546
|
+
}
|
|
547
|
+
if (domain.implement && domain.implement.focusAreas.length > 0) {
|
|
548
|
+
body.push('## Top implementation focus');
|
|
549
|
+
body.push('');
|
|
550
|
+
for (const item of domain.implement.focusAreas.slice(0, 4)) {
|
|
551
|
+
body.push(`- ${item}`);
|
|
552
|
+
}
|
|
553
|
+
if (domain.implement.focusAreas.length > 4) {
|
|
554
|
+
body.push(`- _…full ${domain.implement.focusAreas.length}-item list in references/CHECKLIST.md_`);
|
|
555
|
+
}
|
|
556
|
+
body.push('');
|
|
557
|
+
}
|
|
558
|
+
body.push('## See also');
|
|
559
|
+
body.push('');
|
|
560
|
+
body.push('- [`references/CHECKLIST.md`](references/CHECKLIST.md) — full design / implementation / verification lists');
|
|
561
|
+
body.push('- [`references/PATTERNS.md`](references/PATTERNS.md) — recommended patterns + anti-patterns');
|
|
562
|
+
body.push('- [`references/CROSS_DOMAIN.md`](references/CROSS_DOMAIN.md) — how this domain interacts with the other five');
|
|
563
|
+
// ── references/CHECKLIST.md ────────────────────────────────
|
|
564
|
+
const checklist = [];
|
|
565
|
+
checklist.push(`# ${domain.name} — Full checklist`);
|
|
566
|
+
checklist.push('');
|
|
567
|
+
checklist.push(`Complete design / implementation / verification reference for the **${domain.name}** domain. Loaded on demand when SKILL.md hints at deeper detail.`);
|
|
568
|
+
checklist.push('');
|
|
569
|
+
if (domain.explore) {
|
|
570
|
+
checklist.push('## All clarifying questions (explore stage)');
|
|
571
|
+
checklist.push('');
|
|
572
|
+
for (const q of extractQuestions(domain.explore.questions)) {
|
|
573
|
+
checklist.push(`- ${q}`);
|
|
574
|
+
}
|
|
575
|
+
checklist.push('');
|
|
576
|
+
}
|
|
577
|
+
if (domain.design && domain.design.checklist.length > 0) {
|
|
578
|
+
checklist.push('## Design checklist');
|
|
579
|
+
checklist.push('');
|
|
580
|
+
for (const item of domain.design.checklist) {
|
|
581
|
+
checklist.push(`- ${item}`);
|
|
582
|
+
}
|
|
583
|
+
checklist.push('');
|
|
584
|
+
}
|
|
585
|
+
if (domain.implement && domain.implement.focusAreas.length > 0) {
|
|
586
|
+
checklist.push('## Implementation focus areas');
|
|
587
|
+
checklist.push('');
|
|
588
|
+
for (const item of domain.implement.focusAreas) {
|
|
589
|
+
checklist.push(`- ${item}`);
|
|
590
|
+
}
|
|
591
|
+
checklist.push('');
|
|
592
|
+
}
|
|
593
|
+
if (domain.verify && domain.verify.checklist.length > 0) {
|
|
594
|
+
checklist.push('## Verification checklist');
|
|
595
|
+
checklist.push('');
|
|
596
|
+
for (const item of domain.verify.checklist) {
|
|
597
|
+
checklist.push(`- ${item}`);
|
|
598
|
+
}
|
|
599
|
+
checklist.push('');
|
|
600
|
+
}
|
|
601
|
+
// ── references/PATTERNS.md ─────────────────────────────────
|
|
602
|
+
const patterns = [];
|
|
603
|
+
patterns.push(`# ${domain.name} — Patterns & anti-patterns`);
|
|
604
|
+
patterns.push('');
|
|
605
|
+
patterns.push('Recommended patterns and known anti-patterns from both the design and implementation stages.');
|
|
606
|
+
patterns.push('');
|
|
607
|
+
if (domain.design?.patterns && Object.keys(domain.design.patterns).length > 0) {
|
|
608
|
+
patterns.push('## Design patterns');
|
|
609
|
+
patterns.push('');
|
|
610
|
+
for (const [name, desc] of Object.entries(domain.design.patterns)) {
|
|
611
|
+
patterns.push(`### ${name}`);
|
|
612
|
+
patterns.push('');
|
|
613
|
+
patterns.push(desc);
|
|
614
|
+
patterns.push('');
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (domain.design?.antiPatterns && domain.design.antiPatterns.length > 0) {
|
|
618
|
+
patterns.push('## Design anti-patterns');
|
|
619
|
+
patterns.push('');
|
|
620
|
+
for (const item of domain.design.antiPatterns) {
|
|
621
|
+
patterns.push(`- ${item}`);
|
|
622
|
+
}
|
|
623
|
+
patterns.push('');
|
|
624
|
+
}
|
|
625
|
+
if (domain.implement?.patterns && Object.keys(domain.implement.patterns).length > 0) {
|
|
626
|
+
patterns.push('## Implementation patterns');
|
|
627
|
+
patterns.push('');
|
|
628
|
+
for (const [name, desc] of Object.entries(domain.implement.patterns)) {
|
|
629
|
+
patterns.push(`### ${name}`);
|
|
630
|
+
patterns.push('');
|
|
631
|
+
patterns.push(desc);
|
|
632
|
+
patterns.push('');
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (domain.implement?.antiPatterns && domain.implement.antiPatterns.length > 0) {
|
|
636
|
+
patterns.push('## Implementation anti-patterns');
|
|
637
|
+
patterns.push('');
|
|
638
|
+
for (const item of domain.implement.antiPatterns) {
|
|
639
|
+
patterns.push(`- ${item}`);
|
|
640
|
+
}
|
|
641
|
+
patterns.push('');
|
|
642
|
+
}
|
|
643
|
+
// ── references/CROSS_DOMAIN.md ─────────────────────────────
|
|
644
|
+
const crossDomain = CROSS_DOMAIN_NOTES[domain.name] ??
|
|
645
|
+
`# ${domain.name} — Cross-domain collaboration\n\n_(No cross-domain notes authored yet for this domain.)_\n`;
|
|
646
|
+
return {
|
|
647
|
+
name: `specdo-domain-${domain.name}`,
|
|
648
|
+
description,
|
|
649
|
+
body: body.join('\n'),
|
|
650
|
+
references: {
|
|
651
|
+
'CHECKLIST.md': checklist.join('\n').trimEnd() + '\n',
|
|
652
|
+
'PATTERNS.md': patterns.join('\n').trimEnd() + '\n',
|
|
653
|
+
'CROSS_DOMAIN.md': crossDomain,
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* 构造协议 skill 文档(progressive disclosure)。
|
|
659
|
+
*
|
|
660
|
+
* SKILL.md:goal + trigger / suppression / doNotUseWhen + workflow step 标题 + stop conditions
|
|
661
|
+
* references/WORKFLOW.md:完整 workflow 各 step 的 actions / exitCriteria / loopBackTo
|
|
662
|
+
* references/OUTPUT_CONTRACT.md:outputContract + severityLevels + prohibitions + artifactChecklists + generalConstraints
|
|
663
|
+
* references/EXAMPLES.md:手写 do/don't 代码示例(取自 protocol-examples.ts)
|
|
664
|
+
*/
|
|
665
|
+
export function buildProtocolSkill(protocol) {
|
|
666
|
+
const phrases = protocol.triggerPhrases.slice(0, 4).join(', ');
|
|
667
|
+
const description = `Use when ${protocol.goal} Trigger phrases include: ${phrases}. Avoid for: ${protocol.doNotUseWhen
|
|
668
|
+
.slice(0, 2)
|
|
669
|
+
.join('; ')}. See references/ for the full workflow, output contract, and SOLID examples.`;
|
|
670
|
+
// ── SKILL.md body (concise) ────────────────────────────────
|
|
671
|
+
const body = [];
|
|
672
|
+
body.push(`# specdo-protocol-${protocol.name}`);
|
|
673
|
+
body.push('');
|
|
674
|
+
body.push(`> ${protocol.description}`);
|
|
675
|
+
body.push('');
|
|
676
|
+
body.push('## Goal');
|
|
677
|
+
body.push('');
|
|
678
|
+
body.push(protocol.goal);
|
|
679
|
+
body.push('');
|
|
680
|
+
body.push('## Trigger phrases');
|
|
681
|
+
body.push('');
|
|
682
|
+
for (const phrase of protocol.triggerPhrases) {
|
|
683
|
+
body.push(`- \`${phrase}\``);
|
|
684
|
+
}
|
|
685
|
+
body.push('');
|
|
686
|
+
if (protocol.suppressionPhrases && protocol.suppressionPhrases.length > 0) {
|
|
687
|
+
body.push('## Suppression phrases (do NOT trigger when present)');
|
|
688
|
+
body.push('');
|
|
689
|
+
for (const phrase of protocol.suppressionPhrases) {
|
|
690
|
+
body.push(`- \`${phrase}\``);
|
|
691
|
+
}
|
|
692
|
+
body.push('');
|
|
693
|
+
}
|
|
694
|
+
body.push('## Do NOT use when');
|
|
695
|
+
body.push('');
|
|
696
|
+
for (const item of protocol.doNotUseWhen) {
|
|
697
|
+
body.push(`- ${item}`);
|
|
698
|
+
}
|
|
699
|
+
body.push('');
|
|
700
|
+
body.push('## Workflow (titles only — see references/WORKFLOW.md for actions)');
|
|
701
|
+
body.push('');
|
|
702
|
+
for (const step of protocol.workflow) {
|
|
703
|
+
body.push(`${step.step}. **${step.title}** — ${step.goal}`);
|
|
704
|
+
}
|
|
705
|
+
body.push('');
|
|
706
|
+
body.push('## Stop conditions');
|
|
707
|
+
body.push('');
|
|
708
|
+
for (const cond of protocol.stopConditions) {
|
|
709
|
+
body.push(`- ${cond}`);
|
|
710
|
+
}
|
|
711
|
+
body.push('');
|
|
712
|
+
body.push('## See also');
|
|
713
|
+
body.push('');
|
|
714
|
+
body.push('- [`references/WORKFLOW.md`](references/WORKFLOW.md) — per-step actions, exit criteria, loop-back rules');
|
|
715
|
+
body.push('- [`references/OUTPUT_CONTRACT.md`](references/OUTPUT_CONTRACT.md) — required output sections, severity levels, prohibitions, artifact-specific checks');
|
|
716
|
+
body.push('- [`references/EXAMPLES.md`](references/EXAMPLES.md) — do/don\'t code examples for each SOLID letter');
|
|
717
|
+
// ── references/WORKFLOW.md ─────────────────────────────────
|
|
718
|
+
const workflowLines = [];
|
|
719
|
+
workflowLines.push(`# ${protocol.name} — Workflow detail`);
|
|
720
|
+
workflowLines.push('');
|
|
721
|
+
workflowLines.push('Full per-step actions, exit criteria, and loop-back rules. Each step in this list mirrors the title shown in SKILL.md.');
|
|
722
|
+
workflowLines.push('');
|
|
723
|
+
for (const step of protocol.workflow) {
|
|
724
|
+
workflowLines.push(`## Step ${step.step} — ${step.title}`);
|
|
725
|
+
workflowLines.push('');
|
|
726
|
+
workflowLines.push(`**Goal:** ${step.goal}`);
|
|
727
|
+
workflowLines.push('');
|
|
728
|
+
workflowLines.push('**Actions:**');
|
|
729
|
+
for (const action of step.actions) {
|
|
730
|
+
workflowLines.push(`- ${action}`);
|
|
731
|
+
}
|
|
732
|
+
if (step.exitCriteria && step.exitCriteria.length > 0) {
|
|
733
|
+
workflowLines.push('');
|
|
734
|
+
workflowLines.push('**Exit criteria:**');
|
|
735
|
+
for (const c of step.exitCriteria) {
|
|
736
|
+
workflowLines.push(`- ${c}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (step.loopBackTo && step.loopBackTo.length > 0) {
|
|
740
|
+
workflowLines.push('');
|
|
741
|
+
workflowLines.push(`**May loop back to step(s):** ${step.loopBackTo.join(', ')}`);
|
|
742
|
+
}
|
|
743
|
+
workflowLines.push('');
|
|
744
|
+
}
|
|
745
|
+
// ── references/OUTPUT_CONTRACT.md ──────────────────────────
|
|
746
|
+
const contractLines = [];
|
|
747
|
+
contractLines.push(`# ${protocol.name} — Output contract`);
|
|
748
|
+
contractLines.push('');
|
|
749
|
+
contractLines.push('Required output sections, severity levels, prohibitions, and any artifact-specific checks. Loaded on demand when preparing the final deliverable.');
|
|
750
|
+
contractLines.push('');
|
|
751
|
+
contractLines.push('## Required sections');
|
|
752
|
+
contractLines.push('');
|
|
753
|
+
for (const section of protocol.outputContract) {
|
|
754
|
+
contractLines.push(`- **${section.name}** (${section.required ? 'required' : 'optional'}, ${section.format}) — ${section.description}`);
|
|
755
|
+
}
|
|
756
|
+
contractLines.push('');
|
|
757
|
+
if (protocol.severityLevels && Object.keys(protocol.severityLevels).length > 0) {
|
|
758
|
+
contractLines.push('## Severity levels');
|
|
759
|
+
contractLines.push('');
|
|
760
|
+
for (const [level, desc] of Object.entries(protocol.severityLevels)) {
|
|
761
|
+
contractLines.push(`- **${level}** — ${desc}`);
|
|
762
|
+
}
|
|
763
|
+
contractLines.push('');
|
|
764
|
+
}
|
|
765
|
+
contractLines.push('## Prohibitions');
|
|
766
|
+
contractLines.push('');
|
|
767
|
+
for (const item of protocol.prohibitions) {
|
|
768
|
+
contractLines.push(`- ${item}`);
|
|
769
|
+
}
|
|
770
|
+
contractLines.push('');
|
|
771
|
+
if (protocol.artifactChecklists && Object.keys(protocol.artifactChecklists).length > 0) {
|
|
772
|
+
contractLines.push('## Artifact-specific checklists');
|
|
773
|
+
contractLines.push('');
|
|
774
|
+
for (const [artifact, checks] of Object.entries(protocol.artifactChecklists)) {
|
|
775
|
+
contractLines.push(`### ${artifact}`);
|
|
776
|
+
contractLines.push('');
|
|
777
|
+
for (const c of checks) {
|
|
778
|
+
contractLines.push(`- ${c}`);
|
|
779
|
+
}
|
|
780
|
+
contractLines.push('');
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (protocol.generalConstraints && protocol.generalConstraints.length > 0) {
|
|
784
|
+
contractLines.push('## General constraints');
|
|
785
|
+
contractLines.push('');
|
|
786
|
+
for (const item of protocol.generalConstraints) {
|
|
787
|
+
contractLines.push(`- ${item}`);
|
|
788
|
+
}
|
|
789
|
+
contractLines.push('');
|
|
790
|
+
}
|
|
791
|
+
// ── references/EXAMPLES.md ─────────────────────────────────
|
|
792
|
+
const examples = PROTOCOL_EXAMPLES[protocol.name] ??
|
|
793
|
+
`# ${protocol.name} — Examples\n\n_(No code examples authored yet for this protocol.)_\n`;
|
|
794
|
+
return {
|
|
795
|
+
name: `specdo-protocol-${protocol.name}`,
|
|
796
|
+
description,
|
|
797
|
+
body: body.join('\n'),
|
|
798
|
+
references: {
|
|
799
|
+
'WORKFLOW.md': workflowLines.join('\n').trimEnd() + '\n',
|
|
800
|
+
'OUTPUT_CONTRACT.md': contractLines.join('\n').trimEnd() + '\n',
|
|
801
|
+
'EXAMPLES.md': examples,
|
|
802
|
+
},
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
// ─── Aggregate:组装 5 个 core workflow skill ───────────────────
|
|
806
|
+
/** 返回所有应被导出的 SkillDoc */
|
|
807
|
+
export function collectAllSkills() {
|
|
808
|
+
return WORKFLOW_CONTENT.map((entry) => buildWorkflowSkill(entry.name));
|
|
809
|
+
}
|
|
810
|
+
// ─── 写入实现 ─────────────────────────────────────────────────
|
|
811
|
+
function resolveAgentRoot(agent, scope, cwd) {
|
|
812
|
+
const spec = AGENT_REGISTRY[agent];
|
|
813
|
+
if (scope === 'user') {
|
|
814
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
815
|
+
return path.join(homeDir, spec.homeSubdir);
|
|
816
|
+
}
|
|
817
|
+
return path.join(cwd, spec.projectSubdir);
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* 把单个 SkillDoc 写入指定 agent 根目录。
|
|
821
|
+
*
|
|
822
|
+
* 落盘结构:
|
|
823
|
+
* <root>/<name>/SKILL.md
|
|
824
|
+
* <root>/<name>/references/<filename> (每个 references 条目一个文件)
|
|
825
|
+
*
|
|
826
|
+
* 写入语义:
|
|
827
|
+
* - 目录不存在则递归创建
|
|
828
|
+
* - 同名文件直接覆盖(specdo 视 skill 文件为派生产物)
|
|
829
|
+
*/
|
|
830
|
+
function writeSkillDoc(doc, root) {
|
|
831
|
+
const written = [];
|
|
832
|
+
const skillDir = path.join(root, doc.name);
|
|
833
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
834
|
+
// SKILL.md
|
|
835
|
+
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
836
|
+
fs.writeFileSync(skillPath, renderSkillFile(doc), 'utf-8');
|
|
837
|
+
written.push(skillPath);
|
|
838
|
+
// references/*
|
|
839
|
+
if (doc.references) {
|
|
840
|
+
const refsDir = path.join(skillDir, 'references');
|
|
841
|
+
fs.mkdirSync(refsDir, { recursive: true });
|
|
842
|
+
for (const [filename, content] of Object.entries(doc.references)) {
|
|
843
|
+
const refPath = path.join(refsDir, filename);
|
|
844
|
+
fs.writeFileSync(refPath, content, 'utf-8');
|
|
845
|
+
written.push(refPath);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return written;
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* 清理 agent skill 根目录下以 `specdo-` 开头但不再属于当前导出列表的旧目录。
|
|
852
|
+
*
|
|
853
|
+
* 场景:skill 从 12 个缩减为 5 个后,用户重新 `specdo init` 时,旧的
|
|
854
|
+
* `specdo-domain-*` 和 `specdo-protocol-*` 目录会残留,导致 AI 工具同时
|
|
855
|
+
* 加载新 workflow skill 和旧 domain/protocol skill,产生冲突指令。
|
|
856
|
+
*
|
|
857
|
+
* 安全约束:
|
|
858
|
+
* - 仅删除以 `specdo-` 开头的子目录(不碰用户自定义 skill)
|
|
859
|
+
* - 跳过当前导出列表中存在的目录名
|
|
860
|
+
* - 目录不存在时静默跳过
|
|
861
|
+
*/
|
|
862
|
+
function removeOrphanSpecdoSkills(root, currentSkillNames) {
|
|
863
|
+
const pruned = [];
|
|
864
|
+
let entries;
|
|
865
|
+
try {
|
|
866
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
867
|
+
}
|
|
868
|
+
catch {
|
|
869
|
+
return pruned;
|
|
870
|
+
}
|
|
871
|
+
for (const entry of entries) {
|
|
872
|
+
if (!entry.isDirectory())
|
|
873
|
+
continue;
|
|
874
|
+
if (!entry.name.startsWith('specdo-'))
|
|
875
|
+
continue;
|
|
876
|
+
if (currentSkillNames.has(entry.name))
|
|
877
|
+
continue;
|
|
878
|
+
const orphanDir = path.join(root, entry.name);
|
|
879
|
+
fs.rmSync(orphanDir, { recursive: true, force: true });
|
|
880
|
+
pruned.push(orphanDir);
|
|
881
|
+
}
|
|
882
|
+
return pruned;
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* 主导出函数:把 5 个 core workflow skill 写到指定 agent 的指定 scope 目录。
|
|
886
|
+
*
|
|
887
|
+
* 写入前会清理 agent skill 根目录下以 `specdo-` 开头但不再属于当前导出列表
|
|
888
|
+
* 的旧目录避免 AI 工具加载冲突指令。
|
|
889
|
+
*/
|
|
890
|
+
export function exportSkills(options) {
|
|
891
|
+
const { cwd, scope } = options;
|
|
892
|
+
const agentSet = new Set();
|
|
893
|
+
for (const agent of options.agents) {
|
|
894
|
+
if (!isAgentName(agent)) {
|
|
895
|
+
throw new Error(`Unknown agent target: ${String(agent)}`);
|
|
896
|
+
}
|
|
897
|
+
agentSet.add(agent);
|
|
898
|
+
}
|
|
899
|
+
const agents = [...agentSet];
|
|
900
|
+
const docs = collectAllSkills();
|
|
901
|
+
const currentSkillNames = new Set(docs.map((d) => d.name));
|
|
902
|
+
const filesWritten = [];
|
|
903
|
+
const pruned = [];
|
|
904
|
+
for (const agent of agents) {
|
|
905
|
+
const root = resolveAgentRoot(agent, scope, cwd);
|
|
906
|
+
pruned.push(...removeOrphanSpecdoSkills(root, currentSkillNames));
|
|
907
|
+
for (const doc of docs) {
|
|
908
|
+
const written = writeSkillDoc(doc, root);
|
|
909
|
+
filesWritten.push(...written);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return { filesWritten, skillCount: agents.length > 0 ? docs.length : 0, agents, skipped: [], pruned };
|
|
913
|
+
}
|
|
914
|
+
/** 导出供测试与命令层引用 */
|
|
915
|
+
export const __INTERNAL = {
|
|
916
|
+
AGENT_REGISTRY,
|
|
917
|
+
WORKFLOW_SKILL_NAMES: WORKFLOW_CONTENT.map((s) => s.name),
|
|
918
|
+
resolveAgentRoot,
|
|
919
|
+
writeSkillDoc,
|
|
920
|
+
removeOrphanSpecdoSkills,
|
|
921
|
+
};
|
|
922
|
+
//# sourceMappingURL=skill-exporter.js.map
|