@zhuan-ai/zhuanspec 2.8.0 → 2.10.0
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/dist/cli/hooks.js +8 -8
- package/dist/commands/design.js +21 -0
- package/dist/commands/review.d.ts +0 -25
- package/dist/commands/review.js +35 -405
- package/dist/core/archive.js +3 -1
- package/dist/core/hooks/collect-knowledge.d.ts +13 -5
- package/dist/core/hooks/collect-knowledge.js +56 -164
- package/dist/core/hooks/deviation-check.js +117 -55
- package/dist/core/hooks/init.js +30 -1
- package/dist/core/hooks/pre-archive.js +13 -14
- package/dist/core/hooks/record-progress.js +58 -16
- package/dist/core/hooks/review-hooks.js +5 -4
- package/dist/core/hooks/review-orchestrator.js +4 -4
- package/dist/core/init.js +29 -9
- package/dist/core/templates/slash-command-templates.js +41 -21
- package/dist/core/update.d.ts +3 -0
- package/dist/core/update.js +85 -2
- package/package.json +1 -1
|
@@ -211,38 +211,80 @@ function extractMcpTool(toolName) {
|
|
|
211
211
|
return parts.length >= 3 ? parts[2] : undefined;
|
|
212
212
|
}
|
|
213
213
|
/**
|
|
214
|
-
* Detect active change from progress.json files
|
|
215
|
-
*
|
|
216
|
-
*
|
|
214
|
+
* Detect active change from progress.json files.
|
|
215
|
+
*
|
|
216
|
+
* Strategy:
|
|
217
|
+
* 1. Primary — if filePath is under zhuanspec/changes/{changeId}/, extract changeId directly.
|
|
218
|
+
* 2. Fallback — scan all changes and return the most recently updated one
|
|
219
|
+
* (by lastUpdatedAt field; stub files without it fall back to filesystem mtime).
|
|
217
220
|
*/
|
|
218
|
-
async function detectActiveChange(cwd) {
|
|
221
|
+
async function detectActiveChange(cwd, filePath) {
|
|
219
222
|
const changesDir = path.join(cwd, 'zhuanspec', 'changes');
|
|
220
223
|
if (!await FileSystemUtils.directoryExists(changesDir)) {
|
|
221
224
|
return null;
|
|
222
225
|
}
|
|
226
|
+
// Primary: derive changeId from the file being modified
|
|
227
|
+
if (filePath) {
|
|
228
|
+
const absFilePath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
229
|
+
const changesDirAbs = path.resolve(changesDir);
|
|
230
|
+
if (absFilePath.startsWith(changesDirAbs + path.sep)) {
|
|
231
|
+
const changeId = path.relative(changesDirAbs, absFilePath).split(path.sep)[0];
|
|
232
|
+
if (changeId && changeId !== 'archive') {
|
|
233
|
+
const progressPath = path.join(changesDirAbs, changeId, 'metrics', 'progress.json');
|
|
234
|
+
if (await FileSystemUtils.fileExists(progressPath)) {
|
|
235
|
+
try {
|
|
236
|
+
const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
|
|
237
|
+
if (progress.phase && progress.phase !== 'idle' && PHASE_ORDER.includes(progress.phase)) {
|
|
238
|
+
return { changeId, phase: progress.phase };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Fall through to fallback
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Fallback: most recently updated active change
|
|
223
249
|
const entries = await fs.readdir(changesDir, { withFileTypes: true });
|
|
250
|
+
const candidates = [];
|
|
224
251
|
for (const entry of entries) {
|
|
225
252
|
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'archive')
|
|
226
253
|
continue;
|
|
227
254
|
const progressPath = path.join(changesDir, entry.name, 'metrics', 'progress.json');
|
|
228
|
-
if (await FileSystemUtils.fileExists(progressPath))
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
255
|
+
if (!await FileSystemUtils.fileExists(progressPath))
|
|
256
|
+
continue;
|
|
257
|
+
try {
|
|
258
|
+
const progress = JSON.parse(await FileSystemUtils.readFile(progressPath));
|
|
259
|
+
if (!progress.phase || progress.phase === 'idle' || !PHASE_ORDER.includes(progress.phase))
|
|
260
|
+
continue;
|
|
261
|
+
let sortKey;
|
|
262
|
+
if (progress.lastUpdatedAt) {
|
|
263
|
+
// "2026-04-22 11:39:59" (Beijing time) — replace space with T for Date parsing
|
|
264
|
+
sortKey = new Date(progress.lastUpdatedAt.replace(' ', 'T')).getTime();
|
|
265
|
+
if (isNaN(sortKey))
|
|
266
|
+
sortKey = 0;
|
|
234
267
|
}
|
|
235
|
-
|
|
236
|
-
//
|
|
268
|
+
else {
|
|
269
|
+
// Stub files (e.g. techDesign) lack lastUpdatedAt — use filesystem mtime
|
|
270
|
+
const stat = await fs.stat(progressPath);
|
|
271
|
+
sortKey = stat.mtime.getTime();
|
|
237
272
|
}
|
|
273
|
+
candidates.push({ changeId: entry.name, phase: progress.phase, sortKey });
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// Ignore parse errors
|
|
238
277
|
}
|
|
239
278
|
}
|
|
240
|
-
|
|
279
|
+
if (candidates.length === 0)
|
|
280
|
+
return null;
|
|
281
|
+
candidates.sort((a, b) => b.sortKey - a.sortKey);
|
|
282
|
+
return { changeId: candidates[0].changeId, phase: candidates[0].phase };
|
|
241
283
|
}
|
|
242
284
|
async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
243
285
|
const cwd = process.cwd();
|
|
244
|
-
// Priority: progress.json > environment variables
|
|
245
|
-
const activeChange = await detectActiveChange(cwd);
|
|
286
|
+
// Priority: filePath-derived > progress.json scan > environment variables
|
|
287
|
+
const activeChange = await detectActiveChange(cwd, filePath);
|
|
246
288
|
const phase = activeChange?.phase || process.env.ZHUANSPEC_PHASE || 'idle';
|
|
247
289
|
const changeId = activeChange?.changeId || process.env.ZHUANSPEC_CHANGE_ID || '';
|
|
248
290
|
const currentTask = process.env.ZHUANSPEC_CURRENT_TASK || '';
|
|
@@ -641,7 +683,7 @@ export async function initializeProgress(changeId, initialPhase = 'propose') {
|
|
|
641
683
|
*/
|
|
642
684
|
export async function recordHookTrigger(hookName, hookPhase, success, options) {
|
|
643
685
|
const cwd = process.cwd();
|
|
644
|
-
const activeChange = await detectActiveChange(cwd);
|
|
686
|
+
const activeChange = await detectActiveChange(cwd, options?.filePath);
|
|
645
687
|
const phase = activeChange?.phase || process.env.ZHUANSPEC_PHASE || 'idle';
|
|
646
688
|
const changeId = activeChange?.changeId || process.env.ZHUANSPEC_CHANGE_ID || '';
|
|
647
689
|
if (!changeId || phase === 'idle')
|
|
@@ -16,7 +16,6 @@ import path from 'path';
|
|
|
16
16
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
17
17
|
import { getBeijingTime } from './record-progress.js';
|
|
18
18
|
// Output file paths for skill results
|
|
19
|
-
const SKILL_OUTPUT_DIR = 'zhuanspec/.skill-outputs';
|
|
20
19
|
const CODE_REVIEW_OUTPUT = 'code-review-result.json';
|
|
21
20
|
const UNIT_TEST_OUTPUT = 'unit-test-result.json';
|
|
22
21
|
const SPEC_CONSISTENCY_OUTPUT = 'spec-consistency-result.json';
|
|
@@ -205,7 +204,7 @@ export async function specConsistencyResultCheck(changeId, skillOutput) {
|
|
|
205
204
|
// ============================================================
|
|
206
205
|
async function readSkillOutput(changeId, filename) {
|
|
207
206
|
const cwd = process.cwd();
|
|
208
|
-
const outputPath = path.join(cwd,
|
|
207
|
+
const outputPath = path.join(cwd, 'zhuanspec', 'changes', changeId, 'review', filename);
|
|
209
208
|
try {
|
|
210
209
|
if (await FileSystemUtils.fileExists(outputPath)) {
|
|
211
210
|
const content = await FileSystemUtils.readFile(outputPath);
|
|
@@ -276,7 +275,7 @@ export async function writeSpecConsistencyOutput(changeId, output) {
|
|
|
276
275
|
export { writeSkillOutput };
|
|
277
276
|
async function writeSkillOutput(changeId, filename, data) {
|
|
278
277
|
const cwd = process.cwd();
|
|
279
|
-
const outputDir = path.join(cwd,
|
|
278
|
+
const outputDir = path.join(cwd, 'zhuanspec', 'changes', changeId, 'review');
|
|
280
279
|
await FileSystemUtils.createDirectory(outputDir);
|
|
281
280
|
const outputPath = path.join(outputDir, filename);
|
|
282
281
|
await FileSystemUtils.writeFile(outputPath, JSON.stringify(data, null, 2));
|
|
@@ -337,7 +336,9 @@ ${results.codeReview.pass && results.unitTest.pass && results.specConsistency.pa
|
|
|
337
336
|
// Write report to change directory
|
|
338
337
|
const cwd = process.cwd();
|
|
339
338
|
const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeId);
|
|
340
|
-
const
|
|
339
|
+
const reviewDir = path.join(changeDir, 'review');
|
|
340
|
+
const reportPath = path.join(reviewDir, 'review-report.md');
|
|
341
|
+
await FileSystemUtils.createDirectory(reviewDir);
|
|
341
342
|
await FileSystemUtils.writeFile(reportPath, report);
|
|
342
343
|
return report;
|
|
343
344
|
}
|
|
@@ -25,7 +25,7 @@ function generateCodeReviewPrompt(changeId) {
|
|
|
25
25
|
1. 执行 code-review-expert skill 进行代码审查
|
|
26
26
|
2. 如果发现 Critical 问题,立即修复
|
|
27
27
|
3. 修复后重新检查(最多3轮)
|
|
28
|
-
4. 输出 code-review-result.json 报告到 zhuanspec
|
|
28
|
+
4. 输出 code-review-result.json 报告到 zhuanspec/changes/${changeId}/review/
|
|
29
29
|
|
|
30
30
|
## 输出格式
|
|
31
31
|
{
|
|
@@ -50,7 +50,7 @@ function generateUnitTestPrompt(changeId) {
|
|
|
50
50
|
2. 运行单测,检查通过率和覆盖率
|
|
51
51
|
3. 如果失败或不达标,立即修复
|
|
52
52
|
4. 修复后重新测试(最多3轮)
|
|
53
|
-
5. 输出 unit-test-result.json 报告到 zhuanspec
|
|
53
|
+
5. 输出 unit-test-result.json 报告到 zhuanspec/changes/${changeId}/review/
|
|
54
54
|
|
|
55
55
|
## 输出格式
|
|
56
56
|
{
|
|
@@ -76,7 +76,7 @@ function generateSpecConsistencyPrompt(changeId) {
|
|
|
76
76
|
2. 检查代码实现是否覆盖每个 Scenario
|
|
77
77
|
3. 如果发现未覆盖,立即补充代码或更新 Spec
|
|
78
78
|
4. 修复后重新检查(最多3轮)
|
|
79
|
-
5. 输出 spec-consistency-result.json 报告到 zhuanspec
|
|
79
|
+
5. 输出 spec-consistency-result.json 报告到 zhuanspec/changes/${changeId}/review/
|
|
80
80
|
|
|
81
81
|
## 输出格式
|
|
82
82
|
{
|
|
@@ -100,7 +100,7 @@ export async function orchestrateReview(options) {
|
|
|
100
100
|
const { changeId } = options;
|
|
101
101
|
// Ensure output directory exists
|
|
102
102
|
const cwd = process.cwd();
|
|
103
|
-
const outputDir = path.join(cwd, 'zhuanspec', '
|
|
103
|
+
const outputDir = path.join(cwd, 'zhuanspec', 'changes', changeId, 'review');
|
|
104
104
|
await FileSystemUtils.createDirectory(outputDir);
|
|
105
105
|
return {
|
|
106
106
|
codeReviewPrompt: generateCodeReviewPrompt(changeId),
|
package/dist/core/init.js
CHANGED
|
@@ -314,7 +314,7 @@ export class InitCommand {
|
|
|
314
314
|
}
|
|
315
315
|
const specTemplateSpinner = this.startSpinner(`正在同步 ${this.businessDirection} 业务规范模板...`);
|
|
316
316
|
const syncResult = this.syncBusinessSpecTemplate(zhuanspecPath);
|
|
317
|
-
if (syncResult.syncedSpecs || syncResult.syncedChanges) {
|
|
317
|
+
if (syncResult.syncedSpecs || syncResult.syncedChanges || syncResult.syncedKnowledge) {
|
|
318
318
|
const syncedParts = [];
|
|
319
319
|
if (syncResult.syncedSpecs) {
|
|
320
320
|
syncedParts.push('specs');
|
|
@@ -322,6 +322,9 @@ export class InitCommand {
|
|
|
322
322
|
if (syncResult.syncedChanges) {
|
|
323
323
|
syncedParts.push('changes');
|
|
324
324
|
}
|
|
325
|
+
if (syncResult.syncedKnowledge) {
|
|
326
|
+
syncedParts.push('knowledge');
|
|
327
|
+
}
|
|
325
328
|
specTemplateSpinner.stopAndPersist({
|
|
326
329
|
symbol: PALETTE.white('▌'),
|
|
327
330
|
text: PALETTE.white(`已从远端模板同步 ${syncedParts.join('、')} 目录`),
|
|
@@ -791,28 +794,29 @@ export class InitCommand {
|
|
|
791
794
|
}
|
|
792
795
|
syncBusinessSpecTemplate(zhuanspecPath) {
|
|
793
796
|
if (!this.businessDirection) {
|
|
794
|
-
return { syncedSpecs: false, syncedChanges: false };
|
|
797
|
+
return { syncedSpecs: false, syncedChanges: false, syncedKnowledge: false };
|
|
795
798
|
}
|
|
796
799
|
const tempDir = this.cloneRemoteArchitectureRepoToTemp();
|
|
797
800
|
if (!tempDir) {
|
|
798
|
-
return { syncedSpecs: false, syncedChanges: false };
|
|
801
|
+
return { syncedSpecs: false, syncedChanges: false, syncedKnowledge: false };
|
|
799
802
|
}
|
|
800
803
|
try {
|
|
801
804
|
const specsRoot = path.join(tempDir, 'specs');
|
|
802
805
|
const businessRoot = this.resolveBusinessDirectionRoot(specsRoot);
|
|
803
806
|
if (!businessRoot) {
|
|
804
|
-
return { syncedSpecs: false, syncedChanges: false };
|
|
807
|
+
return { syncedSpecs: false, syncedChanges: false, syncedKnowledge: false };
|
|
805
808
|
}
|
|
806
809
|
const templateRoot = this.findFirstTemplateSourceRoot(businessRoot);
|
|
807
810
|
if (!templateRoot) {
|
|
808
|
-
return { syncedSpecs: false, syncedChanges: false };
|
|
811
|
+
return { syncedSpecs: false, syncedChanges: false, syncedKnowledge: false };
|
|
809
812
|
}
|
|
810
813
|
const syncedSpecs = this.copyDirectoryContentsIfExists(path.join(templateRoot, 'specs'), path.join(zhuanspecPath, 'specs'));
|
|
811
814
|
const syncedChanges = this.copyDirectoryContentsIfExists(path.join(templateRoot, 'changes'), path.join(zhuanspecPath, 'changes'));
|
|
812
|
-
|
|
815
|
+
const syncedKnowledge = this.copyDirectoryContentsIfExists(path.join(templateRoot, 'knowledge'), path.join(zhuanspecPath, 'knowledge'));
|
|
816
|
+
return { syncedSpecs, syncedChanges, syncedKnowledge };
|
|
813
817
|
}
|
|
814
818
|
catch {
|
|
815
|
-
return { syncedSpecs: false, syncedChanges: false };
|
|
819
|
+
return { syncedSpecs: false, syncedChanges: false, syncedKnowledge: false };
|
|
816
820
|
}
|
|
817
821
|
finally {
|
|
818
822
|
rmSync(tempDir, { recursive: true, force: true });
|
|
@@ -1254,6 +1258,17 @@ export class InitCommand {
|
|
|
1254
1258
|
],
|
|
1255
1259
|
},
|
|
1256
1260
|
],
|
|
1261
|
+
UserPromptSubmit: [
|
|
1262
|
+
{
|
|
1263
|
+
hooks: [
|
|
1264
|
+
{
|
|
1265
|
+
type: 'command',
|
|
1266
|
+
command: 'zhuanspec-hook deviation-check --trigger post-prompt',
|
|
1267
|
+
statusMessage: 'Checking prompt vs proposal consistency...',
|
|
1268
|
+
},
|
|
1269
|
+
],
|
|
1270
|
+
},
|
|
1271
|
+
],
|
|
1257
1272
|
PreToolUse: [
|
|
1258
1273
|
{
|
|
1259
1274
|
matcher: 'Write|Edit',
|
|
@@ -1276,10 +1291,15 @@ export class InitCommand {
|
|
|
1276
1291
|
statusMessage: '📝 Recording progress...',
|
|
1277
1292
|
async: false,
|
|
1278
1293
|
},
|
|
1294
|
+
],
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
matcher: 'Write|Edit',
|
|
1298
|
+
hooks: [
|
|
1279
1299
|
{
|
|
1280
1300
|
type: 'command',
|
|
1281
|
-
command: 'zhuanspec-hook collect-knowledge',
|
|
1282
|
-
statusMessage: '
|
|
1301
|
+
command: 'zhuanspec-hook collect-knowledge --json',
|
|
1302
|
+
statusMessage: '💡 Checking for knowledge to capture...',
|
|
1283
1303
|
async: false,
|
|
1284
1304
|
},
|
|
1285
1305
|
],
|
|
@@ -6,7 +6,7 @@ const baseGuardrails = `**约束条件**
|
|
|
6
6
|
const proposalGuardrails = `${baseGuardrails}\n- **强制澄清要求**:在创建任何提案文件之前,必须首先分析用户请求,识别所有不确定或模糊的方面(范围、技术选择、优先级、验收标准等)。如果发现任何模糊之处,必须停止并使用**选项式交互**(如 \`AskQuestion\` 工具)提问,获得明确答复后才能继续。严禁在不确定的情况下自行推测、假设或创建提案。严禁要求用户手动输入大段文字来回答澄清问题。
|
|
7
7
|
- 识别任何模糊或歧义的细节,使用带预设选项的选择题在编辑文件之前询问必要的后续问题。
|
|
8
8
|
- 在提案阶段不要编写任何代码。先完成 techDesign(需求技术方案)后再创建 proposal/specs/tasks,实施在批准后的应用阶段进行。
|
|
9
|
-
- **TechDesign 目录识别**:proposal 阶段扫描 changes/ 目录,通过 progress.json 的 phase 字段识别 techDesign
|
|
9
|
+
- **TechDesign 目录识别**:proposal 阶段扫描 changes/ 目录,通过 progress.json 的 phase 字段识别 techDesign 目录,**自动复用**(无需询问用户)。
|
|
10
10
|
- **Phase 自动更新**:复用 techDesign 目录时,自动运行 \`zhuanspec progress set-phase <change-id> propose\` 更新 phase。`;
|
|
11
11
|
const proposalOutputFormat = `**输出格式要求**
|
|
12
12
|
每个步骤完成后,输出阶段性进度报告:
|
|
@@ -64,11 +64,9 @@ const proposalSteps = `**步骤**
|
|
|
64
64
|
- **TechDesign 目录识别**:
|
|
65
65
|
* 扫描 changes/ 目录下的所有变更
|
|
66
66
|
* 检查每个变更的 metrics/progress.json 的 phase 字段
|
|
67
|
-
* 如果存在 phase=techDesign
|
|
68
|
-
*
|
|
69
|
-
|
|
70
|
-
- 创建新目录
|
|
71
|
-
- 其他(手动指定)
|
|
67
|
+
* 如果存在 phase=techDesign 的目录,**直接复用该目录,无需询问用户**,并输出提示:"techDesign 阶段已提前创建提案目录,直接复用该目录"
|
|
68
|
+
* 如果存在多个 phase=techDesign 目录,复用最新创建的目录
|
|
69
|
+
* 如果不存在 phase=techDesign 目录,创建新目录
|
|
72
70
|
- **Phase 初始化**:
|
|
73
71
|
* 如果复用 techDesign 目录:运行 \`zhuanspec progress set-phase <change-id> propose\`
|
|
74
72
|
* 如果新建目录:目录创建时自动初始化 phase=propose
|
|
@@ -106,6 +104,13 @@ const proposalSteps = `**步骤**
|
|
|
106
104
|
-如果复用了techDesign创建的提案目录,需要输出'techDesign阶段已经提前创建好提案目录,我将直接在该目录生成相关提案文件'
|
|
107
105
|
3. 将变更映射为具体的功能或要求,将多范围的工作分解为具有明确关系和顺序的不同规范增量。
|
|
108
106
|
4. 创建 \`design.md\`(必选),记录技术决策、最佳实践、隐式约定。归档时会自动提取这些内容沉淀到项目知识库。
|
|
107
|
+
- **如果用户已提供详细技术方案**,必须将以下内容系统性摄取到 design.md 对应章节,不得遗漏:
|
|
108
|
+
* 接口定义(方法签名、入参/出参字段、类型、枚举值)→ 写入 \`## 接口设计\`
|
|
109
|
+
* DB schema 变更、实体类字段 → 写入 \`## 数据模型\`
|
|
110
|
+
* 流程图/时序图 → 写入 \`## 业务流程\`
|
|
111
|
+
* 分层实现说明、关键逻辑 → 写入 \`## 实现细节\`
|
|
112
|
+
* 参数校验、错误码、异常处理 → 写入 \`## 边界条件 / 异常处理\`
|
|
113
|
+
- **摄取原则**:保留原始章节标题和结构,便于 apply 阶段按章节定位;不要压缩或省略具体字段定义
|
|
109
114
|
5. 在 \`changes/<id>/specs/<capability>/spec.md\` 中起草规范增量(每个功能一个文件夹),使用 \`## ADDED|MODIFIED|REMOVED Requirements\`,每个要求至少包含一个 \`#### Scenario:\`,并在相关时交叉引用相关功能。
|
|
110
115
|
6. **发现可用 Skill 并创建 tasks.md**:
|
|
111
116
|
- 运行 \`zhuanspec skills list --json\` 获取当前环境中可用的 skill 列表及其 description
|
|
@@ -144,9 +149,7 @@ const proposalSteps = `**步骤**
|
|
|
144
149
|
* 选择"提供测试 case":
|
|
145
150
|
- 启用 **TDD 模式**:先获取测试 case,再基于 case 设计 spec 和任务
|
|
146
151
|
- **【强制】获取测试 case 的方式**:
|
|
147
|
-
*
|
|
148
|
-
* 如果用户提供 **taskId/bicId**:使用 \`@skill:zzcase-data-fetcher\` skill 通过参数获取
|
|
149
|
-
* **禁止**使用 MCP 工具 \`zzcase_get_api_report_queryBaseCase\` 获取测试 case
|
|
152
|
+
* 如果用户提供的是**链接**(格式:\`https://zzcase.zhuanspirit.com/plan/taskDetail/module/{moduleId}/task/{taskId}\`):从 URL 路径中提取 \`moduleId\`,调用 MCP 工具 \`mcp__caseweb__GET_get2\`,传入 \`moduleId\` 参数读取测试 case 数据
|
|
150
153
|
- 或手动录入测试 case
|
|
151
154
|
- **【强制】创建 test-cases.md 文件**:
|
|
152
155
|
* 使用 \`getTestCasesTemplate\` 模板(从 \`src/core/templates/index.ts\` 导出)
|
|
@@ -228,11 +231,13 @@ const applySteps = `**步骤**
|
|
|
228
231
|
→ **Agent 类型选择**:根据任务是否标注 \`@test-case\` 选择:
|
|
229
232
|
* 无 \`@test-case\` 标注 → 使用 \`applyAgent\`(普通代码实施)
|
|
230
233
|
* 有 \`@test-case:TC-XXX\` 标注 → 使用 \`tddApplyAgent\`(TDD 模式)
|
|
231
|
-
→ **
|
|
234
|
+
→ **Subagent 上下文注入(最小必要原则)**:编排 Agent 在每个 subagent prompt 中只注入:
|
|
235
|
+
* 当前任务描述(task 文本 + @skill + @test-case 标注)
|
|
236
|
+
* 变更目录路径:\`changes/<change-id>/\`(agent 按需 Read)
|
|
237
|
+
* 提案文件清单(列出目录下存在哪些文件,agent 知道可以读什么)
|
|
232
238
|
* Rules:根据任务的 \`@skill\` 标注选择性注入 \`api-design.md\`、\`dao-standards.md\`、\`coding-standards.md\` 等
|
|
233
239
|
* Knowledge:始终注入 \`knowledge/index.md\` 知识索引摘要
|
|
234
|
-
|
|
235
|
-
→ Subagent 上下文隔离:仅注入当前任务、关联 skill、相关 spec、Rules/Knowledge
|
|
240
|
+
→ **不预先注入提案文件全文**,由 subagent 按需 Read,避免无关文件消耗 token
|
|
236
241
|
- **如果 tasks.md 无依赖声明**:
|
|
237
242
|
→ 按顺序执行(见下方步骤 3-5)
|
|
238
243
|
3. **按顺序实施任务**(无依赖声明时)- 保持编辑最小化并专注于请求的变更。如果任务标注了 \`@skill:<skill-name>\`,直接调用对应 skill 获取指导后再实施。
|
|
@@ -242,10 +247,12 @@ const applySteps = `**步骤**
|
|
|
242
247
|
4. **确认完成** - 在更新状态之前确保 \`tasks.md\` 中的每个项目都已完成。
|
|
243
248
|
5. **更新清单** - 所有工作完成后,将每个任务设置为 \`- [x]\`,以便列表反映实际情况。
|
|
244
249
|
6. **处理 Subagent 状态报告**(Wave 并行执行时):
|
|
245
|
-
-
|
|
246
|
-
- **
|
|
247
|
-
- **
|
|
248
|
-
- **
|
|
250
|
+
- **必须转发进度汇报**:每个 subagent 完成后,将其输出的 \`━━━ ✅ 任务完成\` 进度块原文转发输出给用户
|
|
251
|
+
- **DONE**:转发进度汇报 → 继续下一任务
|
|
252
|
+
- **DONE_WITH_CONCERNS**:转发进度汇报 → 记录 concerns 并评估是否需要修复
|
|
253
|
+
- **BLOCKED**:转发进度汇报 → 评估 blocker 类型并决定处理策略(提供上下文/换更强模型/拆分任务)
|
|
254
|
+
- **NEEDS_CONTEXT**:转发进度汇报 → 提供缺失信息并重新启动 subagent
|
|
255
|
+
- 所有任务报告文件均保存在 \`changes/<change-id>/reports/\` 目录,可告知用户按 task-id 查阅
|
|
249
256
|
8. 需要额外上下文时,参考 \`zhuanspec list\` 或 \`zhuanspec show <item>\`。
|
|
250
257
|
9. **触发审查门禁** - 所有任务完成后选项式提问用户是否进入 Review 阶段`;
|
|
251
258
|
const applyGuardrails = `${baseGuardrails}\n- **Wave 并行执行**:当 tasks.md 包含 \`@depends\` 时,按 Wave 编号串行执行,Wave 内任务通过 Agent tool 并行启动 subagent。
|
|
@@ -339,7 +346,7 @@ const designReferences = `**参考**
|
|
|
339
346
|
- 使用 \`zhuanspec techDesign --help\` 查看 CLI 命令选项(用于生成请求文档或验证现有设计)`;
|
|
340
347
|
const reviewGuardrails = `${baseGuardrails}\n- **门禁审查阶段**:review 命令用于执行三轨并行校验并汇总结论。
|
|
341
348
|
- **强制审查**:所有变更在归档前必须通过三轨门禁,确保代码质量、测试覆盖与 Spec-Code 一致性。
|
|
342
|
-
- **Skill 触发要求**:必须触发 \`code-review-expert\`(代码审查轨)与 \`generate-mockito-unit-test
|
|
349
|
+
- **Skill 触发要求**:必须触发 \`code-review-expert\`(代码审查轨)与 \`generate-mockito-unit-test\`(单测轨)。`;
|
|
343
350
|
const reviewSteps = `**步骤**
|
|
344
351
|
1. **确定变更 ID 并设置 Phase**:
|
|
345
352
|
- 如果此提示已包含特定的变更 ID,请使用该值
|
|
@@ -349,15 +356,28 @@ const reviewSteps = `**步骤**
|
|
|
349
356
|
- **轨道 A:Code Review 轨**
|
|
350
357
|
- 使用 Skill 工具调用 \`code-review-expert\` skill
|
|
351
358
|
- 输出 \`code-review-report.md\`,记录 Critical/Important 问题
|
|
359
|
+
- skill 完成后,**AI 将结果写入** \`changes/<id>/review/code-review-result.json\`,格式:
|
|
360
|
+
\`\`\`json
|
|
361
|
+
{ "criticalCount": 0, "importantCount": 0, "infoCount": 0, "issues": [], "sonarStatus": "skip", "loopCount": 0 }
|
|
362
|
+
\`\`\`
|
|
352
363
|
- **轨道 B:Unit Test 轨(Skill)**
|
|
353
|
-
- 使用 Skill 工具调用 \`generate-mockito-unit-test
|
|
364
|
+
- 使用 Skill 工具调用 \`generate-mockito-unit-test\`
|
|
354
365
|
- 对缺失场景补齐单测并执行测试命令,输出单测结果
|
|
366
|
+
- skill 完成后,**AI 将结果写入** \`changes/<id>/review/unit-test-result.json\`,格式:
|
|
367
|
+
\`\`\`json
|
|
368
|
+
{ "testPassed": true, "passRate": 100, "coverage": 85, "coverageThreshold": 80, "newTestsGenerated": [], "failedTests": [], "loopCount": 0 }
|
|
369
|
+
\`\`\`
|
|
355
370
|
- **轨道 C:Spec-Code 一致性轨**
|
|
356
371
|
- 运行 \`zhuanspec validate <change-id> --strict\`
|
|
357
372
|
- 确认 \`spec-code-consistent\` 规则通过
|
|
373
|
+
- validate 完成后,**AI 将结果写入** \`changes/<id>/review/spec-consistency-result.json\`,格式:
|
|
374
|
+
\`\`\`json
|
|
375
|
+
{ "consistencyRate": 100, "totalRequirements": 0, "totalScenarios": 0, "coveredScenarios": 0, "uncoveredScenarios": [], "mapping": [], "loopCount": 0 }
|
|
376
|
+
\`\`\`
|
|
358
377
|
3. **汇总执行 CLI Review**:
|
|
359
378
|
- 运行 \`zhuanspec review <change-id>\`
|
|
360
|
-
- CLI
|
|
379
|
+
- CLI 读取三个 JSON,生成 \`changes/<id>/review/review-report.md\`
|
|
380
|
+
- 如果 skill JSON 未写入,CLI 将打印缺失文件列表并退出
|
|
361
381
|
4. **审查结果处理**:
|
|
362
382
|
- **PASS**:三轨全部通过(CR=PASS、UT=PASS、Spec-Code=PASS)且 Critical=0,可以继续归档
|
|
363
383
|
- **FAIL**:输出问题列表,需要修复后重新审查
|
|
@@ -369,9 +389,9 @@ const reviewSteps = `**步骤**
|
|
|
369
389
|
- 提示可以使用 \`/zhuanspec:archive\` 进行归档`;
|
|
370
390
|
const reviewReferences = `**参考**
|
|
371
391
|
- 使用 \`zhuanspec review --help\` 查看完整选项
|
|
372
|
-
-
|
|
392
|
+
- 审查报告文件均位于 \`changes/<id>/review/\` 目录下:\`code-review-result.json\`、\`unit-test-result.json\`、\`spec-consistency-result.json\`、\`review-report.md\`
|
|
373
393
|
- 单元测试覆盖率阈值默认 80%,可通过 \`--coverage-threshold\` 调整
|
|
374
|
-
- \`code-review-expert\` Skill
|
|
394
|
+
- \`code-review-expert\` Skill 结果由 AI 写入 \`changes/<id>/review/code-review-result.json\`,\`generate-mockito-unit-test\` Skill 结果由 AI 写入 \`changes/<id>/review/unit-test-result.json\``;
|
|
375
395
|
const knowledgeGuardrails = `${baseGuardrails}\n- **知识管理阶段**:knowledge 命令用于管理项目级知识库(最佳实践、陷阱、隐式约定)。
|
|
376
396
|
- **跨变更积累**:知识不绑定单个变更,是长期积累的项目资产。
|
|
377
397
|
- **职责分离**:
|
package/dist/core/update.d.ts
CHANGED
package/dist/core/update.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { mkdtempSync, rmSync, readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
5
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
3
6
|
import { ZHUANSPEC_DIR_NAME } from './config.js';
|
|
4
7
|
import { ToolRegistry } from './configurators/registry.js';
|
|
5
8
|
import { SlashCommandRegistry } from './configurators/slash/registry.js';
|
|
6
9
|
import { agentsTemplate } from './templates/agents-template.js';
|
|
10
|
+
const ARCH_REPO_URL = 'http://gitlab.zhuanspirit.com/zz-kf/spec_repo.git';
|
|
11
|
+
const ARCH_REPO_BRANCH = 'spec_repo-feature-6612-2';
|
|
7
12
|
export class UpdateCommand {
|
|
8
13
|
async execute(projectPath) {
|
|
9
14
|
const resolvedProjectPath = path.resolve(projectPath);
|
|
@@ -59,6 +64,8 @@ export class UpdateCommand {
|
|
|
59
64
|
console.error(`更新 ${slashConfigurator.toolId} 的斜杠命令失败:${error instanceof Error ? error.message : String(error)}`);
|
|
60
65
|
}
|
|
61
66
|
}
|
|
67
|
+
// 4. Sync .claude/ from remote (rules, skills, agents)
|
|
68
|
+
const claudeSyncResult = this.syncDotClaudeFromRemote(resolvedProjectPath);
|
|
62
69
|
const summaryParts = [];
|
|
63
70
|
const instructionFiles = ['zhuanspec/AGENTS.md'];
|
|
64
71
|
if (updatedFiles.includes('AGENTS.md')) {
|
|
@@ -70,10 +77,24 @@ export class UpdateCommand {
|
|
|
70
77
|
summaryParts.push(`已更新 AI 工具文件:${aiToolFiles.join(', ')}`);
|
|
71
78
|
}
|
|
72
79
|
if (updatedSlashFiles.length > 0) {
|
|
73
|
-
// Normalize to forward slashes for cross-platform log consistency
|
|
74
80
|
const normalized = updatedSlashFiles.map((p) => FileSystemUtils.toPosixPath(p));
|
|
75
81
|
summaryParts.push(`已更新斜杠命令:${normalized.join(', ')}`);
|
|
76
82
|
}
|
|
83
|
+
if (claudeSyncResult.synced) {
|
|
84
|
+
const { added, overwritten, skipped } = claudeSyncResult;
|
|
85
|
+
const total = added + overwritten + skipped;
|
|
86
|
+
const parts = [];
|
|
87
|
+
if (added > 0)
|
|
88
|
+
parts.push(`新增 ${added} 个`);
|
|
89
|
+
if (overwritten > 0)
|
|
90
|
+
parts.push(`更新 ${overwritten} 个`);
|
|
91
|
+
if (skipped > 0)
|
|
92
|
+
parts.push(`跳过 ${skipped} 个`);
|
|
93
|
+
summaryParts.push(`已同步 .claude/ 远端内容(${total} 个文件,${parts.join(',')})`);
|
|
94
|
+
}
|
|
95
|
+
else if (claudeSyncResult.attempted) {
|
|
96
|
+
summaryParts.push('同步 .claude/ 远端内容失败(网络不可用或权限不足)');
|
|
97
|
+
}
|
|
77
98
|
const failedItems = [
|
|
78
99
|
...failedFiles,
|
|
79
100
|
...failedSlashTools.map((toolId) => `斜杠命令刷新(${toolId})`),
|
|
@@ -82,7 +103,69 @@ export class UpdateCommand {
|
|
|
82
103
|
summaryParts.push(`更新失败:${failedItems.join(', ')}`);
|
|
83
104
|
}
|
|
84
105
|
console.log(summaryParts.join(' | '));
|
|
85
|
-
|
|
106
|
+
}
|
|
107
|
+
syncDotClaudeFromRemote(projectPath) {
|
|
108
|
+
const tempDir = this.cloneRemoteToTemp();
|
|
109
|
+
if (!tempDir) {
|
|
110
|
+
return { attempted: true, synced: false, added: 0, overwritten: 0, skipped: 0 };
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const commonDotClaude = path.join(tempDir, 'specs', 'common', 'claude', '.claude');
|
|
114
|
+
if (!existsSync(commonDotClaude)) {
|
|
115
|
+
return { attempted: true, synced: false, added: 0, overwritten: 0, skipped: 0 };
|
|
116
|
+
}
|
|
117
|
+
const targetDotClaude = path.join(projectPath, '.claude');
|
|
118
|
+
const result = this.syncDirectory(commonDotClaude, targetDotClaude);
|
|
119
|
+
return { attempted: true, synced: true, ...result };
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return { attempted: true, synced: false, added: 0, overwritten: 0, skipped: 0 };
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
cloneRemoteToTemp() {
|
|
129
|
+
const tempDir = mkdtempSync(path.join(os.tmpdir(), 'zhuanspec-update-'));
|
|
130
|
+
try {
|
|
131
|
+
execSync(`git clone --depth 1 --single-branch --branch ${ARCH_REPO_BRANCH} ${ARCH_REPO_URL} "${tempDir}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
132
|
+
return tempDir;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
syncDirectory(srcDir, dstDir) {
|
|
140
|
+
const result = { added: 0, overwritten: 0, skipped: 0 };
|
|
141
|
+
const sync = (src, dst) => {
|
|
142
|
+
mkdirSync(dst, { recursive: true });
|
|
143
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
144
|
+
const srcPath = path.join(src, entry.name);
|
|
145
|
+
const dstPath = path.join(dst, entry.name);
|
|
146
|
+
if (entry.isDirectory()) {
|
|
147
|
+
sync(srcPath, dstPath);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const srcContent = readFileSync(srcPath);
|
|
151
|
+
if (existsSync(dstPath)) {
|
|
152
|
+
if (Buffer.compare(srcContent, readFileSync(dstPath)) === 0) {
|
|
153
|
+
result.skipped++;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
writeFileSync(dstPath, srcContent);
|
|
157
|
+
result.overwritten++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
writeFileSync(dstPath, srcContent);
|
|
162
|
+
result.added++;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
sync(srcDir, dstDir);
|
|
168
|
+
return result;
|
|
86
169
|
}
|
|
87
170
|
}
|
|
88
171
|
//# sourceMappingURL=update.js.map
|