@zhuan-ai/zhuanspec 2.11.3 → 2.11.6
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/commands/change.js +9 -4
- package/dist/commands/progress.js +3 -3
- package/dist/commands/spec.js +12 -4
- package/dist/commands/validate.js +15 -6
- package/dist/core/artifact-graph/instruction-loader.d.ts +3 -1
- package/dist/core/artifact-graph/instruction-loader.js +27 -2
- package/dist/core/hooks/deviation-check.js +16 -3
- package/dist/core/hooks/init.js +14 -1
- package/dist/core/hooks/notify-milestone.js +31 -5
- package/dist/core/hooks/pre-review.js +18 -5
- package/dist/core/hooks/record-progress.d.ts +12 -0
- package/dist/core/hooks/record-progress.js +3 -3
- package/dist/core/hooks/review-hooks.js +5 -4
- package/dist/core/hooks/review-orchestrator.js +9 -0
- package/dist/core/init.js +14 -2
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +11 -0
- package/dist/core/templates/slash-command-templates.js +25 -13
- package/dist/core/templates/tasks-template.d.ts +0 -7
- package/dist/core/templates/tasks-template.js +8 -45
- package/dist/core/templates/tdd-tasks-template.d.ts +0 -6
- package/dist/core/templates/tdd-tasks-template.js +159 -73
- package/dist/core/validation/strict-rules.d.ts +18 -8
- package/dist/core/validation/strict-rules.js +188 -70
- package/dist/core/validation/validator.js +21 -0
- package/dist/utils/task-progress.js +2 -2
- package/package.json +22 -20
- package/schemas/spec-driven/schema.yaml +10 -2
- package/schemas/spec-driven/templates/tasks.md +117 -4
package/dist/commands/change.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
3
4
|
import { JsonConverter } from '../core/converters/json-converter.js';
|
|
4
5
|
import { Validator } from '../core/validation/validator.js';
|
|
5
6
|
import { ChangeParser } from '../core/parsers/change-parser.js';
|
|
@@ -211,11 +212,15 @@ export class ChangeCommand {
|
|
|
211
212
|
console.log(`Change "${changeName}" is valid`);
|
|
212
213
|
}
|
|
213
214
|
else {
|
|
214
|
-
console.error(
|
|
215
|
+
console.error(`变更 "${changeName}" 存在问题`);
|
|
215
216
|
report.issues.forEach(issue => {
|
|
216
|
-
const label = issue.level === 'ERROR' ? '
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
const label = issue.level === 'ERROR' ? '错误' : '警告';
|
|
218
|
+
if (issue.level === 'ERROR') {
|
|
219
|
+
console.error(chalk.red(`✗ [${label}] ${issue.path}: ${issue.message}`));
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
console.error(chalk.yellow(`⚠ [${label}] ${issue.path}: ${issue.message}`));
|
|
223
|
+
}
|
|
219
224
|
});
|
|
220
225
|
// Next steps footer to guide fixing issues
|
|
221
226
|
this.printNextSteps();
|
|
@@ -211,12 +211,12 @@ export class ProgressCommand {
|
|
|
211
211
|
});
|
|
212
212
|
continue;
|
|
213
213
|
}
|
|
214
|
-
// Match "Task X.Y" format: #### Task 1.0: description or ####
|
|
215
|
-
const taskXYMatch = line.match(/^####\s*Task\s+(\d+\.\d+)\s
|
|
214
|
+
// Match "Task X.Y" or bare "X.Y" format: #### Task 1.0: description or #### 1.1 description
|
|
215
|
+
const taskXYMatch = line.match(/^####\s*(?:Task\s+)?(\d+\.\d+)[\s:]\s*(.+)$/i);
|
|
216
216
|
if (taskXYMatch) {
|
|
217
217
|
const taskId = taskXYMatch[1];
|
|
218
218
|
const rawDescription = taskXYMatch[2].trim();
|
|
219
|
-
const isCompleted = rawDescription.endsWith('
|
|
219
|
+
const isCompleted = rawDescription.endsWith('\u2705');
|
|
220
220
|
const description = isCompleted
|
|
221
221
|
? rawDescription.slice(0, -1).trim()
|
|
222
222
|
: rawDescription;
|
package/dist/commands/spec.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
1
2
|
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
2
3
|
import { join } from 'path';
|
|
3
4
|
import { MarkdownParser } from '../core/parsers/markdown-parser.js';
|
|
@@ -205,11 +206,18 @@ export function registerSpecCommand(rootProgram) {
|
|
|
205
206
|
console.log(`Specification '${specId}' is valid`);
|
|
206
207
|
}
|
|
207
208
|
else {
|
|
208
|
-
console.error(
|
|
209
|
+
console.error(`规范 '${specId}' 存在问题`);
|
|
209
210
|
report.issues.forEach(issue => {
|
|
210
|
-
const label = issue.level === 'ERROR' ? '
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
const label = issue.level === 'ERROR' ? '错误' : issue.level === 'WARNING' ? '警告' : '提示';
|
|
212
|
+
if (issue.level === 'ERROR') {
|
|
213
|
+
console.error(chalk.red(`✗ [${label}] ${issue.path}: ${issue.message}`));
|
|
214
|
+
}
|
|
215
|
+
else if (issue.level === 'WARNING') {
|
|
216
|
+
console.error(chalk.yellow(`⚠ [${label}] ${issue.path}: ${issue.message}`));
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
console.error(`ℹ [${label}] ${issue.path}: ${issue.message}`);
|
|
220
|
+
}
|
|
213
221
|
});
|
|
214
222
|
}
|
|
215
223
|
}
|
|
@@ -292,18 +292,25 @@ export class ValidateCommand {
|
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
294
|
else {
|
|
295
|
-
console.error(`${type === 'change' ? '
|
|
295
|
+
console.error(`${type === 'change' ? '变更' : '规范'} '${id}' 存在问题`);
|
|
296
296
|
for (const issue of report.issues) {
|
|
297
|
-
const label = issue.level === 'ERROR' ? '
|
|
298
|
-
|
|
299
|
-
|
|
297
|
+
const label = issue.level === 'ERROR' ? '错误' : issue.level === 'WARNING' ? '警告' : '提示';
|
|
298
|
+
if (issue.level === 'ERROR') {
|
|
299
|
+
console.error(chalk.red(`✗ [${label}] ${issue.path}: ${issue.message}`));
|
|
300
|
+
}
|
|
301
|
+
else if (issue.level === 'WARNING') {
|
|
302
|
+
console.error(chalk.yellow(`⚠ [${label}] ${issue.path}: ${issue.message}`));
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
console.error(`ℹ [${label}] ${issue.path}: ${issue.message}`);
|
|
306
|
+
}
|
|
300
307
|
}
|
|
301
308
|
this.printNextSteps(type);
|
|
302
309
|
}
|
|
303
310
|
// Print strict mode checks summary if available
|
|
304
311
|
if (opts?.strict && report.strictChecks && report.strictChecks.length > 0) {
|
|
305
312
|
console.log('');
|
|
306
|
-
console.log('
|
|
313
|
+
console.log('严格模式检查:');
|
|
307
314
|
for (const check of report.strictChecks) {
|
|
308
315
|
if (check.passed) {
|
|
309
316
|
console.log(` ${chalk.green('✓')} ${check.ruleName}`);
|
|
@@ -311,7 +318,9 @@ export class ValidateCommand {
|
|
|
311
318
|
else {
|
|
312
319
|
const firstError = check.errors[0] || check.warnings[0] || '';
|
|
313
320
|
const errorSuffix = firstError ? ` — ${firstError}` : '';
|
|
314
|
-
|
|
321
|
+
const hasErrors = check.errors.length > 0;
|
|
322
|
+
const color = hasErrors ? chalk.red : chalk.yellow;
|
|
323
|
+
console.log(` ${color('✗')} ${check.ruleName}${errorSuffix}`);
|
|
315
324
|
}
|
|
316
325
|
}
|
|
317
326
|
}
|
|
@@ -100,7 +100,9 @@ export interface ChangeStatus {
|
|
|
100
100
|
* @returns The template content
|
|
101
101
|
* @throws TemplateLoadError if the template cannot be loaded
|
|
102
102
|
*/
|
|
103
|
-
export declare function loadTemplate(schemaName: string, templatePath: string
|
|
103
|
+
export declare function loadTemplate(schemaName: string, templatePath: string, options?: {
|
|
104
|
+
changeName?: string;
|
|
105
|
+
}): string;
|
|
104
106
|
/**
|
|
105
107
|
* Loads change context combining graph and completion state.
|
|
106
108
|
*
|
|
@@ -5,6 +5,8 @@ import { ArtifactGraph } from './graph.js';
|
|
|
5
5
|
import { detectCompleted } from './state.js';
|
|
6
6
|
import { resolveSchemaForChange } from '../../utils/change-metadata.js';
|
|
7
7
|
import { loadProjectConfig, getEffectiveRules } from '../project-config.js';
|
|
8
|
+
import { getTasksTemplate } from '../templates/tasks-template.js';
|
|
9
|
+
import { getTddTasksTemplate } from '../templates/tdd-tasks-template.js';
|
|
8
10
|
/**
|
|
9
11
|
* Error thrown when loading a template fails.
|
|
10
12
|
*/
|
|
@@ -24,7 +26,13 @@ export class TemplateLoadError extends Error {
|
|
|
24
26
|
* @returns The template content
|
|
25
27
|
* @throws TemplateLoadError if the template cannot be loaded
|
|
26
28
|
*/
|
|
27
|
-
export function loadTemplate(schemaName, templatePath) {
|
|
29
|
+
export function loadTemplate(schemaName, templatePath, options) {
|
|
30
|
+
// Prefer TypeScript template functions as single source of truth
|
|
31
|
+
const tsTemplate = loadFromTypeScriptTemplate(schemaName, templatePath, options);
|
|
32
|
+
if (tsTemplate !== null) {
|
|
33
|
+
return tsTemplate;
|
|
34
|
+
}
|
|
35
|
+
// Fallback: load from static file
|
|
28
36
|
const schemaDir = getSchemaDir(schemaName);
|
|
29
37
|
if (!schemaDir) {
|
|
30
38
|
throw new TemplateLoadError(`Schema '${schemaName}' not found`, templatePath);
|
|
@@ -41,6 +49,23 @@ export function loadTemplate(schemaName, templatePath) {
|
|
|
41
49
|
throw new TemplateLoadError(`Failed to read template: ${ioError.message}`, fullPath);
|
|
42
50
|
}
|
|
43
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Attempt to load template from TypeScript template functions.
|
|
54
|
+
* Returns null if no TypeScript template is registered for this schema+template combination.
|
|
55
|
+
* This ensures TypeScript templates are the single source of truth,
|
|
56
|
+
* avoiding dual-track desync between .ts functions and static .md files.
|
|
57
|
+
*/
|
|
58
|
+
function loadFromTypeScriptTemplate(schemaName, templatePath, options) {
|
|
59
|
+
if (templatePath === 'tasks.md') {
|
|
60
|
+
if (schemaName === 'spec-driven') {
|
|
61
|
+
return getTasksTemplate({ changeId: options?.changeName || '[change-id]' });
|
|
62
|
+
}
|
|
63
|
+
if (schemaName === 'tdd') {
|
|
64
|
+
return getTddTasksTemplate({ changeId: options?.changeName || '[change-id]' });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
44
69
|
/**
|
|
45
70
|
* Loads change context combining graph and completion state.
|
|
46
71
|
*
|
|
@@ -82,7 +107,7 @@ export function generateInstructions(context, artifactId) {
|
|
|
82
107
|
if (!artifact) {
|
|
83
108
|
throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'`);
|
|
84
109
|
}
|
|
85
|
-
const template = loadTemplate(context.schemaName, artifact.template);
|
|
110
|
+
const template = loadTemplate(context.schemaName, artifact.template, { changeName: context.changeName });
|
|
86
111
|
const dependencies = getDependencyInfo(artifact, context.graph, context.completed);
|
|
87
112
|
const unlocks = getUnlockedArtifacts(context.graph, artifactId);
|
|
88
113
|
// Load project config for context/rules injection
|
|
@@ -16,7 +16,7 @@ import fs from 'fs';
|
|
|
16
16
|
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
17
17
|
import { GitRepoDetector } from '../../utils/git-repo-detector.js';
|
|
18
18
|
import { checkPreApplyConditions } from './pre-apply.js';
|
|
19
|
-
import { recordHookTrigger } from './record-progress.js';
|
|
19
|
+
import { recordHookTrigger, detectActiveChange } from './record-progress.js';
|
|
20
20
|
// Pitfall keywords for knowledge collection
|
|
21
21
|
const PITFALL_KEYWORDS = [
|
|
22
22
|
'不对', '错了', '踩坑', '坑', '失败', '问题', 'bug',
|
|
@@ -120,9 +120,22 @@ async function readStdin() {
|
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
122
|
async function runDeviationCheck(filePath, trigger, promptText, _options) {
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
// Priority: env vars (set by SessionStart) > filesystem detection (fallback)
|
|
124
|
+
let phase = process.env.ZHUANSPEC_PHASE || '';
|
|
125
|
+
let changeId = process.env.ZHUANSPEC_CHANGE_ID || '';
|
|
125
126
|
const currentTask = process.env.ZHUANSPEC_CURRENT_TASK || '';
|
|
127
|
+
// Fallback: if env vars not set, detect active change from filesystem
|
|
128
|
+
if (!changeId || phase === 'idle' || !phase) {
|
|
129
|
+
const cwd = process.cwd();
|
|
130
|
+
const activeChange = await detectActiveChange(cwd, filePath || undefined);
|
|
131
|
+
if (activeChange) {
|
|
132
|
+
changeId = activeChange.changeId;
|
|
133
|
+
phase = activeChange.phase;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
phase = phase || 'idle';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
126
139
|
// No change bound or idle phase → allow, post-prompt silences itself to avoid noise
|
|
127
140
|
if (!changeId || phase === 'idle') {
|
|
128
141
|
if (trigger === 'post-prompt') {
|
package/dist/core/hooks/init.js
CHANGED
|
@@ -76,7 +76,20 @@ export async function initHook(options) {
|
|
|
76
76
|
await updateClaudeHudCustomLine(output.versionBanner);
|
|
77
77
|
}
|
|
78
78
|
if (options.json) {
|
|
79
|
-
|
|
79
|
+
// Output JSON with env at top level for Claude Code hook protocol
|
|
80
|
+
const jsonOutput = {
|
|
81
|
+
continue: output.continue,
|
|
82
|
+
systemMessage: output.systemMessage,
|
|
83
|
+
};
|
|
84
|
+
if (output.hookSpecificOutput?.env) {
|
|
85
|
+
jsonOutput.env = output.hookSpecificOutput.env;
|
|
86
|
+
}
|
|
87
|
+
if (output.hookSpecificOutput?.additionalContext) {
|
|
88
|
+
jsonOutput.hookSpecificOutput = {
|
|
89
|
+
additionalContext: output.hookSpecificOutput.additionalContext,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
console.log(JSON.stringify(jsonOutput));
|
|
80
93
|
}
|
|
81
94
|
else {
|
|
82
95
|
if (output.systemMessage) {
|
|
@@ -122,16 +122,42 @@ function parseTasks(content) {
|
|
|
122
122
|
currentWave = parseInt(waveMatch[1], 10);
|
|
123
123
|
continue;
|
|
124
124
|
}
|
|
125
|
-
// Match task lines: - [ ] X.Y description or - [x] X.Y description
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
128
|
-
const statusChar =
|
|
129
|
-
const taskId =
|
|
125
|
+
// Match new format task lines: - [ ] X.Y description or - [x] X.Y description
|
|
126
|
+
const newFormatMatch = line.match(/^-\s*\[([x ])\]\s*(\d+\.\d+)\s*(.+)/);
|
|
127
|
+
if (newFormatMatch) {
|
|
128
|
+
const statusChar = newFormatMatch[1];
|
|
129
|
+
const taskId = newFormatMatch[2];
|
|
130
130
|
tasks.push({
|
|
131
131
|
taskId,
|
|
132
132
|
status: statusChar === 'x' ? 'completed' : 'pending',
|
|
133
133
|
wave: currentWave,
|
|
134
134
|
});
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// Match legacy format task headers: #### T1: description or #### T2: description
|
|
138
|
+
const legacyMatch = line.match(/^####\s*(T\d+)\s*:\s*(.+)$/);
|
|
139
|
+
if (legacyMatch) {
|
|
140
|
+
const taskId = legacyMatch[1];
|
|
141
|
+
const rawDescription = legacyMatch[2].trim();
|
|
142
|
+
const isCompleted = rawDescription.endsWith('✅');
|
|
143
|
+
tasks.push({
|
|
144
|
+
taskId,
|
|
145
|
+
status: isCompleted ? 'completed' : 'pending',
|
|
146
|
+
wave: currentWave,
|
|
147
|
+
});
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
// Match "Task X.Y" format: #### Task 1.0: description or #### Task 1.1: description
|
|
151
|
+
const taskXYMatch = line.match(/^####\s*Task\s+(\d+\.\d+)\s*:\s*(.+)$/i);
|
|
152
|
+
if (taskXYMatch) {
|
|
153
|
+
const taskId = taskXYMatch[1];
|
|
154
|
+
const rawDescription = taskXYMatch[2].trim();
|
|
155
|
+
const isCompleted = rawDescription.endsWith('✅');
|
|
156
|
+
tasks.push({
|
|
157
|
+
taskId,
|
|
158
|
+
status: isCompleted ? 'completed' : 'pending',
|
|
159
|
+
wave: currentWave,
|
|
160
|
+
});
|
|
135
161
|
}
|
|
136
162
|
}
|
|
137
163
|
return tasks;
|
|
@@ -36,9 +36,18 @@ async function checkTasksCompletion(changePath) {
|
|
|
36
36
|
}
|
|
37
37
|
try {
|
|
38
38
|
const content = await FileSystemUtils.readFile(tasksPath);
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
const
|
|
39
|
+
// Support checkbox format: - [ ] / - [x]
|
|
40
|
+
const checkboxAll = content.match(/^- \[[ x]\] .+$/gm) || [];
|
|
41
|
+
const checkboxUncompleted = content.match(/^- \[ \] .+$/gm) || [];
|
|
42
|
+
const checkboxCompleted = content.match(/^- \[x\] .+$/gm) || [];
|
|
43
|
+
// Support header format: #### N.M / #### Task N.M: / #### TN:
|
|
44
|
+
const headerAll = content.match(/^####\s*(?:Task\s+)?\d+\.\d+[\s:].+$|^####\s*T\d+\s*:.+$/gm) || [];
|
|
45
|
+
const headerCompleted = headerAll.filter(t => t.includes('\u2705'));
|
|
46
|
+
const headerUncompleted = headerAll.filter(t => !t.includes('\u2705'));
|
|
47
|
+
// Use checkbox format if found, otherwise fall back to header format
|
|
48
|
+
const allTasks = checkboxAll.length > 0 ? checkboxAll : headerAll;
|
|
49
|
+
const uncompletedTasks = checkboxAll.length > 0 ? checkboxUncompleted : headerUncompleted;
|
|
50
|
+
const completedTasks = checkboxAll.length > 0 ? checkboxCompleted : headerCompleted;
|
|
42
51
|
if (allTasks.length === 0) {
|
|
43
52
|
return {
|
|
44
53
|
passed: false,
|
|
@@ -86,8 +95,12 @@ async function checkProgressConsistency(changePath) {
|
|
|
86
95
|
const progressContent = await FileSystemUtils.readFile(progressPath);
|
|
87
96
|
const progress = JSON.parse(progressContent);
|
|
88
97
|
const tasksContent = await FileSystemUtils.readFile(tasksPath);
|
|
89
|
-
const
|
|
90
|
-
const
|
|
98
|
+
const checkboxTotal = (tasksContent.match(/^- \[[ x]\] .+$/gm) || []).length;
|
|
99
|
+
const checkboxCompleted = (tasksContent.match(/^- \[x\] .+$/gm) || []).length;
|
|
100
|
+
const headerTotal = (tasksContent.match(/^####\s*(?:Task\s+)?\d+\.\d+[\s:].+$|^####\s*T\d+\s*:.+$/gm) || []).length;
|
|
101
|
+
const headerCompleted = (tasksContent.match(/^####\s*(?:Task\s+)?\d+\.\d+[\s:].+$|^####\s*T\d+\s*:.+$/gm) || []).filter(t => t.includes('\u2705')).length;
|
|
102
|
+
const totalFromTasks = checkboxTotal > 0 ? checkboxTotal : headerTotal;
|
|
103
|
+
const completedFromTasks = checkboxTotal > 0 ? checkboxCompleted : headerCompleted;
|
|
91
104
|
const totalFromProgress = progress.totalTasks || 0;
|
|
92
105
|
if (totalFromProgress !== totalFromTasks) {
|
|
93
106
|
return {
|
|
@@ -213,6 +213,18 @@ export interface ProgressData {
|
|
|
213
213
|
lastEvent?: string;
|
|
214
214
|
}
|
|
215
215
|
export declare function recordProgressHook(options: RecordProgressOptions): Promise<void>;
|
|
216
|
+
/**
|
|
217
|
+
* Detect active change from progress.json files.
|
|
218
|
+
*
|
|
219
|
+
* Strategy:
|
|
220
|
+
* 1. Primary — if filePath is under zhuanspec/changes/{changeId}/, extract changeId directly.
|
|
221
|
+
* 2. Fallback — scan all changes and return the most recently updated one
|
|
222
|
+
* (by lastUpdatedAt field; stub files without it fall back to filesystem mtime).
|
|
223
|
+
*/
|
|
224
|
+
export declare function detectActiveChange(cwd: string, filePath?: string): Promise<{
|
|
225
|
+
changeId: string;
|
|
226
|
+
phase: Phase;
|
|
227
|
+
} | null>;
|
|
216
228
|
/**
|
|
217
229
|
* Initialize progress.json at phase start
|
|
218
230
|
* This function should be called when a change is created, before any files are written.
|
|
@@ -219,7 +219,7 @@ function extractMcpTool(toolName) {
|
|
|
219
219
|
* 2. Fallback — scan all changes and return the most recently updated one
|
|
220
220
|
* (by lastUpdatedAt field; stub files without it fall back to filesystem mtime).
|
|
221
221
|
*/
|
|
222
|
-
async function detectActiveChange(cwd, filePath) {
|
|
222
|
+
export async function detectActiveChange(cwd, filePath) {
|
|
223
223
|
const changesDir = path.join(cwd, 'zhuanspec', 'changes');
|
|
224
224
|
if (!await FileSystemUtils.directoryExists(changesDir)) {
|
|
225
225
|
return null;
|
|
@@ -544,7 +544,7 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
544
544
|
const tasksContent = await FileSystemUtils.readFile(tasksPath);
|
|
545
545
|
// Match checkbox format: - [ ] or - [x], and also #### Task X.Y: format
|
|
546
546
|
const checkboxTasks = tasksContent.match(/^- \[[ x]\] .+$/gm) || [];
|
|
547
|
-
const taskHeaderTasks = tasksContent.match(/^####\s*Task\s
|
|
547
|
+
const taskHeaderTasks = tasksContent.match(/^####\s*(?:Task\s+)?\d+\.\d+[\s:].+$/gm) || [];
|
|
548
548
|
const legacyHeaderTasks = tasksContent.match(/^####\s*T\d+\s*:.+$/gm) || [];
|
|
549
549
|
const totalTaskMatches = checkboxTasks.length > 0 ? checkboxTasks : (taskHeaderTasks.length > 0 ? taskHeaderTasks : legacyHeaderTasks);
|
|
550
550
|
const completedCheckbox = tasksContent.match(/^- \[x\] .+$/gm) || [];
|
|
@@ -552,7 +552,7 @@ async function runRecordProgress(filePath, toolName, success, stdinData) {
|
|
|
552
552
|
const completedTaskMatches = checkboxTasks.length > 0 ? completedCheckbox : completedHeaders;
|
|
553
553
|
progress.totalTasks = totalTaskMatches.length;
|
|
554
554
|
// Update completedTasks array with completed task descriptions
|
|
555
|
-
const completedDescriptions = completedTaskMatches.map(t => t.replace(/^- \[x\] /, '').replace(/^####\s*(?:Task\s
|
|
555
|
+
const completedDescriptions = completedTaskMatches.map(t => t.replace(/^- \[x\] /, '').replace(/^####\s*(?:Task\s+)?\d+\.\d+[\s:]\s*/, '').replace(/^####\s*T\d+\s*:\s*/, '').replace(/\u2705$/, '').trim());
|
|
556
556
|
if (completedDescriptions.length > 0) {
|
|
557
557
|
progress.completedTasks = completedDescriptions;
|
|
558
558
|
}
|
|
@@ -420,11 +420,12 @@ function formatConsistencyFixPrompt(output, loopCount) {
|
|
|
420
420
|
|
|
421
421
|
${uncoveredList}
|
|
422
422
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
2. 或更新 Spec 描述以反映当前实现状态
|
|
423
|
+
⚠️ 铁律:Spec is Truth — 文档与代码冲突时,错的一定是代码。
|
|
424
|
+
🚫 禁止修改 Spec 文件内容(包括 Requirement 名称、Scenario 描述、Given/When/Then 条件)。
|
|
426
425
|
|
|
427
|
-
|
|
426
|
+
请通过以下方式修复:
|
|
427
|
+
1. 补充或修改代码实现,使其满足上述 Scenario 描述的业务诉求
|
|
428
|
+
2. 修复后重新运行 spec-code-consistency 检查验证覆盖情况
|
|
428
429
|
`;
|
|
429
430
|
}
|
|
430
431
|
// ============================================================
|
|
@@ -45,6 +45,12 @@ function generateUnitTestPrompt(changeId) {
|
|
|
45
45
|
## 任务目标
|
|
46
46
|
为 changeId=${changeId} 的代码变更生成并验证单元测试
|
|
47
47
|
|
|
48
|
+
## TDD 模式感知
|
|
49
|
+
在生成单测前,检查 Apply 阶段是否已有 TDD 生成的测试文件:
|
|
50
|
+
- 读取 zhuanspec/changes/${changeId}/tasks.md,查看是否有已完成的 @test-case 任务
|
|
51
|
+
- 若存在 TDD 已生成的测试,识别已有测试做增量补全
|
|
52
|
+
- 若不存在 TDD 测试,按正常流程全量生成单测
|
|
53
|
+
|
|
48
54
|
## 自闭环指令
|
|
49
55
|
1. 执行 generate-mockito-unit-test skill 生成单测
|
|
50
56
|
2. 运行单测,检查通过率和覆盖率
|
|
@@ -111,6 +117,9 @@ function generateSpecConsistencyPrompt(changeId) {
|
|
|
111
117
|
|
|
112
118
|
### Step 4:自闭环修复
|
|
113
119
|
- 发现 ❌ 未覆盖的 Scenario → 补充对应代码实现
|
|
120
|
+
- ⚠️ **铁律:Spec is Truth** — 文档与代码冲突时,错的一定是代码
|
|
121
|
+
- 🚫 **绝对禁止修改 Spec 文件**(包括 Requirement 名称、Scenario 标题、Given/When/Then 描述)
|
|
122
|
+
- 只能通过新增或修改代码来满足 Spec 描述的业务诉求
|
|
114
123
|
- 修复后重新执行 Step 2 验证(最多3轮)
|
|
115
124
|
- ⚠️ 仅修复真实遗漏,禁止把"文件存在但逻辑未实现"误报为已覆盖
|
|
116
125
|
|
package/dist/core/init.js
CHANGED
|
@@ -1254,7 +1254,7 @@ export class InitCommand {
|
|
|
1254
1254
|
hooks: [
|
|
1255
1255
|
{
|
|
1256
1256
|
type: 'command',
|
|
1257
|
-
command: 'zhuanspec-hook init',
|
|
1257
|
+
command: 'zhuanspec-hook init --json',
|
|
1258
1258
|
statusMessage: 'Initializing ZhuanSpec...',
|
|
1259
1259
|
},
|
|
1260
1260
|
],
|
|
@@ -1552,6 +1552,18 @@ export class InitCommand {
|
|
|
1552
1552
|
}
|
|
1553
1553
|
const spinner = this.startSpinner('正在安装 claude-hud(用于版本信息展示)...');
|
|
1554
1554
|
try {
|
|
1555
|
+
// Step 1: Add the claude-hud marketplace (skip if already added)
|
|
1556
|
+
try {
|
|
1557
|
+
execSync('claude plugin marketplace add jarrodwatts/claude-hud', {
|
|
1558
|
+
encoding: 'utf-8',
|
|
1559
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1560
|
+
timeout: 30000,
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
catch {
|
|
1564
|
+
// Marketplace may already be added, ignore error and proceed
|
|
1565
|
+
}
|
|
1566
|
+
// Step 2: Install the plugin from the marketplace
|
|
1555
1567
|
execSync('claude plugin install claude-hud', {
|
|
1556
1568
|
encoding: 'utf-8',
|
|
1557
1569
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -1565,7 +1577,7 @@ export class InitCommand {
|
|
|
1565
1577
|
catch {
|
|
1566
1578
|
spinner.stopAndPersist({
|
|
1567
1579
|
symbol: PALETTE.midGray('▌'),
|
|
1568
|
-
text: PALETTE.midGray('claude-hud 安装失败,可手动执行: claude
|
|
1580
|
+
text: PALETTE.midGray('claude-hud 安装失败,可手动执行: claude plugin marketplace add jarrodwatts/claude-hud && claude plugin install claude-hud'),
|
|
1569
1581
|
});
|
|
1570
1582
|
}
|
|
1571
1583
|
}
|