@zhuan-ai/zhuanspec 2.2.4 → 2.4.8
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/bin/zhuanspec-hook.js +3 -0
- package/dist/cli/hooks.d.ts +14 -0
- package/dist/cli/hooks.js +465 -0
- package/dist/cli/index.js +100 -0
- package/dist/commands/artifact-workflow.js +15 -34
- package/dist/commands/design.d.ts +42 -0
- package/dist/commands/design.js +337 -0
- package/dist/commands/progress.d.ts +32 -0
- package/dist/commands/progress.js +278 -0
- package/dist/commands/review.d.ts +32 -0
- package/dist/commands/review.js +472 -0
- package/dist/commands/validate.d.ts +14 -0
- package/dist/commands/validate.js +161 -10
- package/dist/core/archive.d.ts +1 -0
- package/dist/core/archive.js +45 -3
- package/dist/core/completions/command-registry.js +67 -0
- package/dist/core/configurators/slash/amazon-q.js +32 -2
- package/dist/core/configurators/slash/antigravity.js +8 -2
- package/dist/core/configurators/slash/auggie.js +16 -1
- package/dist/core/configurators/slash/base.js +1 -1
- package/dist/core/configurators/slash/claude.d.ts +4 -0
- package/dist/core/configurators/slash/claude.js +56 -1
- package/dist/core/configurators/slash/cline.js +8 -2
- package/dist/core/configurators/slash/codebuddy.js +22 -1
- package/dist/core/configurators/slash/codex.js +21 -0
- package/dist/core/configurators/slash/costrict.js +15 -0
- package/dist/core/configurators/slash/crush.js +22 -1
- package/dist/core/configurators/slash/cursor.js +22 -1
- package/dist/core/configurators/slash/factory.js +16 -1
- package/dist/core/configurators/slash/gemini.js +8 -2
- package/dist/core/configurators/slash/github-copilot.js +19 -1
- package/dist/core/configurators/slash/iflow.js +22 -1
- package/dist/core/configurators/slash/kilocode.js +4 -1
- package/dist/core/configurators/slash/opencode.js +27 -0
- package/dist/core/configurators/slash/qoder.d.ts +4 -0
- package/dist/core/configurators/slash/qoder.js +59 -1
- package/dist/core/configurators/slash/qwen.js +8 -2
- package/dist/core/configurators/slash/roocode.js +8 -2
- package/dist/core/configurators/slash/windsurf.js +8 -2
- package/dist/core/dashboard/metrics.d.ts +33 -0
- package/dist/core/dashboard/metrics.js +114 -0
- package/dist/core/hooks/collect-knowledge.d.ts +16 -0
- package/dist/core/hooks/collect-knowledge.js +203 -0
- package/dist/core/hooks/context-load-hook.d.ts +25 -0
- package/dist/core/hooks/context-load-hook.js +159 -0
- package/dist/core/hooks/deviation-check.d.ts +27 -0
- package/dist/core/hooks/deviation-check.js +403 -0
- package/dist/core/hooks/deviation-handler.d.ts +43 -0
- package/dist/core/hooks/deviation-handler.js +98 -0
- package/dist/core/hooks/init.d.ts +14 -0
- package/dist/core/hooks/init.js +244 -0
- package/dist/core/hooks/notify-milestone.d.ts +14 -0
- package/dist/core/hooks/notify-milestone.js +170 -0
- package/dist/core/hooks/post-apply.d.ts +29 -0
- package/dist/core/hooks/post-apply.js +173 -0
- package/dist/core/hooks/post-archive.d.ts +7 -0
- package/dist/core/hooks/post-archive.js +208 -0
- package/dist/core/hooks/pre-apply.d.ts +34 -0
- package/dist/core/hooks/pre-apply.js +139 -0
- package/dist/core/hooks/pre-archive.d.ts +7 -0
- package/dist/core/hooks/pre-archive.js +50 -0
- package/dist/core/hooks/record-progress.d.ts +49 -0
- package/dist/core/hooks/record-progress.js +494 -0
- package/dist/core/hooks/review-hooks.d.ts +89 -0
- package/dist/core/hooks/review-hooks.js +345 -0
- package/dist/core/hooks/review-orchestrator.d.ts +40 -0
- package/dist/core/hooks/review-orchestrator.js +146 -0
- package/dist/core/hooks/summarize.d.ts +15 -0
- package/dist/core/hooks/summarize.js +282 -0
- package/dist/core/hooks/user-input-hook.d.ts +25 -0
- package/dist/core/hooks/user-input-hook.js +179 -0
- package/dist/core/init.d.ts +8 -0
- package/dist/core/init.js +251 -23
- package/dist/core/parsers/requirement-blocks.js +13 -10
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +510 -243
- package/dist/core/templates/index.d.ts +1 -0
- package/dist/core/templates/index.js +1 -0
- package/dist/core/templates/skill-templates.js +46 -152
- package/dist/core/templates/slash-command-templates.d.ts +1 -1
- package/dist/core/templates/slash-command-templates.js +352 -20
- package/dist/core/templates/tasks-template.d.ts +7 -0
- package/dist/core/templates/tasks-template.js +130 -24
- package/dist/core/templates/tdd-tasks-template.d.ts +3 -0
- package/dist/core/templates/tdd-tasks-template.js +91 -38
- package/dist/core/templates/test-cases-template.d.ts +41 -0
- package/dist/core/templates/test-cases-template.js +128 -0
- package/dist/core/validation/strict-rules.d.ts +44 -5
- package/dist/core/validation/strict-rules.js +302 -8
- package/dist/core/validation/validator.js +52 -2
- package/dist/core/view.d.ts +1 -0
- package/dist/core/view.js +60 -2
- package/dist/mcp/index.d.ts +28 -0
- package/dist/mcp/index.js +31 -0
- package/dist/utils/file-system.d.ts +1 -0
- package/dist/utils/file-system.js +11 -0
- package/dist/utils/item-discovery.js +24 -2
- package/dist/utils/phase-utils.d.ts +36 -0
- package/dist/utils/phase-utils.js +117 -0
- package/package.json +22 -23
- package/schemas/spec-driven/schema.yaml +45 -31
- package/schemas/spec-driven/templates/spec.md +142 -5
- package/schemas/spec-driven/templates/tasks.md +73 -9
|
@@ -21,7 +21,7 @@ export class ValidateCommand {
|
|
|
21
21
|
// No item and no flags
|
|
22
22
|
if (!itemName) {
|
|
23
23
|
if (interactive) {
|
|
24
|
-
await this.runInteractiveSelector({ strict: !!options.strict, json: !!options.json, concurrency: options.concurrency });
|
|
24
|
+
await this.runInteractiveSelector({ strict: !!options.strict, json: !!options.json, concurrency: options.concurrency, autoFix: !!options.autoFix });
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
this.printNonInteractiveHint();
|
|
@@ -30,7 +30,7 @@ export class ValidateCommand {
|
|
|
30
30
|
}
|
|
31
31
|
// Direct item validation with type detection or override
|
|
32
32
|
const typeOverride = this.normalizeType(options.type);
|
|
33
|
-
await this.validateDirectItem(itemName, { typeOverride, strict: !!options.strict, json: !!options.json });
|
|
33
|
+
await this.validateDirectItem(itemName, { typeOverride, strict: !!options.strict, json: !!options.json, autoFix: !!options.autoFix });
|
|
34
34
|
}
|
|
35
35
|
normalizeType(value) {
|
|
36
36
|
if (!value)
|
|
@@ -101,24 +101,64 @@ export class ValidateCommand {
|
|
|
101
101
|
}
|
|
102
102
|
async validateByType(type, id, opts) {
|
|
103
103
|
const validator = new Validator(opts.strict);
|
|
104
|
+
const MAX_AUTO_FIX_ATTEMPTS = 3;
|
|
104
105
|
if (type === 'change') {
|
|
105
106
|
const changeDir = path.join(process.cwd(), 'zhuanspec', 'changes', id);
|
|
107
|
+
// Auto-fix loop
|
|
108
|
+
let attempt = 0;
|
|
109
|
+
let report;
|
|
110
|
+
while (attempt < MAX_AUTO_FIX_ATTEMPTS) {
|
|
111
|
+
const start = Date.now();
|
|
112
|
+
report = await validator.validateChangeDeltaSpecs(changeDir);
|
|
113
|
+
// Validate tasks.md dependencies if present
|
|
114
|
+
const taskReport = await this.validateTaskDependencies(changeDir);
|
|
115
|
+
// Merge task dependency issues into report
|
|
116
|
+
if (taskReport.issues.length > 0) {
|
|
117
|
+
report.issues.push(...taskReport.issues);
|
|
118
|
+
report.valid = report.valid && taskReport.valid;
|
|
119
|
+
}
|
|
120
|
+
// Write Mermaid diagram to tasks.md if generated
|
|
121
|
+
if (report.mermaidDiagram) {
|
|
122
|
+
await this.writeMermaidDiagramToTasks(changeDir, report.mermaidDiagram);
|
|
123
|
+
}
|
|
124
|
+
const durationMs = Date.now() - start;
|
|
125
|
+
// If valid or no auto-fix, break the loop
|
|
126
|
+
if (report.valid || !opts.autoFix) {
|
|
127
|
+
this.printReport('change', id, report, durationMs, opts.json, taskReport, { strict: opts.strict });
|
|
128
|
+
process.exitCode = report.valid ? 0 : 1;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Auto-fix attempt
|
|
132
|
+
attempt++;
|
|
133
|
+
if (!opts.json) {
|
|
134
|
+
console.log(`Auto-fix attempt ${attempt}/${MAX_AUTO_FIX_ATTEMPTS}...`);
|
|
135
|
+
}
|
|
136
|
+
// Try to fix common issues
|
|
137
|
+
const fixed = await this.tryAutoFix(changeDir, report.issues);
|
|
138
|
+
if (!fixed) {
|
|
139
|
+
// No fixes applied, break the loop
|
|
140
|
+
this.printReport('change', id, report, durationMs, opts.json, taskReport, { strict: opts.strict });
|
|
141
|
+
if (!opts.json) {
|
|
142
|
+
console.error(`Auto-fix failed after ${attempt} attempts. Manual intervention required.`);
|
|
143
|
+
}
|
|
144
|
+
process.exitCode = 1;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// Fixes applied, continue loop to re-validate
|
|
148
|
+
}
|
|
149
|
+
// Max attempts reached
|
|
106
150
|
const start = Date.now();
|
|
107
|
-
|
|
108
|
-
// Validate tasks.md dependencies if present
|
|
151
|
+
report = await validator.validateChangeDeltaSpecs(changeDir);
|
|
109
152
|
const taskReport = await this.validateTaskDependencies(changeDir);
|
|
110
|
-
// Merge task dependency issues into report
|
|
111
153
|
if (taskReport.issues.length > 0) {
|
|
112
154
|
report.issues.push(...taskReport.issues);
|
|
113
155
|
report.valid = report.valid && taskReport.valid;
|
|
114
156
|
}
|
|
115
|
-
// Write Mermaid diagram to tasks.md if generated
|
|
116
|
-
if (report.mermaidDiagram) {
|
|
117
|
-
await this.writeMermaidDiagramToTasks(changeDir, report.mermaidDiagram);
|
|
118
|
-
}
|
|
119
157
|
const durationMs = Date.now() - start;
|
|
120
158
|
this.printReport('change', id, report, durationMs, opts.json, taskReport, { strict: opts.strict });
|
|
121
|
-
|
|
159
|
+
if (!opts.json && !report.valid) {
|
|
160
|
+
console.error(`Auto-fix failed after ${MAX_AUTO_FIX_ATTEMPTS} attempts. Manual intervention required.`);
|
|
161
|
+
}
|
|
122
162
|
process.exitCode = report.valid ? 0 : 1;
|
|
123
163
|
return;
|
|
124
164
|
}
|
|
@@ -129,6 +169,117 @@ export class ValidateCommand {
|
|
|
129
169
|
this.printReport('spec', id, report, durationMs, opts.json, undefined, { strict: opts.strict });
|
|
130
170
|
process.exitCode = report.valid ? 0 : 1;
|
|
131
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Attempt to auto-fix common validation issues.
|
|
174
|
+
* Returns true if any fixes were applied.
|
|
175
|
+
*/
|
|
176
|
+
async tryAutoFix(changeDir, issues) {
|
|
177
|
+
let fixed = false;
|
|
178
|
+
for (const issue of issues) {
|
|
179
|
+
// Skip non-ERROR issues
|
|
180
|
+
if (issue.level !== 'ERROR')
|
|
181
|
+
continue;
|
|
182
|
+
// 1. Missing proposal section - add skeleton
|
|
183
|
+
if (issue.message.includes('Proposal 缺少必需章节')) {
|
|
184
|
+
const sectionMatch = issue.message.match(/缺少必需章节[::]\s*(.+)/);
|
|
185
|
+
if (sectionMatch) {
|
|
186
|
+
const sectionName = sectionMatch[1];
|
|
187
|
+
await this.addProposalSectionSkeleton(changeDir, sectionName);
|
|
188
|
+
fixed = true;
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// 2. Missing review gate - add to tasks.md
|
|
193
|
+
if (issue.message.includes('review gate') || issue.message.includes('zhuanspec review')) {
|
|
194
|
+
await this.addReviewGateToTasks(changeDir);
|
|
195
|
+
fixed = true;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// 4. Missing @skill tag - add @skill:none (existing logic)
|
|
199
|
+
if (issue.message.includes('Skill Mapping') || issue.message.includes('@skill')) {
|
|
200
|
+
const tasksFile = path.join(changeDir, 'tasks.md');
|
|
201
|
+
try {
|
|
202
|
+
const content = await fs.readFile(tasksFile, 'utf-8');
|
|
203
|
+
const lines = content.split('\n');
|
|
204
|
+
const newLines = lines.map(line => {
|
|
205
|
+
// If line is a task checkbox without @skill, add @skill:none
|
|
206
|
+
if (line.match(/^- \[ \] \d+\.\d+/) && !line.includes('@skill:')) {
|
|
207
|
+
return line + ' @skill:none';
|
|
208
|
+
}
|
|
209
|
+
return line;
|
|
210
|
+
});
|
|
211
|
+
const newContent = newLines.join('\n');
|
|
212
|
+
if (newContent !== content) {
|
|
213
|
+
await fs.writeFile(tasksFile, newContent, 'utf-8');
|
|
214
|
+
fixed = true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// Cannot read/write tasks.md
|
|
219
|
+
}
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
// 5. Wave ordering issues - cannot auto-fix (needs manual review)
|
|
223
|
+
if (issue.message.includes('appears after tasks of wave') || issue.message.includes('Wave headers')) {
|
|
224
|
+
// This requires understanding the full task structure - manual fix required
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
// 6. Missing scenario - cannot auto-fix (needs content)
|
|
228
|
+
if (issue.message.includes('must have at least one scenario')) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return fixed;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Add a section skeleton to proposal.md
|
|
236
|
+
*/
|
|
237
|
+
async addProposalSectionSkeleton(changeDir, sectionName) {
|
|
238
|
+
const proposalPath = path.join(changeDir, 'proposal.md');
|
|
239
|
+
try {
|
|
240
|
+
let content = await fs.readFile(proposalPath, 'utf-8');
|
|
241
|
+
const skeletons = {
|
|
242
|
+
'Why/变更原因': '\n\n## 变更原因\n\n[待补充:问题背景、解决动机]\n',
|
|
243
|
+
'What Changes/变更内容': '\n\n## 变更内容\n\n- [待补充:具体改动点]\n',
|
|
244
|
+
'Impact/影响范围': '\n\n## 影响范围\n\n- Affected specs: [待补充]\n- Affected code: [待补充]\n',
|
|
245
|
+
};
|
|
246
|
+
// Try to match the section name
|
|
247
|
+
let skeleton = skeletons[sectionName];
|
|
248
|
+
if (!skeleton) {
|
|
249
|
+
// Try partial match
|
|
250
|
+
for (const key of Object.keys(skeletons)) {
|
|
251
|
+
if (sectionName.includes(key.split('/')[0]) || sectionName.includes(key.split('/')[1])) {
|
|
252
|
+
skeleton = skeletons[key];
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!skeleton) {
|
|
258
|
+
skeleton = `\n\n## ${sectionName}\n\n[待补充]\n`;
|
|
259
|
+
}
|
|
260
|
+
content = content.trimEnd() + skeleton;
|
|
261
|
+
await fs.writeFile(proposalPath, content, 'utf-8');
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Cannot read/write proposal.md
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Add review gate section to tasks.md
|
|
269
|
+
*/
|
|
270
|
+
async addReviewGateToTasks(changeDir) {
|
|
271
|
+
const tasksPath = path.join(changeDir, 'tasks.md');
|
|
272
|
+
try {
|
|
273
|
+
let content = await fs.readFile(tasksPath, 'utf-8');
|
|
274
|
+
if (!content.includes('zhuanspec review') && !content.includes('审查门禁')) {
|
|
275
|
+
content = content.trimEnd() + '\n\n## 审查门禁\n\n- [ ] 执行 `zhuanspec review <change-id>` 并确认通过\n';
|
|
276
|
+
await fs.writeFile(tasksPath, content, 'utf-8');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// Cannot read/write tasks.md
|
|
281
|
+
}
|
|
282
|
+
}
|
|
132
283
|
printReport(type, id, report, durationMs, json, taskReport, opts) {
|
|
133
284
|
if (json) {
|
|
134
285
|
const out = {
|
package/dist/core/archive.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export declare class ArchiveCommand {
|
|
|
31
31
|
private hasGitChanges;
|
|
32
32
|
private copyDirectoryContentsIfExists;
|
|
33
33
|
private mirrorDirectory;
|
|
34
|
+
private directoryExists;
|
|
34
35
|
private findFirstDirectoryByName;
|
|
35
36
|
private resolveBusinessTemplateRoot;
|
|
36
37
|
private resolveRemoteBusinessRoot;
|
package/dist/core/archive.js
CHANGED
|
@@ -7,6 +7,8 @@ import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progre
|
|
|
7
7
|
import { Validator } from './validation/validator.js';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { findSpecUpdates, buildUpdatedSpec, writeUpdatedSpec, } from './specs-apply.js';
|
|
10
|
+
import { preArchiveHook } from './hooks/pre-archive.js';
|
|
11
|
+
import { postArchiveHook } from './hooks/post-archive.js';
|
|
10
12
|
const ARCH_REPO_URL = 'http://gitlab.zhuanspirit.com/zz-kf/spec_repo.git';
|
|
11
13
|
const ARCH_REPO_BRANCH = 'spec_repo-feature-6612-2';
|
|
12
14
|
export class ArchiveCommand {
|
|
@@ -27,6 +29,7 @@ export class ArchiveCommand {
|
|
|
27
29
|
if (!businessDirection) {
|
|
28
30
|
throw new Error('未解析到业务方向,无法执行归档前拉取与归档后回推。请使用 --business-direction 或在 zhuanspec/.business-direction、zhuanspec/project.md 中配置。');
|
|
29
31
|
}
|
|
32
|
+
console.log(chalk.gray(`业务方向:${businessDirection}`));
|
|
30
33
|
await this.refreshBusinessTemplateFromRemote(zhuanspecDir, businessDirection);
|
|
31
34
|
// Get change name interactively if not provided
|
|
32
35
|
if (!changeName) {
|
|
@@ -153,6 +156,8 @@ export class ArchiveCommand {
|
|
|
153
156
|
console.log(`警告:发现 ${incompleteTasks} 个未完成的任务。由于 --yes 标志,继续执行。`);
|
|
154
157
|
}
|
|
155
158
|
}
|
|
159
|
+
// Single source of truth for archive gate: preArchiveHook (review/tests/spec-code consistency)
|
|
160
|
+
await preArchiveHook({ change: changeName, json: false });
|
|
156
161
|
// Handle spec updates unless skipSpecs flag is set
|
|
157
162
|
if (options.skipSpecs) {
|
|
158
163
|
console.log('跳过规范更新(已提供 --skip-specs 标志)。');
|
|
@@ -245,7 +250,17 @@ export class ArchiveCommand {
|
|
|
245
250
|
await this.moveDirectory(changeDir, archivePath);
|
|
246
251
|
console.log(`变更 '${changeName}' 已归档为 '${archiveName}'。`);
|
|
247
252
|
}
|
|
253
|
+
// Run post-archive hook (knowledge collection) before pushing to remote
|
|
254
|
+
console.log(chalk.cyan('\n━━━ 知识收集 ━━━'));
|
|
255
|
+
await postArchiveHook({ archived: archiveName, json: false });
|
|
256
|
+
// Push archive result (specs + archive + knowledge) to remote
|
|
257
|
+
console.log(chalk.cyan('\n━━━ 推送到远端 ━━━'));
|
|
248
258
|
await this.pushArchiveResultToRemote(zhuanspecDir, businessDirection);
|
|
259
|
+
// Output feedback link after archive completes
|
|
260
|
+
console.log(chalk.cyan('\n━━━ 使用反馈 ━━━'));
|
|
261
|
+
console.log(chalk.yellow(' 请填写使用反馈,帮助我们改进 ZhuanSpec 工作流:'));
|
|
262
|
+
console.log(chalk.blue(' 📋 https://doc.weixin.qq.com/forms/AJ4AfQfgAAwACwAAwaVABsCNqDXIKn8sf'));
|
|
263
|
+
console.log(chalk.green('\n✅ 归档完成!'));
|
|
249
264
|
}
|
|
250
265
|
/**
|
|
251
266
|
* Move a directory, falling back to copy+remove when rename fails
|
|
@@ -486,8 +501,11 @@ export class ArchiveCommand {
|
|
|
486
501
|
return null;
|
|
487
502
|
}
|
|
488
503
|
async refreshBusinessTemplateFromRemote(zhuanspecDir, businessDirection) {
|
|
504
|
+
console.log(chalk.cyan('\n━━━ 拉取远端模板 ━━━'));
|
|
505
|
+
console.log(chalk.gray(' → 克隆远端模板仓库...'));
|
|
489
506
|
const tempRepo = this.cloneRemoteRepoToTemp();
|
|
490
507
|
try {
|
|
508
|
+
console.log(chalk.gray(' → 解析远端业务目录...'));
|
|
491
509
|
const remoteBusiness = this.resolveRemoteBusinessRoot(tempRepo, businessDirection);
|
|
492
510
|
if (!remoteBusiness) {
|
|
493
511
|
throw new Error(`未在远端 specs 目录中找到业务方向 '${businessDirection}',归档已中止。`);
|
|
@@ -497,17 +515,21 @@ export class ArchiveCommand {
|
|
|
497
515
|
if (!templateRoot) {
|
|
498
516
|
throw new Error(`未在业务方向 '${businessDirection}' 目录下递归找到可用的 zhuanspec 目录,归档已中止。`);
|
|
499
517
|
}
|
|
518
|
+
console.log(chalk.gray(' → 同步 specs 目录...'));
|
|
500
519
|
await this.copyDirectoryContentsIfExists(path.join(templateRoot, 'specs'), path.join(zhuanspecDir, 'specs'));
|
|
520
|
+
console.log(chalk.gray(' → 同步 changes 目录...'));
|
|
501
521
|
await this.copyDirectoryContentsIfExists(path.join(templateRoot, 'changes'), path.join(zhuanspecDir, 'changes'));
|
|
502
|
-
console.log(chalk.
|
|
522
|
+
console.log(chalk.green(`✅ 已拉取 ${businessDirection} 业务模板(specs/changes)。`));
|
|
503
523
|
}
|
|
504
524
|
finally {
|
|
505
525
|
rmSync(tempRepo, { recursive: true, force: true });
|
|
506
526
|
}
|
|
507
527
|
}
|
|
508
528
|
async pushArchiveResultToRemote(zhuanspecDir, businessDirection) {
|
|
529
|
+
console.log(chalk.gray(' → 克隆远端模板仓库...'));
|
|
509
530
|
const tempRepo = this.cloneRemoteRepoToTemp();
|
|
510
531
|
try {
|
|
532
|
+
console.log(chalk.gray(' → 解析远端业务目录...'));
|
|
511
533
|
const remoteBusiness = this.resolveRemoteBusinessRoot(tempRepo, businessDirection);
|
|
512
534
|
if (!remoteBusiness) {
|
|
513
535
|
throw new Error(`未在远端 specs 目录中找到业务方向 '${businessDirection}',归档结果回推已中止。`);
|
|
@@ -520,20 +542,31 @@ export class ArchiveCommand {
|
|
|
520
542
|
const remoteBase = templateRoot;
|
|
521
543
|
const remoteSpecsPath = path.join(remoteBase, 'specs');
|
|
522
544
|
const remoteArchivePath = path.join(remoteBase, 'changes', 'archive');
|
|
545
|
+
const remoteKnowledgePath = path.join(remoteBase, 'knowledge');
|
|
523
546
|
const localSpecsPath = path.join(zhuanspecDir, 'specs');
|
|
524
547
|
const localArchivePath = path.join(zhuanspecDir, 'changes', 'archive');
|
|
548
|
+
const localKnowledgePath = path.join(zhuanspecDir, 'knowledge');
|
|
549
|
+
console.log(chalk.gray(' → 同步 specs 目录...'));
|
|
525
550
|
await this.mirrorDirectory(localSpecsPath, remoteSpecsPath);
|
|
551
|
+
console.log(chalk.gray(' → 同步 archive 目录...'));
|
|
526
552
|
await this.mirrorDirectory(localArchivePath, remoteArchivePath);
|
|
553
|
+
// Knowledge directory may not exist on first archive, sync only if exists
|
|
554
|
+
if (await this.directoryExists(localKnowledgePath)) {
|
|
555
|
+
console.log(chalk.gray(' → 同步 knowledge 目录...'));
|
|
556
|
+
await this.mirrorDirectory(localKnowledgePath, remoteKnowledgePath);
|
|
557
|
+
}
|
|
527
558
|
const changed = this.hasGitChanges(tempRepo);
|
|
528
559
|
if (!changed) {
|
|
529
560
|
console.log(chalk.gray('远端模板仓库无变更,跳过 push。'));
|
|
530
561
|
return;
|
|
531
562
|
}
|
|
563
|
+
console.log(chalk.gray(' → 提交变更...'));
|
|
532
564
|
this.git(tempRepo, 'git add .');
|
|
533
|
-
const commitMsg = `chore: sync archive outputs for ${businessDirection}`;
|
|
565
|
+
const commitMsg = `chore: sync archive outputs (specs/archive/knowledge) for ${businessDirection}`;
|
|
534
566
|
this.git(tempRepo, `git commit -m "${commitMsg}"`);
|
|
567
|
+
console.log(chalk.gray(' → 推送到远端...'));
|
|
535
568
|
this.git(tempRepo, `git push origin ${ARCH_REPO_BRANCH}`);
|
|
536
|
-
console.log(chalk.green(
|
|
569
|
+
console.log(chalk.green(`✅ 已推送归档结果到远端 ${ARCH_REPO_BRANCH}(${businessDirection})。`));
|
|
537
570
|
}
|
|
538
571
|
finally {
|
|
539
572
|
rmSync(tempRepo, { recursive: true, force: true });
|
|
@@ -599,6 +632,15 @@ export class ArchiveCommand {
|
|
|
599
632
|
await fs.mkdir(path.dirname(targetDir), { recursive: true });
|
|
600
633
|
await fs.cp(sourceDir, targetDir, { recursive: true, force: true });
|
|
601
634
|
}
|
|
635
|
+
async directoryExists(dirPath) {
|
|
636
|
+
try {
|
|
637
|
+
const stat = await fs.stat(dirPath);
|
|
638
|
+
return stat.isDirectory();
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
602
644
|
findFirstDirectoryByName(searchRoot, targetName) {
|
|
603
645
|
if (!existsSync(searchRoot)) {
|
|
604
646
|
return null;
|
|
@@ -69,6 +69,73 @@ export const COMMAND_REGISTRY = [
|
|
|
69
69
|
description: 'Display an interactive dashboard of specs and changes',
|
|
70
70
|
flags: [],
|
|
71
71
|
},
|
|
72
|
+
{
|
|
73
|
+
name: 'techDesign',
|
|
74
|
+
description: 'Prepare technical design generation request (techDesign phase, independent before proposal by default)',
|
|
75
|
+
acceptsPositional: true,
|
|
76
|
+
positionalType: 'change-id',
|
|
77
|
+
flags: [
|
|
78
|
+
{
|
|
79
|
+
name: 'change',
|
|
80
|
+
description: 'Optionally bind generated design source to a change',
|
|
81
|
+
takesValue: true,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'desc',
|
|
85
|
+
description: 'Requirement description used for technical design generation',
|
|
86
|
+
takesValue: true,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'dashen-page-id',
|
|
90
|
+
description: 'Dashen pageId used as requirement source',
|
|
91
|
+
takesValue: true,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'dashen-url',
|
|
95
|
+
description: 'Dashen document URL used as requirement source',
|
|
96
|
+
takesValue: true,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'output',
|
|
100
|
+
description: 'Output path of generated request markdown',
|
|
101
|
+
takesValue: true,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'init-template',
|
|
105
|
+
description: 'Generate empty design.md template inside a change (legacy mode)',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'force',
|
|
109
|
+
description: 'Overwrite existing output file or bound design.md',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'design',
|
|
115
|
+
description: 'Deprecated alias of techDesign',
|
|
116
|
+
acceptsPositional: true,
|
|
117
|
+
positionalType: 'change-id',
|
|
118
|
+
flags: [],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'review',
|
|
122
|
+
description: 'Run ZhuanSpec review and unit test verification',
|
|
123
|
+
acceptsPositional: true,
|
|
124
|
+
positionalType: 'change-id',
|
|
125
|
+
flags: [
|
|
126
|
+
COMMON_FLAGS.json,
|
|
127
|
+
{
|
|
128
|
+
name: 'test-command',
|
|
129
|
+
description: 'Test command to execute (default: npm test)',
|
|
130
|
+
takesValue: true,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'coverage-threshold',
|
|
134
|
+
description: 'Coverage threshold percentage',
|
|
135
|
+
takesValue: true,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
72
139
|
{
|
|
73
140
|
name: 'validate',
|
|
74
141
|
description: 'Validate changes and specs',
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { SlashCommandConfigurator } from './base.js';
|
|
2
2
|
const FILE_PATHS = {
|
|
3
3
|
proposal: '.amazonq/prompts/zhuanspec-proposal.md',
|
|
4
|
+
design: '.amazonq/prompts/zhuanspec-design.md',
|
|
4
5
|
apply: '.amazonq/prompts/zhuanspec-apply.md',
|
|
5
|
-
|
|
6
|
+
review: '.amazonq/prompts/zhuanspec-review.md',
|
|
7
|
+
archive: '.amazonq/prompts/zhuanspec-archive.md',
|
|
8
|
+
knowledge: '.amazonq/prompts/zhuanspec-knowledge.md'
|
|
6
9
|
};
|
|
7
10
|
const FRONTMATTER = {
|
|
8
11
|
proposal: `---
|
|
@@ -14,12 +17,30 @@ The user has requested the following change proposal. Use the zhuanspec instruct
|
|
|
14
17
|
<UserRequest>
|
|
15
18
|
$ARGUMENTS
|
|
16
19
|
</UserRequest>`,
|
|
20
|
+
design: `---
|
|
21
|
+
description: 在 proposal 之前独立生成技术设计请求文档。
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
The user wants to generate a technical design request. Use the zhuanspec instructions to create the design document.
|
|
25
|
+
|
|
26
|
+
<Input>
|
|
27
|
+
$ARGUMENTS
|
|
28
|
+
</Input>`,
|
|
17
29
|
apply: `---
|
|
18
30
|
description: 实施已批准的 ZhuanSpec 变更并保持任务同步。
|
|
19
31
|
---
|
|
20
32
|
|
|
21
33
|
The user wants to apply the following change. Use the zhuanspec instructions to implement the approved change.
|
|
22
34
|
|
|
35
|
+
<ChangeId>
|
|
36
|
+
$ARGUMENTS
|
|
37
|
+
</ChangeId>`,
|
|
38
|
+
review: `---
|
|
39
|
+
description: 在归档前执行代码审查和单元测试验证。
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
The user wants to review the following change. Use the zhuanspec instructions to run code review and unit test verification.
|
|
43
|
+
|
|
23
44
|
<ChangeId>
|
|
24
45
|
$ARGUMENTS
|
|
25
46
|
</ChangeId>`,
|
|
@@ -31,7 +52,16 @@ The user wants to archive the following deployed change. Use the zhuanspec instr
|
|
|
31
52
|
|
|
32
53
|
<ChangeId>
|
|
33
54
|
$ARGUMENTS
|
|
34
|
-
</ChangeId
|
|
55
|
+
</ChangeId>`,
|
|
56
|
+
knowledge: `---
|
|
57
|
+
description: 管理项目知识库(最佳实践、陷阱、隐式约定)。
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
The user wants to manage the project knowledge base. Use the zhuanspec instructions to view, add, search, or sync knowledge entries.
|
|
61
|
+
|
|
62
|
+
<Operation>
|
|
63
|
+
$ARGUMENTS
|
|
64
|
+
</Operation>`
|
|
35
65
|
};
|
|
36
66
|
export class AmazonQSlashCommandConfigurator extends SlashCommandConfigurator {
|
|
37
67
|
toolId = 'amazon-q';
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { SlashCommandConfigurator } from './base.js';
|
|
2
2
|
const FILE_PATHS = {
|
|
3
3
|
proposal: '.agent/workflows/zhuanspec-proposal.md',
|
|
4
|
+
design: '.agent/workflows/zhuanspec-design.md',
|
|
4
5
|
apply: '.agent/workflows/zhuanspec-apply.md',
|
|
5
|
-
|
|
6
|
+
review: '.agent/workflows/zhuanspec-review.md',
|
|
7
|
+
archive: '.agent/workflows/zhuanspec-archive.md',
|
|
8
|
+
knowledge: '.agent/workflows/zhuanspec-knowledge.md'
|
|
6
9
|
};
|
|
7
10
|
const DESCRIPTIONS = {
|
|
8
11
|
proposal: '搭建新的 ZhuanSpec 变更并严格验证。',
|
|
12
|
+
design: '在 proposal 之前独立生成技术设计请求文档。',
|
|
9
13
|
apply: '实施已批准的 ZhuanSpec 变更并保持任务同步。',
|
|
10
|
-
|
|
14
|
+
review: '在归档前执行代码审查和单元测试验证。',
|
|
15
|
+
archive: '归档已部署的 ZhuanSpec 变更并更新规范。',
|
|
16
|
+
knowledge: '管理项目知识库(最佳实践、陷阱、隐式约定)。'
|
|
11
17
|
};
|
|
12
18
|
export class AntigravitySlashCommandConfigurator extends SlashCommandConfigurator {
|
|
13
19
|
toolId = 'antigravity';
|
|
@@ -1,21 +1,36 @@
|
|
|
1
1
|
import { SlashCommandConfigurator } from './base.js';
|
|
2
2
|
const FILE_PATHS = {
|
|
3
3
|
proposal: '.augment/commands/zhuanspec-proposal.md',
|
|
4
|
+
design: '.augment/commands/zhuanspec-design.md',
|
|
4
5
|
apply: '.augment/commands/zhuanspec-apply.md',
|
|
5
|
-
|
|
6
|
+
review: '.augment/commands/zhuanspec-review.md',
|
|
7
|
+
archive: '.augment/commands/zhuanspec-archive.md',
|
|
8
|
+
knowledge: '.augment/commands/zhuanspec-knowledge.md'
|
|
6
9
|
};
|
|
7
10
|
const FRONTMATTER = {
|
|
8
11
|
proposal: `---
|
|
9
12
|
description: 搭建新的 ZhuanSpec 变更并严格验证。
|
|
10
13
|
argument-hint: feature description or request
|
|
14
|
+
---`,
|
|
15
|
+
design: `---
|
|
16
|
+
description: 在 proposal 之前独立生成技术设计请求文档。
|
|
17
|
+
argument-hint: dashen-page-id or description
|
|
11
18
|
---`,
|
|
12
19
|
apply: `---
|
|
13
20
|
description: 实施已批准的 ZhuanSpec 变更并保持任务同步。
|
|
14
21
|
argument-hint: change-id
|
|
22
|
+
---`,
|
|
23
|
+
review: `---
|
|
24
|
+
description: 在归档前执行代码审查和单元测试验证。
|
|
25
|
+
argument-hint: change-id
|
|
15
26
|
---`,
|
|
16
27
|
archive: `---
|
|
17
28
|
description: 归档已部署的 ZhuanSpec 变更并更新规范。
|
|
18
29
|
argument-hint: change-id
|
|
30
|
+
---`,
|
|
31
|
+
knowledge: `---
|
|
32
|
+
description: 管理项目知识库(最佳实践、陷阱、隐式约定)。
|
|
33
|
+
argument-hint: operation (view, add, search, sync)
|
|
19
34
|
---`
|
|
20
35
|
};
|
|
21
36
|
export class AuggieSlashCommandConfigurator extends SlashCommandConfigurator {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FileSystemUtils } from '../../../utils/file-system.js';
|
|
2
2
|
import { TemplateManager } from '../../templates/index.js';
|
|
3
3
|
import { ZHUANSPEC_MARKERS } from '../../config.js';
|
|
4
|
-
const ALL_COMMANDS = ['proposal', 'apply', 'archive'];
|
|
4
|
+
const ALL_COMMANDS = ['proposal', 'design', 'apply', 'review', 'archive', 'knowledge'];
|
|
5
5
|
export class SlashCommandConfigurator {
|
|
6
6
|
getTargets() {
|
|
7
7
|
return ALL_COMMANDS.map((id) => ({
|
|
@@ -5,5 +5,9 @@ export declare class ClaudeSlashCommandConfigurator extends SlashCommandConfigur
|
|
|
5
5
|
readonly isAvailable = true;
|
|
6
6
|
protected getRelativePath(id: SlashCommandId): string;
|
|
7
7
|
protected getFrontmatter(id: SlashCommandId): string;
|
|
8
|
+
generateAll(projectPath: string, zhuanspecDir: string): Promise<string[]>;
|
|
9
|
+
updateExisting(projectPath: string, zhuanspecDir: string): Promise<string[]>;
|
|
10
|
+
private migrateLegacyDesignCommand;
|
|
11
|
+
private removeLegacyDesignCommand;
|
|
8
12
|
}
|
|
9
13
|
//# sourceMappingURL=claude.d.ts.map
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { SlashCommandConfigurator } from './base.js';
|
|
2
|
+
import { FileSystemUtils } from '../../../utils/file-system.js';
|
|
3
|
+
import { ZHUANSPEC_MARKERS } from '../../config.js';
|
|
4
|
+
import { promises as fs } from 'fs';
|
|
2
5
|
const FILE_PATHS = {
|
|
3
6
|
proposal: '.claude/commands/zhuanspec/proposal.md',
|
|
7
|
+
design: '.claude/commands/zhuanspec/techDesign.md',
|
|
4
8
|
apply: '.claude/commands/zhuanspec/apply.md',
|
|
5
|
-
|
|
9
|
+
review: '.claude/commands/zhuanspec/review.md',
|
|
10
|
+
archive: '.claude/commands/zhuanspec/archive.md',
|
|
11
|
+
knowledge: '.claude/commands/zhuanspec/knowledge.md'
|
|
6
12
|
};
|
|
7
13
|
const FRONTMATTER = {
|
|
8
14
|
proposal: `---
|
|
@@ -10,18 +16,36 @@ name: ZhuanSpec: Proposal
|
|
|
10
16
|
description: 搭建新的 ZhuanSpec 变更并严格验证。
|
|
11
17
|
category: ZhuanSpec
|
|
12
18
|
tags: [zhuanspec, change]
|
|
19
|
+
---`,
|
|
20
|
+
design: `---
|
|
21
|
+
name: ZhuanSpec: TechDesign
|
|
22
|
+
description: 在 proposal 之前独立生成技术设计请求文档。
|
|
23
|
+
category: ZhuanSpec
|
|
24
|
+
tags: [zhuanspec, techDesign, tech-spec]
|
|
13
25
|
---`,
|
|
14
26
|
apply: `---
|
|
15
27
|
name: ZhuanSpec: Apply
|
|
16
28
|
description: 实施已批准的 ZhuanSpec 变更并保持任务同步。
|
|
17
29
|
category: ZhuanSpec
|
|
18
30
|
tags: [zhuanspec, apply]
|
|
31
|
+
---`,
|
|
32
|
+
review: `---
|
|
33
|
+
name: ZhuanSpec: Review
|
|
34
|
+
description: 在归档前执行代码审查和单元测试验证。
|
|
35
|
+
category: ZhuanSpec
|
|
36
|
+
tags: [zhuanspec, review, code-review]
|
|
19
37
|
---`,
|
|
20
38
|
archive: `---
|
|
21
39
|
name: ZhuanSpec: Archive
|
|
22
40
|
description: 归档已部署的 ZhuanSpec 变更并更新规范。
|
|
23
41
|
category: ZhuanSpec
|
|
24
42
|
tags: [zhuanspec, archive]
|
|
43
|
+
---`,
|
|
44
|
+
knowledge: `---
|
|
45
|
+
name: ZhuanSpec: Knowledge
|
|
46
|
+
description: 管理项目知识库(最佳实践、陷阱、隐式约定)。
|
|
47
|
+
category: ZhuanSpec
|
|
48
|
+
tags: [zhuanspec, knowledge]
|
|
25
49
|
---`
|
|
26
50
|
};
|
|
27
51
|
export class ClaudeSlashCommandConfigurator extends SlashCommandConfigurator {
|
|
@@ -33,5 +57,36 @@ export class ClaudeSlashCommandConfigurator extends SlashCommandConfigurator {
|
|
|
33
57
|
getFrontmatter(id) {
|
|
34
58
|
return FRONTMATTER[id];
|
|
35
59
|
}
|
|
60
|
+
async generateAll(projectPath, zhuanspecDir) {
|
|
61
|
+
const createdOrUpdated = await super.generateAll(projectPath, zhuanspecDir);
|
|
62
|
+
await this.removeLegacyDesignCommand(projectPath);
|
|
63
|
+
return createdOrUpdated;
|
|
64
|
+
}
|
|
65
|
+
async updateExisting(projectPath, zhuanspecDir) {
|
|
66
|
+
const updated = await super.updateExisting(projectPath, zhuanspecDir);
|
|
67
|
+
await this.migrateLegacyDesignCommand(projectPath);
|
|
68
|
+
return updated;
|
|
69
|
+
}
|
|
70
|
+
async migrateLegacyDesignCommand(projectPath) {
|
|
71
|
+
const legacyPath = FileSystemUtils.joinPath(projectPath, '.claude/commands/zhuanspec/design.md');
|
|
72
|
+
const nextPath = FileSystemUtils.joinPath(projectPath, '.claude/commands/zhuanspec/techDesign.md');
|
|
73
|
+
if (!await FileSystemUtils.fileExists(legacyPath)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!await FileSystemUtils.fileExists(nextPath)) {
|
|
77
|
+
const body = this.getBody('design');
|
|
78
|
+
const frontmatter = this.getFrontmatter('design');
|
|
79
|
+
const content = `${frontmatter.trim()}\n${ZHUANSPEC_MARKERS.start}\n${body}\n${ZHUANSPEC_MARKERS.end}\n`;
|
|
80
|
+
await FileSystemUtils.writeFile(nextPath, content);
|
|
81
|
+
}
|
|
82
|
+
await fs.unlink(legacyPath).catch(() => undefined);
|
|
83
|
+
}
|
|
84
|
+
async removeLegacyDesignCommand(projectPath) {
|
|
85
|
+
const legacyPath = FileSystemUtils.joinPath(projectPath, '.claude/commands/zhuanspec/design.md');
|
|
86
|
+
if (!await FileSystemUtils.fileExists(legacyPath)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
await fs.unlink(legacyPath).catch(() => undefined);
|
|
90
|
+
}
|
|
36
91
|
}
|
|
37
92
|
//# sourceMappingURL=claude.js.map
|