@zhuan-ai/zhuanspec 2.10.0 → 2.11.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/dist/cli/index.js +1 -0
- package/dist/commands/design.d.ts +12 -2
- package/dist/commands/design.js +64 -12
- package/dist/commands/progress.js +11 -0
- package/dist/commands/review.d.ts +1 -0
- package/dist/commands/review.js +13 -0
- package/dist/core/completions/command-registry.js +4 -0
- package/dist/core/hooks/deviation-check.js +42 -0
- package/dist/core/hooks/init.js +21 -4
- package/dist/core/hooks/post-apply.js +2 -0
- package/dist/core/hooks/pre-review.d.ts +27 -0
- package/dist/core/hooks/pre-review.js +169 -0
- package/dist/core/hooks/record-progress.d.ts +25 -0
- package/dist/core/hooks/record-progress.js +88 -1
- package/dist/core/hooks/review-hooks.js +171 -3
- package/dist/core/hooks/review-orchestrator.js +55 -8
- package/dist/core/hooks/tdd-phase-hook.d.ts +59 -0
- package/dist/core/hooks/tdd-phase-hook.js +313 -0
- package/dist/core/init.d.ts +1 -0
- package/dist/core/init.js +27 -0
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +139 -23
- package/dist/core/templates/skill-templates.js +21 -1
- package/dist/core/templates/slash-command-templates.js +129 -49
- package/dist/core/templates/tasks-template.js +89 -0
- package/dist/core/validation/strict-rules.d.ts +47 -0
- package/dist/core/validation/strict-rules.js +589 -42
- package/dist/utils/git-repo-detector.js +1 -1
- package/dist/utils/phase-utils.js +14 -1
- package/package.json +22 -20
package/dist/cli/index.js
CHANGED
|
@@ -220,6 +220,7 @@ program
|
|
|
220
220
|
.command('review [change-name]')
|
|
221
221
|
.description('Run ZhuanSpec review and unit test verification')
|
|
222
222
|
.option('--json', 'Output review report as JSON')
|
|
223
|
+
.option('--force', 'Skip task completion check and force review')
|
|
223
224
|
.option('--test-command <command>', 'Test command to execute (default: npm test)')
|
|
224
225
|
.option('--coverage-threshold <n>', 'Coverage threshold percentage (default: 80)')
|
|
225
226
|
.action(async (changeName, options) => {
|
|
@@ -18,13 +18,22 @@ export declare class DesignCommand {
|
|
|
18
18
|
private bindDesignSourceToChange;
|
|
19
19
|
private selectChangeInteractively;
|
|
20
20
|
/**
|
|
21
|
-
* Validate design.md for required sections and optionally auto-fix
|
|
21
|
+
* Validate design.md for required sections and optionally auto-fix.
|
|
22
|
+
* Uses dynamic section derivation: always-required sections are checked for all proposals,
|
|
23
|
+
* while conditional sections are only required when techDesign/tech-spec.md content matches.
|
|
22
24
|
*/
|
|
23
25
|
private validateDesign;
|
|
24
26
|
/**
|
|
25
27
|
* Auto-fix design content by adding missing section skeletons
|
|
26
28
|
*/
|
|
27
29
|
private autoFixDesign;
|
|
30
|
+
/**
|
|
31
|
+
* Derive required design.md sections dynamically.
|
|
32
|
+
* Always-required: 背景/Background, 决策/Decision, 风险/Risks.
|
|
33
|
+
* Conditional sections are added based on techDesign/tech-spec.md content.
|
|
34
|
+
* When no techDesign directory exists, only always-required sections are enforced (backward compat).
|
|
35
|
+
*/
|
|
36
|
+
private getRequiredSections;
|
|
28
37
|
/**
|
|
29
38
|
* Generate a kebab-case change name from design context
|
|
30
39
|
*/
|
|
@@ -32,7 +41,8 @@ export declare class DesignCommand {
|
|
|
32
41
|
/**
|
|
33
42
|
* Create a new change directory for techDesign phase
|
|
34
43
|
* - Creates change directory using createChange()
|
|
35
|
-
* - Creates .tech-design marker file
|
|
44
|
+
* - Creates .tech-design marker file (backward compat)
|
|
45
|
+
* - Creates techDesign/ directory with tech-spec.md placeholder
|
|
36
46
|
* - Initializes progress.json with phase=techDesign
|
|
37
47
|
* - Creates design.md skeleton
|
|
38
48
|
* - Creates doc/ request file
|
package/dist/commands/design.js
CHANGED
|
@@ -209,7 +209,9 @@ export class DesignCommand {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
/**
|
|
212
|
-
* Validate design.md for required sections and optionally auto-fix
|
|
212
|
+
* Validate design.md for required sections and optionally auto-fix.
|
|
213
|
+
* Uses dynamic section derivation: always-required sections are checked for all proposals,
|
|
214
|
+
* while conditional sections are only required when techDesign/tech-spec.md content matches.
|
|
213
215
|
*/
|
|
214
216
|
async validateDesign(changeName, autoFix) {
|
|
215
217
|
const changeDir = path.join(process.cwd(), 'zhuanspec', 'changes', changeName);
|
|
@@ -219,15 +221,8 @@ export class DesignCommand {
|
|
|
219
221
|
}
|
|
220
222
|
const content = await FileSystemUtils.readFile(designPath);
|
|
221
223
|
const issues = [];
|
|
222
|
-
//
|
|
223
|
-
const requiredSections =
|
|
224
|
-
{ pattern: /^##\s+(背景|Background)/im, name: '背景/Background', skeleton: '## 背景\n\n[请补充背景、约束、利益相关者]\n' },
|
|
225
|
-
{ pattern: /^##\s+(目标|Goals|非目标|Non-Goals)/im, name: '目标/Goals', skeleton: '## 目标 / 非目标\n\n- Goals: [...]\n- Non-Goals: [...]\n' },
|
|
226
|
-
{ pattern: /^##\s+(决策|Decision)/im, name: '决策/Decision', skeleton: '## 决策\n\n- Decision: [内容和原因]\n- Alternatives considered: [选项 + 理由]\n' },
|
|
227
|
-
{ pattern: /^##\s+(接口设计|Interface|API)/im, name: '接口设计/Interface', skeleton: '## 接口设计\n\n<!-- 接口签名、入参出参字段类型、枚举值、SCF 方法名 -->\n' },
|
|
228
|
-
{ pattern: /^##\s+(实现细节|Implementation)/im, name: '实现细节/Implementation', skeleton: '## 实现细节\n\n<!-- 分层实现说明(Component→Application→Domain→Assemble→DAO)、关键逻辑、复用点 -->\n' },
|
|
229
|
-
{ pattern: /^##\s+(风险|权衡|Risks)/im, name: '风险/Risks', skeleton: '## 风险 / 权衡\n\n- [风险] -> 缓解措施\n' },
|
|
230
|
-
];
|
|
224
|
+
// Dynamically derive required sections based on techDesign content
|
|
225
|
+
const requiredSections = await this.getRequiredSections(changeDir);
|
|
231
226
|
for (const section of requiredSections) {
|
|
232
227
|
if (!section.pattern.test(content)) {
|
|
233
228
|
issues.push({
|
|
@@ -291,6 +286,58 @@ export class DesignCommand {
|
|
|
291
286
|
}
|
|
292
287
|
return result;
|
|
293
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* Derive required design.md sections dynamically.
|
|
291
|
+
* Always-required: 背景/Background, 决策/Decision, 风险/Risks.
|
|
292
|
+
* Conditional sections are added based on techDesign/tech-spec.md content.
|
|
293
|
+
* When no techDesign directory exists, only always-required sections are enforced (backward compat).
|
|
294
|
+
*/
|
|
295
|
+
async getRequiredSections(changePath) {
|
|
296
|
+
// Always-required sections for every proposal
|
|
297
|
+
const sections = [
|
|
298
|
+
{ pattern: /^##\s+(背景|Background)/im, name: '背景/Background', skeleton: '## 背景\n\n[请补充背景、约束、利益相关者]\n' },
|
|
299
|
+
{ pattern: /^##\s+(决策|Decision)/im, name: '决策/Decision', skeleton: '## 决策\n\n- Decision: [内容和原因]\n- Alternatives considered: [选项 + 理由]\n' },
|
|
300
|
+
{ pattern: /^##\s+(风险|权衡|Risks)/im, name: '风险/Risks', skeleton: '## 风险 / 权衡\n\n- [风险] -> 缓解措施\n' },
|
|
301
|
+
];
|
|
302
|
+
// Conditionally add sections based on tech-spec.md content
|
|
303
|
+
const techSpecPath = path.join(changePath, 'techDesign', 'tech-spec.md');
|
|
304
|
+
if (await FileSystemUtils.fileExists(techSpecPath)) {
|
|
305
|
+
const techContent = await fs.readFile(techSpecPath, 'utf-8');
|
|
306
|
+
// Tech spec has DDL / table creation → require Database Design
|
|
307
|
+
if (/CREATE\s+TABLE|DDL|建表|ALTER\s+TABLE/i.test(techContent)) {
|
|
308
|
+
sections.push({
|
|
309
|
+
pattern: /^##\s+(数据模型|数据库设计|Database\s*Design)/im,
|
|
310
|
+
name: '数据模型/Database Design',
|
|
311
|
+
skeleton: '## 数据模型\n\n<!-- DB schema 变更、实体类字段、索引设计 -->\n',
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
// Tech spec has API / interface definitions → require API Contract
|
|
315
|
+
if (/接口|API|SCF|RPC|Controller|@RequestMapping/i.test(techContent)) {
|
|
316
|
+
sections.push({
|
|
317
|
+
pattern: /^##\s+(接口设计|接口契约|API\s*Contract|Interface)/im,
|
|
318
|
+
name: '接口设计/API Contract',
|
|
319
|
+
skeleton: '## 接口设计\n\n<!-- 接口签名、入参出参字段类型、枚举值、SCF 方法名 -->\n',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
// Tech spec has ES / MQ / Cache → require Infrastructure
|
|
323
|
+
if (/ElasticSearch|ES.*索引|mapping|MQ|RocketMQ|Consumer|Redis|缓存|Cache/i.test(techContent)) {
|
|
324
|
+
sections.push({
|
|
325
|
+
pattern: /^##\s+(基础设施|Infrastructure)/im,
|
|
326
|
+
name: '基础设施/Infrastructure',
|
|
327
|
+
skeleton: '## 基础设施\n\n<!-- ES / MQ / Cache 等基础设施设计 -->\n',
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
// Tech spec has frontend design → require Frontend Design
|
|
331
|
+
if (/前端|页面|组件|路由|Frontend|Component|React|Vue/i.test(techContent)) {
|
|
332
|
+
sections.push({
|
|
333
|
+
pattern: /^##\s+(前端设计|Frontend\s*Design)/im,
|
|
334
|
+
name: '前端设计/Frontend Design',
|
|
335
|
+
skeleton: '## 前端设计\n\n<!-- 页面布局、组件结构、路由设计 -->\n',
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return sections;
|
|
340
|
+
}
|
|
294
341
|
/**
|
|
295
342
|
* Generate a kebab-case change name from design context
|
|
296
343
|
*/
|
|
@@ -328,7 +375,8 @@ export class DesignCommand {
|
|
|
328
375
|
/**
|
|
329
376
|
* Create a new change directory for techDesign phase
|
|
330
377
|
* - Creates change directory using createChange()
|
|
331
|
-
* - Creates .tech-design marker file
|
|
378
|
+
* - Creates .tech-design marker file (backward compat)
|
|
379
|
+
* - Creates techDesign/ directory with tech-spec.md placeholder
|
|
332
380
|
* - Initializes progress.json with phase=techDesign
|
|
333
381
|
* - Creates design.md skeleton
|
|
334
382
|
* - Creates doc/ request file
|
|
@@ -340,7 +388,11 @@ export class DesignCommand {
|
|
|
340
388
|
const changeDir = path.join(projectRoot, 'zhuanspec', 'changes', changeName);
|
|
341
389
|
// 2. Initialize progress.json with phase=techDesign
|
|
342
390
|
await initializeProgress(changeName, 'techDesign');
|
|
343
|
-
// 3. Create
|
|
391
|
+
// 3. Create techDesign/ directory and tech-spec.md placeholder
|
|
392
|
+
const techDesignDir = path.join(changeDir, 'techDesign');
|
|
393
|
+
await FileSystemUtils.createDirectory(techDesignDir);
|
|
394
|
+
await FileSystemUtils.writeFile(path.join(techDesignDir, 'tech-spec.md'), `# Tech Spec\n\ncreated: ${new Date().toISOString()}\nsource: ${context.desc || context.dashenPageId || 'manual'}\n\n> This file will be populated by the techDesign skill.\n`);
|
|
395
|
+
// 3b. Also create .tech-design marker for backward compatibility
|
|
344
396
|
await FileSystemUtils.writeFile(path.join(changeDir, '.tech-design'), `created: ${new Date().toISOString()}\nsource: ${context.desc || context.dashenPageId || 'manual'}\n`);
|
|
345
397
|
// 4. Create design.md skeleton
|
|
346
398
|
await FileSystemUtils.writeFile(path.join(changeDir, 'design.md'), DEFAULT_DESIGN_TEMPLATE);
|
|
@@ -312,6 +312,17 @@ export class ProgressCommand {
|
|
|
312
312
|
else if (completedCount === totalTasks && totalTasks > 0) {
|
|
313
313
|
console.log('✅ 全部任务完成,准备进入 Review 阶段');
|
|
314
314
|
}
|
|
315
|
+
// Proposal changes history
|
|
316
|
+
if (progress?.proposalChanges && progress.proposalChanges.length > 0) {
|
|
317
|
+
console.log('');
|
|
318
|
+
console.log('📝 提案修改历史:');
|
|
319
|
+
for (const change of progress.proposalChanges) {
|
|
320
|
+
console.log(` [${change.timestamp}] ${change.triggeredBy} (${change.phase})${change.relatedDeviationId ? ` ← ${change.relatedDeviationId}` : ''}`);
|
|
321
|
+
for (const file of change.modifiedFiles) {
|
|
322
|
+
console.log(` ${file.changeType}: ${file.filePath} - ${file.summary}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
315
326
|
}
|
|
316
327
|
generateProgressBar(percentage, blocks) {
|
|
317
328
|
const filled = Math.round((percentage / 100) * blocks);
|
package/dist/commands/review.js
CHANGED
|
@@ -22,6 +22,19 @@ export class ReviewCommand {
|
|
|
22
22
|
if (!proposalExists || !tasksExists) {
|
|
23
23
|
throw new Error(`Change '${changeName}' is not ready for review. Missing proposal.md or tasks.md`);
|
|
24
24
|
}
|
|
25
|
+
// === Task completion hard gate ===
|
|
26
|
+
if (tasksExists) {
|
|
27
|
+
const tasksContent = await fs.readFile(tasksPath, 'utf-8');
|
|
28
|
+
const uncompletedTasks = tasksContent.match(/^- \[ \] .+$/gm) || [];
|
|
29
|
+
if (uncompletedTasks.length > 0 && !options?.force) {
|
|
30
|
+
throw new Error(`Change '${changeName}' has ${uncompletedTasks.length} uncompleted tasks. ` +
|
|
31
|
+
`Please complete all tasks before review. Use --force to override.\n` +
|
|
32
|
+
uncompletedTasks.map(t => ` ${t.trim()}`).join('\n'));
|
|
33
|
+
}
|
|
34
|
+
if (uncompletedTasks.length > 0 && options?.force) {
|
|
35
|
+
console.log(`⚠️ Force mode: skipping ${uncompletedTasks.length} uncompleted tasks`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
25
38
|
// Check skill output JSON files exist
|
|
26
39
|
const codeReviewJsonPath = path.join(reviewDir, 'code-review-result.json');
|
|
27
40
|
const unitTestJsonPath = path.join(reviewDir, 'unit-test-result.json');
|
|
@@ -124,6 +124,10 @@ export const COMMAND_REGISTRY = [
|
|
|
124
124
|
positionalType: 'change-id',
|
|
125
125
|
flags: [
|
|
126
126
|
COMMON_FLAGS.json,
|
|
127
|
+
{
|
|
128
|
+
name: 'force',
|
|
129
|
+
description: 'Skip task completion check and force review',
|
|
130
|
+
},
|
|
127
131
|
{
|
|
128
132
|
name: 'test-command',
|
|
129
133
|
description: 'Test command to execute (default: npm test)',
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* - Out of scope → block with deviation handling options
|
|
13
13
|
*/
|
|
14
14
|
import path from 'path';
|
|
15
|
+
import fs from 'fs';
|
|
15
16
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
16
17
|
import { GitRepoDetector } from '../../utils/git-repo-detector.js';
|
|
17
18
|
import { checkPreApplyConditions } from './pre-apply.js';
|
|
@@ -148,6 +149,13 @@ async function runDeviationCheck(filePath, trigger, promptText, _options) {
|
|
|
148
149
|
return !estimatedFiles.map(normalizePath).some((scope) => normalized.includes(scope) || scope.includes(normalized));
|
|
149
150
|
});
|
|
150
151
|
if (outOfScope.length > 0) {
|
|
152
|
+
const resumeDeviationId = `dev-resume-${Date.now()}`;
|
|
153
|
+
// Write deviation ID to temp file for record-progress to read (cross-process)
|
|
154
|
+
const metricsDir = path.join(changeDir, 'metrics');
|
|
155
|
+
if (!fs.existsSync(metricsDir)) {
|
|
156
|
+
fs.mkdirSync(metricsDir, { recursive: true });
|
|
157
|
+
}
|
|
158
|
+
fs.writeFileSync(path.join(metricsDir, '.last-deviation-id'), resumeDeviationId, 'utf-8');
|
|
151
159
|
return {
|
|
152
160
|
continue: false,
|
|
153
161
|
stopReason: 'Resume deviation detected',
|
|
@@ -268,6 +276,34 @@ async function runDeviationCheck(filePath, trigger, promptText, _options) {
|
|
|
268
276
|
}
|
|
269
277
|
// Apply phase → check deviation
|
|
270
278
|
if (phase === 'apply') {
|
|
279
|
+
// === Task completion gate for Apply→Review transition ===
|
|
280
|
+
if (trigger === 'post-prompt') {
|
|
281
|
+
const lowerPrompt = promptText.toLowerCase();
|
|
282
|
+
const reviewTransitionKeywords = [
|
|
283
|
+
'review', '审查', '进入review', 'review阶段', '代码审查',
|
|
284
|
+
'zhuanspec review', '/review', '开始review',
|
|
285
|
+
];
|
|
286
|
+
const isReviewTransition = reviewTransitionKeywords.some(kw => lowerPrompt.includes(kw));
|
|
287
|
+
if (isReviewTransition && await FileSystemUtils.fileExists(tasksPath)) {
|
|
288
|
+
try {
|
|
289
|
+
const tasksContent = await FileSystemUtils.readFile(tasksPath);
|
|
290
|
+
const uncompleted = tasksContent.match(/^- \[ \] .+$/gm) || [];
|
|
291
|
+
if (uncompleted.length > 0) {
|
|
292
|
+
return {
|
|
293
|
+
continue: false,
|
|
294
|
+
stopReason: 'Task completion gate: uncompleted tasks block Review transition',
|
|
295
|
+
systemMessage: `⚠️ BLOCKED: Cannot transition to Review — ${uncompleted.length} tasks uncompleted in tasks.md.\n\nUncompleted tasks:\n${uncompleted.map(t => ` ${t.trim()}`).join('\n')}\n\nPlease complete all tasks before entering Review, or use \`zhuanspec review <id> --force\` to override.`,
|
|
296
|
+
hookSpecificOutput: {
|
|
297
|
+
additionalContext: `Task completion gate blocked Apply→Review transition. ${uncompleted.length} uncompleted tasks found.`,
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// If tasks.md read fails, allow transition
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
271
307
|
let estimatedFiles = [];
|
|
272
308
|
let currentTaskFiles = [];
|
|
273
309
|
// Parse proposal for estimated changes
|
|
@@ -293,6 +329,12 @@ async function runDeviationCheck(filePath, trigger, promptText, _options) {
|
|
|
293
329
|
}
|
|
294
330
|
// File is out of scope → deviation detected
|
|
295
331
|
const deviationId = `dev-${Date.now()}`;
|
|
332
|
+
// Write deviation ID to temp file for record-progress to read (cross-process)
|
|
333
|
+
const deviationMetricsDir = path.join(changeDir, 'metrics');
|
|
334
|
+
if (!fs.existsSync(deviationMetricsDir)) {
|
|
335
|
+
fs.mkdirSync(deviationMetricsDir, { recursive: true });
|
|
336
|
+
}
|
|
337
|
+
fs.writeFileSync(path.join(deviationMetricsDir, '.last-deviation-id'), deviationId, 'utf-8');
|
|
296
338
|
return {
|
|
297
339
|
continue: false,
|
|
298
340
|
stopReason: 'Deviation detected: File not in approved proposal scope',
|
package/dist/core/hooks/init.js
CHANGED
|
@@ -36,6 +36,22 @@ async function checkVersionUpdate() {
|
|
|
36
36
|
return null;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
async function updateClaudeHudCustomLine(versionBanner) {
|
|
40
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(process.env.HOME || '~', '.claude');
|
|
41
|
+
const hudConfigPath = path.join(configDir, 'plugins', 'claude-hud', 'config.json');
|
|
42
|
+
try {
|
|
43
|
+
const raw = await fs.promises.readFile(hudConfigPath, 'utf-8');
|
|
44
|
+
const config = JSON.parse(raw);
|
|
45
|
+
if (!config.display)
|
|
46
|
+
config.display = {};
|
|
47
|
+
config.display.customLine = versionBanner;
|
|
48
|
+
await fs.promises.writeFile(hudConfigPath, JSON.stringify(config, null, 2));
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
39
55
|
export async function initHook(options) {
|
|
40
56
|
const startTime = Date.now();
|
|
41
57
|
const output = await runInitHook(options);
|
|
@@ -56,6 +72,9 @@ export async function initHook(options) {
|
|
|
56
72
|
catch {
|
|
57
73
|
// Ignore write errors
|
|
58
74
|
}
|
|
75
|
+
if (output.versionBanner) {
|
|
76
|
+
await updateClaudeHudCustomLine(output.versionBanner);
|
|
77
|
+
}
|
|
59
78
|
if (options.json) {
|
|
60
79
|
console.log(JSON.stringify(output));
|
|
61
80
|
}
|
|
@@ -252,12 +271,10 @@ async function runInitHook(_options) {
|
|
|
252
271
|
additionalContext += `(These are loaded automatically by Claude Code system)\n\n`;
|
|
253
272
|
}
|
|
254
273
|
const versionBanner = await checkVersionUpdate();
|
|
255
|
-
const finalMessage = versionBanner
|
|
256
|
-
? versionBanner + '\n' + (systemMessage || '✓ ZhuanSpec session initialized')
|
|
257
|
-
: (systemMessage || '✓ ZhuanSpec session initialized');
|
|
258
274
|
return {
|
|
259
275
|
continue: true,
|
|
260
|
-
systemMessage:
|
|
276
|
+
systemMessage: systemMessage || '✓ ZhuanSpec session initialized',
|
|
277
|
+
versionBanner: versionBanner || undefined,
|
|
261
278
|
hookSpecificOutput: {
|
|
262
279
|
additionalContext: additionalContext || undefined,
|
|
263
280
|
env: {
|
|
@@ -189,6 +189,7 @@ async function updateProgressJson(changeDir, event) {
|
|
|
189
189
|
reviewStats: { loopCount: 0, criticalFixes: 0, testFixes: 0, consistencyFixes: 0 },
|
|
190
190
|
phaseTransitions: [],
|
|
191
191
|
phaseDurations: [],
|
|
192
|
+
proposalChanges: [],
|
|
192
193
|
stats: { tokenUsageTotal: 0, contextLoad: 0, durationMs: { propose: 0, apply: 0, review: 0, archive: 0 } },
|
|
193
194
|
};
|
|
194
195
|
}
|
|
@@ -217,6 +218,7 @@ async function updateProgressJson(changeDir, event) {
|
|
|
217
218
|
reviewStats: { loopCount: 0, criticalFixes: 0, testFixes: 0, consistencyFixes: 0 },
|
|
218
219
|
phaseTransitions: [],
|
|
219
220
|
phaseDurations: [],
|
|
221
|
+
proposalChanges: [],
|
|
220
222
|
stats: { tokenUsageTotal: 0, contextLoad: 0, durationMs: { propose: 0, apply: 0, review: 0, archive: 0 } },
|
|
221
223
|
};
|
|
222
224
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-Review Hook
|
|
3
|
+
*
|
|
4
|
+
* Quality gate checks before entering Review phase:
|
|
5
|
+
* 1. All tasks in tasks.md are completed
|
|
6
|
+
* 2. Progress data is consistent with tasks.md
|
|
7
|
+
* 3. Specs completeness check (all delta specs have content)
|
|
8
|
+
*/
|
|
9
|
+
interface CheckResult {
|
|
10
|
+
passed: boolean;
|
|
11
|
+
message: string;
|
|
12
|
+
details?: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface PreReviewResult {
|
|
15
|
+
passed: boolean;
|
|
16
|
+
details: {
|
|
17
|
+
tasksComplete: CheckResult;
|
|
18
|
+
progressConsistent: CheckResult;
|
|
19
|
+
specsComplete: CheckResult;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Run all pre-review checks for a change directory.
|
|
24
|
+
*/
|
|
25
|
+
export declare function preReviewHook(changePath: string): Promise<PreReviewResult>;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=pre-review.d.ts.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-Review Hook
|
|
3
|
+
*
|
|
4
|
+
* Quality gate checks before entering Review phase:
|
|
5
|
+
* 1. All tasks in tasks.md are completed
|
|
6
|
+
* 2. Progress data is consistent with tasks.md
|
|
7
|
+
* 3. Specs completeness check (all delta specs have content)
|
|
8
|
+
*/
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
11
|
+
import { readdirSync, existsSync } from 'fs';
|
|
12
|
+
/**
|
|
13
|
+
* Run all pre-review checks for a change directory.
|
|
14
|
+
*/
|
|
15
|
+
export async function preReviewHook(changePath) {
|
|
16
|
+
const checks = {
|
|
17
|
+
tasksComplete: await checkTasksCompletion(changePath),
|
|
18
|
+
progressConsistent: await checkProgressConsistency(changePath),
|
|
19
|
+
specsComplete: await checkSpecsCompleteness(changePath),
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
passed: Object.values(checks).every(c => c.passed),
|
|
23
|
+
details: checks,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Check that all tasks in tasks.md are completed.
|
|
28
|
+
*/
|
|
29
|
+
async function checkTasksCompletion(changePath) {
|
|
30
|
+
const tasksPath = path.join(changePath, 'tasks.md');
|
|
31
|
+
if (!await FileSystemUtils.fileExists(tasksPath)) {
|
|
32
|
+
return {
|
|
33
|
+
passed: false,
|
|
34
|
+
message: 'tasks.md not found',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const content = await FileSystemUtils.readFile(tasksPath);
|
|
39
|
+
const allTasks = content.match(/^- \[[ x]\] .+$/gm) || [];
|
|
40
|
+
const uncompletedTasks = content.match(/^- \[ \] .+$/gm) || [];
|
|
41
|
+
const completedTasks = content.match(/^- \[x\] .+$/gm) || [];
|
|
42
|
+
if (allTasks.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
passed: false,
|
|
45
|
+
message: 'No tasks found in tasks.md',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (uncompletedTasks.length > 0) {
|
|
49
|
+
return {
|
|
50
|
+
passed: false,
|
|
51
|
+
message: `${uncompletedTasks.length}/${allTasks.length} tasks uncompleted`,
|
|
52
|
+
details: uncompletedTasks.map(t => t.trim()),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
passed: true,
|
|
57
|
+
message: `All ${completedTasks.length} tasks completed`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return {
|
|
62
|
+
passed: false,
|
|
63
|
+
message: 'Failed to read tasks.md',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check that progress.json data is consistent with tasks.md counts.
|
|
69
|
+
*/
|
|
70
|
+
async function checkProgressConsistency(changePath) {
|
|
71
|
+
const progressPath = path.join(changePath, 'metrics', 'progress.json');
|
|
72
|
+
const tasksPath = path.join(changePath, 'tasks.md');
|
|
73
|
+
if (!await FileSystemUtils.fileExists(progressPath)) {
|
|
74
|
+
return {
|
|
75
|
+
passed: true,
|
|
76
|
+
message: 'No progress.json found (skipped)',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (!await FileSystemUtils.fileExists(tasksPath)) {
|
|
80
|
+
return {
|
|
81
|
+
passed: true,
|
|
82
|
+
message: 'No tasks.md found (skipped)',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const progressContent = await FileSystemUtils.readFile(progressPath);
|
|
87
|
+
const progress = JSON.parse(progressContent);
|
|
88
|
+
const tasksContent = await FileSystemUtils.readFile(tasksPath);
|
|
89
|
+
const totalFromTasks = (tasksContent.match(/^- \[[ x]\] .+$/gm) || []).length;
|
|
90
|
+
const completedFromTasks = (tasksContent.match(/^- \[x\] .+$/gm) || []).length;
|
|
91
|
+
const totalFromProgress = progress.totalTasks || 0;
|
|
92
|
+
if (totalFromProgress !== totalFromTasks) {
|
|
93
|
+
return {
|
|
94
|
+
passed: false,
|
|
95
|
+
message: `Task count mismatch: progress.json says ${totalFromProgress}, tasks.md has ${totalFromTasks}`,
|
|
96
|
+
details: [
|
|
97
|
+
`progress.json totalTasks: ${totalFromProgress}`,
|
|
98
|
+
`tasks.md total tasks: ${totalFromTasks}`,
|
|
99
|
+
`tasks.md completed: ${completedFromTasks}`,
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
passed: true,
|
|
105
|
+
message: `Progress consistent: ${completedFromTasks}/${totalFromTasks} tasks`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return {
|
|
110
|
+
passed: true,
|
|
111
|
+
message: 'Progress consistency check skipped (parse error)',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Check that all delta spec files under specs/ have meaningful content.
|
|
117
|
+
*/
|
|
118
|
+
async function checkSpecsCompleteness(changePath) {
|
|
119
|
+
const specsDir = path.join(changePath, 'specs');
|
|
120
|
+
if (!existsSync(specsDir)) {
|
|
121
|
+
return {
|
|
122
|
+
passed: false,
|
|
123
|
+
message: 'specs/ directory not found',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const capabilities = readdirSync(specsDir, { withFileTypes: true })
|
|
128
|
+
.filter(e => e.isDirectory())
|
|
129
|
+
.map(e => e.name);
|
|
130
|
+
if (capabilities.length === 0) {
|
|
131
|
+
return {
|
|
132
|
+
passed: false,
|
|
133
|
+
message: 'No capability directories found in specs/',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const emptySpecs = [];
|
|
137
|
+
for (const cap of capabilities) {
|
|
138
|
+
const specPath = path.join(specsDir, cap, 'spec.md');
|
|
139
|
+
if (!existsSync(specPath)) {
|
|
140
|
+
emptySpecs.push(`${cap}/spec.md (missing)`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const content = await FileSystemUtils.readFile(specPath);
|
|
144
|
+
// Check if spec has at least one delta marker (ADDED/MODIFIED/REMOVED)
|
|
145
|
+
const hasDelta = /\b(ADDED|MODIFIED|REMOVED)\b/.test(content);
|
|
146
|
+
if (!hasDelta) {
|
|
147
|
+
emptySpecs.push(`${cap}/spec.md (no delta markers)`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (emptySpecs.length > 0) {
|
|
151
|
+
return {
|
|
152
|
+
passed: false,
|
|
153
|
+
message: `${emptySpecs.length} spec(s) incomplete`,
|
|
154
|
+
details: emptySpecs,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
passed: true,
|
|
159
|
+
message: `All ${capabilities.length} spec(s) complete`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return {
|
|
164
|
+
passed: false,
|
|
165
|
+
message: 'Failed to check specs completeness',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=pre-review.js.map
|
|
@@ -98,6 +98,8 @@ interface SkillCallRecord {
|
|
|
98
98
|
durationMs?: number;
|
|
99
99
|
success: boolean;
|
|
100
100
|
outputSummary?: string;
|
|
101
|
+
afterAction?: string;
|
|
102
|
+
flowResumed?: boolean;
|
|
101
103
|
}
|
|
102
104
|
/**
|
|
103
105
|
* Hook 触发记录
|
|
@@ -149,6 +151,28 @@ export interface PhaseDurationRecord {
|
|
|
149
151
|
taskCount: number;
|
|
150
152
|
completedTaskCount: number;
|
|
151
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Proposal change record for tracking modifications to proposal files
|
|
156
|
+
*/
|
|
157
|
+
export interface ProposalChangeRecord {
|
|
158
|
+
changeRecordId: string;
|
|
159
|
+
timestamp: string;
|
|
160
|
+
phase: string;
|
|
161
|
+
triggeredBy: 'reverse-sync' | 'manual-edit' | 'ai-supplement' | 'review-fix';
|
|
162
|
+
modifiedFiles: Array<{
|
|
163
|
+
filePath: string;
|
|
164
|
+
changeType: 'modified' | 'created' | 'deleted';
|
|
165
|
+
summary: string;
|
|
166
|
+
diff?: {
|
|
167
|
+
before: string;
|
|
168
|
+
after: string;
|
|
169
|
+
};
|
|
170
|
+
linesAdded?: number;
|
|
171
|
+
linesRemoved?: number;
|
|
172
|
+
}>;
|
|
173
|
+
reason?: string;
|
|
174
|
+
relatedDeviationId?: string;
|
|
175
|
+
}
|
|
152
176
|
export interface ProgressData {
|
|
153
177
|
changeId: string;
|
|
154
178
|
sessionId: string;
|
|
@@ -181,6 +205,7 @@ export interface ProgressData {
|
|
|
181
205
|
archive: number;
|
|
182
206
|
};
|
|
183
207
|
};
|
|
208
|
+
proposalChanges: ProposalChangeRecord[];
|
|
184
209
|
events?: Array<{
|
|
185
210
|
event: string;
|
|
186
211
|
timestamp: string;
|