@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
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { SlashCommandConfigurator } from './base.js';
|
|
2
2
|
const FILE_PATHS = {
|
|
3
3
|
proposal: '.windsurf/workflows/zhuanspec-proposal.md',
|
|
4
|
+
design: '.windsurf/workflows/zhuanspec-design.md',
|
|
4
5
|
apply: '.windsurf/workflows/zhuanspec-apply.md',
|
|
5
|
-
|
|
6
|
+
review: '.windsurf/workflows/zhuanspec-review.md',
|
|
7
|
+
archive: '.windsurf/workflows/zhuanspec-archive.md',
|
|
8
|
+
knowledge: '.windsurf/workflows/zhuanspec-knowledge.md'
|
|
6
9
|
};
|
|
7
10
|
export class WindsurfSlashCommandConfigurator extends SlashCommandConfigurator {
|
|
8
11
|
toolId = 'windsurf';
|
|
@@ -13,8 +16,11 @@ export class WindsurfSlashCommandConfigurator extends SlashCommandConfigurator {
|
|
|
13
16
|
getFrontmatter(id) {
|
|
14
17
|
const descriptions = {
|
|
15
18
|
proposal: '搭建新的 ZhuanSpec 变更并严格验证。',
|
|
19
|
+
design: '在 proposal 之前独立生成技术设计请求文档。',
|
|
16
20
|
apply: '实施已批准的 ZhuanSpec 变更并保持任务同步。',
|
|
17
|
-
|
|
21
|
+
review: '在归档前执行代码审查和单元测试验证。',
|
|
22
|
+
archive: '归档已部署的 ZhuanSpec 变更并更新规范。',
|
|
23
|
+
knowledge: '管理项目知识库(最佳实践、陷阱、隐式约定)。'
|
|
18
24
|
};
|
|
19
25
|
const description = descriptions[id];
|
|
20
26
|
return `---\ndescription: ${description}\nauto_execution_mode: 3\n---`;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface ChangeMetricsSummary {
|
|
2
|
+
changeId: string;
|
|
3
|
+
phase: string;
|
|
4
|
+
currentNode: string;
|
|
5
|
+
currentTask: string;
|
|
6
|
+
totalTasks: number;
|
|
7
|
+
completedTasks: number;
|
|
8
|
+
currentWave: number;
|
|
9
|
+
nextTask: string;
|
|
10
|
+
toolCalls: number;
|
|
11
|
+
skillCalls: Record<string, number>;
|
|
12
|
+
review: {
|
|
13
|
+
critical: number;
|
|
14
|
+
important: number;
|
|
15
|
+
minor: number;
|
|
16
|
+
passed: boolean;
|
|
17
|
+
};
|
|
18
|
+
tests: {
|
|
19
|
+
passed: number;
|
|
20
|
+
failed: number;
|
|
21
|
+
coverage: number | null;
|
|
22
|
+
};
|
|
23
|
+
tokenUsage: number;
|
|
24
|
+
contextLoad: number;
|
|
25
|
+
durations: {
|
|
26
|
+
proposeMs: number;
|
|
27
|
+
applyMs: number;
|
|
28
|
+
reviewMs: number;
|
|
29
|
+
archiveMs: number;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export declare function loadChangeMetrics(zhuanspecDir: string, changeId: string): Promise<ChangeMetricsSummary>;
|
|
33
|
+
//# sourceMappingURL=metrics.d.ts.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { countTasksFromContent } from '../../utils/task-progress.js';
|
|
4
|
+
async function readJsonIfExists(filePath) {
|
|
5
|
+
try {
|
|
6
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
7
|
+
return JSON.parse(content);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function parseWaveFromTaskId(taskId) {
|
|
14
|
+
if (!taskId)
|
|
15
|
+
return 0;
|
|
16
|
+
const id = taskId.split('.')[0];
|
|
17
|
+
const num = Number(id);
|
|
18
|
+
return Number.isFinite(num) ? num : 0;
|
|
19
|
+
}
|
|
20
|
+
function findNextTask(tasksContent) {
|
|
21
|
+
const lines = tasksContent.split('\n');
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
const match = line.match(/^-\s*\[\s\]\s*(\d+\.\d+)/);
|
|
24
|
+
if (match)
|
|
25
|
+
return match[1];
|
|
26
|
+
}
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
function countSkillCalls(toolCalls) {
|
|
30
|
+
const result = {};
|
|
31
|
+
for (const call of toolCalls ?? []) {
|
|
32
|
+
const inputContent = (call?.input?.content ?? '');
|
|
33
|
+
const matches = inputContent.match(/@skill:([^\s@]+)/g) ?? [];
|
|
34
|
+
for (const raw of matches) {
|
|
35
|
+
const names = raw.replace('@skill:', '').split(',');
|
|
36
|
+
for (const name of names) {
|
|
37
|
+
const key = name.trim();
|
|
38
|
+
if (!key || key === 'none')
|
|
39
|
+
continue;
|
|
40
|
+
result[key] = (result[key] ?? 0) + 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
export async function loadChangeMetrics(zhuanspecDir, changeId) {
|
|
47
|
+
const changeDir = path.join(zhuanspecDir, 'changes', changeId);
|
|
48
|
+
const metricsDir = path.join(changeDir, 'metrics');
|
|
49
|
+
const progress = await readJsonIfExists(path.join(metricsDir, 'progress.json'));
|
|
50
|
+
const events = await readJsonIfExists(path.join(metricsDir, 'events.json'));
|
|
51
|
+
const review = await readJsonIfExists(path.join(metricsDir, 'review.json'));
|
|
52
|
+
const tests = await readJsonIfExists(path.join(metricsDir, 'tests.json'));
|
|
53
|
+
const tasksPath = path.join(changeDir, 'tasks.md');
|
|
54
|
+
let totalTasks = 0;
|
|
55
|
+
let completedTasks = 0;
|
|
56
|
+
let nextTask = '';
|
|
57
|
+
try {
|
|
58
|
+
const tasksContent = await fs.readFile(tasksPath, 'utf-8');
|
|
59
|
+
const taskProgress = countTasksFromContent(tasksContent);
|
|
60
|
+
totalTasks = taskProgress.total;
|
|
61
|
+
completedTasks = taskProgress.completed;
|
|
62
|
+
nextTask = findNextTask(tasksContent);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// ignore
|
|
66
|
+
}
|
|
67
|
+
const phase = progress?.phase ?? 'idle';
|
|
68
|
+
const currentTask = progress?.currentTask ?? '';
|
|
69
|
+
const currentNode = progress?.currentNode ?? phase;
|
|
70
|
+
const currentWave = parseWaveFromTaskId(currentTask || nextTask);
|
|
71
|
+
const toolCalls = (progress?.toolCalls ?? []);
|
|
72
|
+
const skillCalls = countSkillCalls(toolCalls);
|
|
73
|
+
const tokenUsage = Number(progress?.stats?.tokenUsageTotal ?? 0) +
|
|
74
|
+
Number(progress?.stats?.tokenUsage?.propose ?? 0) +
|
|
75
|
+
Number(progress?.stats?.tokenUsage?.apply ?? 0) +
|
|
76
|
+
Number(progress?.stats?.tokenUsage?.review ?? 0) +
|
|
77
|
+
Number(progress?.stats?.tokenUsage?.archive ?? 0);
|
|
78
|
+
const contextLoad = Number(progress?.stats?.contextLoad ?? 0);
|
|
79
|
+
const phaseDurations = (progress?.stats?.durationMs ?? {});
|
|
80
|
+
return {
|
|
81
|
+
changeId,
|
|
82
|
+
phase,
|
|
83
|
+
currentNode,
|
|
84
|
+
currentTask,
|
|
85
|
+
totalTasks,
|
|
86
|
+
completedTasks,
|
|
87
|
+
currentWave,
|
|
88
|
+
nextTask,
|
|
89
|
+
toolCalls: toolCalls.length,
|
|
90
|
+
skillCalls,
|
|
91
|
+
review: {
|
|
92
|
+
critical: Number(review?.summary?.critical ?? 0),
|
|
93
|
+
important: Number(review?.summary?.important ?? 0),
|
|
94
|
+
minor: Number(review?.summary?.minor ?? 0),
|
|
95
|
+
passed: Boolean(review?.passed ?? false),
|
|
96
|
+
},
|
|
97
|
+
tests: {
|
|
98
|
+
passed: Number(tests?.passed ?? 0),
|
|
99
|
+
failed: Number(tests?.failed ?? 0),
|
|
100
|
+
coverage: typeof tests?.coverage === 'number'
|
|
101
|
+
? Number(tests.coverage)
|
|
102
|
+
: null,
|
|
103
|
+
},
|
|
104
|
+
tokenUsage,
|
|
105
|
+
contextLoad,
|
|
106
|
+
durations: {
|
|
107
|
+
proposeMs: Number(phaseDurations.propose ?? 0),
|
|
108
|
+
applyMs: Number(phaseDurations.apply ?? 0),
|
|
109
|
+
reviewMs: Number(phaseDurations.review ?? 0),
|
|
110
|
+
archiveMs: Number(phaseDurations.archive ?? 0),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=metrics.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostToolUse Hook - Collect Knowledge
|
|
3
|
+
*
|
|
4
|
+
* This hook runs after Write/Edit operations to:
|
|
5
|
+
* 1. Detect pitfall keywords in content or conversation
|
|
6
|
+
* 2. Extract and save troubleshooting knowledge
|
|
7
|
+
* 3. Record implicit conventions discovered
|
|
8
|
+
*/
|
|
9
|
+
interface CollectKnowledgeOptions {
|
|
10
|
+
json?: boolean;
|
|
11
|
+
file?: string;
|
|
12
|
+
content?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function collectKnowledgeHook(options: CollectKnowledgeOptions): Promise<void>;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=collect-knowledge.d.ts.map
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostToolUse Hook - Collect Knowledge
|
|
3
|
+
*
|
|
4
|
+
* This hook runs after Write/Edit operations to:
|
|
5
|
+
* 1. Detect pitfall keywords in content or conversation
|
|
6
|
+
* 2. Extract and save troubleshooting knowledge
|
|
7
|
+
* 3. Record implicit conventions discovered
|
|
8
|
+
*/
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
11
|
+
import { PITFALL_KEYWORDS, BEST_PRACTICE_KEYWORDS, IMPLICIT_CONVENTION_KEYWORDS } from './deviation-check.js';
|
|
12
|
+
import { getBeijingTime, getBeijingTimeForFilename } from './record-progress.js';
|
|
13
|
+
export async function collectKnowledgeHook(options) {
|
|
14
|
+
// Read stdin for Claude Code hook input
|
|
15
|
+
let stdinData = {};
|
|
16
|
+
try {
|
|
17
|
+
const stdinContent = await readStdin();
|
|
18
|
+
if (stdinContent) {
|
|
19
|
+
stdinData = JSON.parse(stdinContent);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Ignore parse errors
|
|
24
|
+
}
|
|
25
|
+
// Get content from stdin or options
|
|
26
|
+
const content = stdinData.tool_input?.content
|
|
27
|
+
|| options.content
|
|
28
|
+
|| '';
|
|
29
|
+
const filePath = stdinData.tool_input?.file_path
|
|
30
|
+
|| options.file
|
|
31
|
+
|| '';
|
|
32
|
+
const output = await runCollectKnowledge(content, filePath);
|
|
33
|
+
if (options.json) {
|
|
34
|
+
console.log(JSON.stringify(output));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
if (output.systemMessage) {
|
|
38
|
+
console.log(output.systemMessage);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function readStdin() {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
let data = '';
|
|
45
|
+
process.stdin.setEncoding('utf8');
|
|
46
|
+
process.stdin.on('data', chunk => data += chunk);
|
|
47
|
+
process.stdin.on('end', () => resolve(data));
|
|
48
|
+
process.stdin.on('error', () => resolve(null));
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async function runCollectKnowledge(content, filePath) {
|
|
52
|
+
const phase = process.env.ZHUANSPEC_PHASE || 'idle';
|
|
53
|
+
const changeId = process.env.ZHUANSPEC_CHANGE_ID || '';
|
|
54
|
+
// No active change → nothing to collect
|
|
55
|
+
if (!changeId || phase === 'idle') {
|
|
56
|
+
return {
|
|
57
|
+
continue: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const cwd = process.cwd();
|
|
61
|
+
const zhuanspecDir = path.join(cwd, 'zhuanspec');
|
|
62
|
+
const changeDir = path.join(zhuanspecDir, 'changes', changeId);
|
|
63
|
+
const knowledgeDir = path.join(changeDir, 'knowledge');
|
|
64
|
+
const troubleshootingDir = path.join(knowledgeDir, 'troubleshooting');
|
|
65
|
+
const bestPracticesDir = path.join(knowledgeDir, 'best-practices');
|
|
66
|
+
const implicitConventionsDir = path.join(knowledgeDir, 'implicit-conventions');
|
|
67
|
+
// Check for all keyword types
|
|
68
|
+
const contentLower = content.toLowerCase();
|
|
69
|
+
const detectedPitfalls = PITFALL_KEYWORDS.filter(kw => contentLower.includes(kw.toLowerCase()));
|
|
70
|
+
const detectedBestPractices = BEST_PRACTICE_KEYWORDS.filter(kw => contentLower.includes(kw.toLowerCase()));
|
|
71
|
+
const detectedImplicitConventions = IMPLICIT_CONVENTION_KEYWORDS.filter(kw => contentLower.includes(kw.toLowerCase()));
|
|
72
|
+
// If no keywords detected, return early
|
|
73
|
+
if (detectedPitfalls.length === 0 && detectedBestPractices.length === 0 && detectedImplicitConventions.length === 0) {
|
|
74
|
+
return {
|
|
75
|
+
continue: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Create directories as needed
|
|
79
|
+
await FileSystemUtils.createDirectory(knowledgeDir);
|
|
80
|
+
const collectedTypes = [];
|
|
81
|
+
let totalEntries = 0;
|
|
82
|
+
// Process pitfalls
|
|
83
|
+
if (detectedPitfalls.length > 0) {
|
|
84
|
+
await FileSystemUtils.createDirectory(troubleshootingDir);
|
|
85
|
+
const pitfall = extractKnowledgeContext(content, filePath, detectedPitfalls, 'troubleshooting');
|
|
86
|
+
const timestamp = getBeijingTimeForFilename();
|
|
87
|
+
const filename = `pitfall-${timestamp}.md`;
|
|
88
|
+
const pitfallPath = path.join(troubleshootingDir, filename);
|
|
89
|
+
await FileSystemUtils.writeFile(pitfallPath, formatKnowledgeRecord(pitfall));
|
|
90
|
+
updateKnowledgeIndex(knowledgeDir, 'troubleshooting', pitfall.title, filename);
|
|
91
|
+
collectedTypes.push('pitfall');
|
|
92
|
+
totalEntries++;
|
|
93
|
+
}
|
|
94
|
+
// Process best practices
|
|
95
|
+
if (detectedBestPractices.length > 0) {
|
|
96
|
+
await FileSystemUtils.createDirectory(bestPracticesDir);
|
|
97
|
+
const bestPractice = extractKnowledgeContext(content, filePath, detectedBestPractices, 'best_practice');
|
|
98
|
+
const timestamp = getBeijingTimeForFilename();
|
|
99
|
+
const filename = `best-practice-${timestamp}.md`;
|
|
100
|
+
const bpPath = path.join(bestPracticesDir, filename);
|
|
101
|
+
await FileSystemUtils.writeFile(bpPath, formatKnowledgeRecord(bestPractice));
|
|
102
|
+
updateKnowledgeIndex(knowledgeDir, 'best-practices', bestPractice.title, filename);
|
|
103
|
+
collectedTypes.push('best practice');
|
|
104
|
+
totalEntries++;
|
|
105
|
+
}
|
|
106
|
+
// Process implicit conventions
|
|
107
|
+
if (detectedImplicitConventions.length > 0) {
|
|
108
|
+
await FileSystemUtils.createDirectory(implicitConventionsDir);
|
|
109
|
+
const implicitConvention = extractKnowledgeContext(content, filePath, detectedImplicitConventions, 'implicit_contract');
|
|
110
|
+
const timestamp = getBeijingTimeForFilename();
|
|
111
|
+
const filename = `implicit-convention-${timestamp}.md`;
|
|
112
|
+
const icPath = path.join(implicitConventionsDir, filename);
|
|
113
|
+
await FileSystemUtils.writeFile(icPath, formatKnowledgeRecord(implicitConvention));
|
|
114
|
+
updateKnowledgeIndex(knowledgeDir, 'implicit-conventions', implicitConvention.title, filename);
|
|
115
|
+
collectedTypes.push('implicit convention');
|
|
116
|
+
totalEntries++;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
continue: true,
|
|
120
|
+
systemMessage: `📝 Knowledge detected and recorded: ${collectedTypes.join(', ')}`,
|
|
121
|
+
hookSpecificOutput: {
|
|
122
|
+
knowledgeCollected: `${totalEntries} entries`,
|
|
123
|
+
pitfallDetected: detectedPitfalls.length > 0,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function updateKnowledgeIndex(knowledgeDir, section, title, filename) {
|
|
128
|
+
const indexPath = path.join(knowledgeDir, 'index.md');
|
|
129
|
+
const subdir = section === 'troubleshooting' ? 'troubleshooting' :
|
|
130
|
+
section === 'best-practices' ? 'best-practices' :
|
|
131
|
+
'implicit-conventions';
|
|
132
|
+
const indexEntry = `\n- [${title}](${subdir}/${filename}) - ${getBeijingTime()}\n`;
|
|
133
|
+
if (await FileSystemUtils.fileExists(indexPath)) {
|
|
134
|
+
const existingIndex = await FileSystemUtils.readFile(indexPath);
|
|
135
|
+
// Find section or create it
|
|
136
|
+
const sectionHeader = `## ${section}`;
|
|
137
|
+
if (existingIndex.includes(sectionHeader)) {
|
|
138
|
+
// Append to existing section
|
|
139
|
+
const updated = existingIndex.replace(sectionHeader, `${sectionHeader}${indexEntry}`);
|
|
140
|
+
await FileSystemUtils.writeFile(indexPath, updated);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Add new section
|
|
144
|
+
await FileSystemUtils.writeFile(indexPath, `${existingIndex}\n\n${sectionHeader}${indexEntry}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
await FileSystemUtils.writeFile(indexPath, `# Knowledge Index\n\n## troubleshooting\n\n## best-practices\n\n## implicit-conventions\n${indexEntry}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function extractKnowledgeContext(content, filePath, keywords, type) {
|
|
152
|
+
// Extract relevant context around keywords
|
|
153
|
+
const lines = content.split('\n');
|
|
154
|
+
const relevantLines = [];
|
|
155
|
+
for (let i = 0; i < lines.length; i++) {
|
|
156
|
+
const lineLower = lines[i].toLowerCase();
|
|
157
|
+
if (keywords.some(kw => lineLower.includes(kw.toLowerCase()))) {
|
|
158
|
+
// Include surrounding context (3 lines before and after)
|
|
159
|
+
const start = Math.max(0, i - 3);
|
|
160
|
+
const end = Math.min(lines.length, i + 4);
|
|
161
|
+
for (let j = start; j < end; j++) {
|
|
162
|
+
relevantLines.push(lines[j]);
|
|
163
|
+
}
|
|
164
|
+
relevantLines.push('---');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Generate title from first keyword found
|
|
168
|
+
const firstKeyword = keywords[0];
|
|
169
|
+
const typeLabel = type === 'troubleshooting' ? 'Issue' :
|
|
170
|
+
type === 'best_practice' ? 'Best Practice' :
|
|
171
|
+
'Convention';
|
|
172
|
+
const title = `${typeLabel}: ${firstKeyword} in ${filePath}`;
|
|
173
|
+
return {
|
|
174
|
+
type,
|
|
175
|
+
title,
|
|
176
|
+
content: relevantLines.join('\n'),
|
|
177
|
+
source: filePath,
|
|
178
|
+
timestamp: getBeijingTime(),
|
|
179
|
+
tags: keywords,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function formatKnowledgeRecord(entry) {
|
|
183
|
+
return `# ${entry.title}
|
|
184
|
+
|
|
185
|
+
**Type**: ${entry.type}
|
|
186
|
+
**Source**: ${entry.source}
|
|
187
|
+
**Timestamp**: ${entry.timestamp}
|
|
188
|
+
**Tags**: ${entry.tags.join(', ')}
|
|
189
|
+
|
|
190
|
+
## Context
|
|
191
|
+
|
|
192
|
+
${entry.content}
|
|
193
|
+
|
|
194
|
+
## Resolution
|
|
195
|
+
|
|
196
|
+
[To be filled when resolved]
|
|
197
|
+
|
|
198
|
+
## Lessons Learned
|
|
199
|
+
|
|
200
|
+
[To be extracted after resolution]
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=collect-knowledge.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Load Hook
|
|
3
|
+
*
|
|
4
|
+
* Records context resource loading (project.md, rules, specs, knowledge, skills).
|
|
5
|
+
* This hook tracks what resources are loaded during each phase.
|
|
6
|
+
*/
|
|
7
|
+
interface ContextLoadOutput {
|
|
8
|
+
continue: boolean;
|
|
9
|
+
systemMessage?: string;
|
|
10
|
+
hookSpecificOutput?: {
|
|
11
|
+
resourceRecorded: boolean;
|
|
12
|
+
resourceType: string;
|
|
13
|
+
resourcePath: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Main hook function - records context resource loading
|
|
18
|
+
*/
|
|
19
|
+
export declare function contextLoadHook(filePath: string, triggerContext?: string): Promise<ContextLoadOutput>;
|
|
20
|
+
/**
|
|
21
|
+
* CLI entry point
|
|
22
|
+
*/
|
|
23
|
+
export declare function runContextLoadHook(): Promise<void>;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=context-load-hook.d.ts.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Load Hook
|
|
3
|
+
*
|
|
4
|
+
* Records context resource loading (project.md, rules, specs, knowledge, skills).
|
|
5
|
+
* This hook tracks what resources are loaded during each phase.
|
|
6
|
+
*/
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import { FileSystemUtils } from '../../utils/file-system.js';
|
|
10
|
+
import { getBeijingTime } from './record-progress.js';
|
|
11
|
+
/**
|
|
12
|
+
* Determine resource type from file path
|
|
13
|
+
*/
|
|
14
|
+
function determineResourceType(filePath) {
|
|
15
|
+
if (filePath.includes('project.md'))
|
|
16
|
+
return 'project.md';
|
|
17
|
+
if (filePath.includes('.claude/rules/'))
|
|
18
|
+
return 'rule';
|
|
19
|
+
if (filePath.includes('zhuanspec/specs/'))
|
|
20
|
+
return 'spec';
|
|
21
|
+
if (filePath.includes('zhuanspec/knowledge/'))
|
|
22
|
+
return 'knowledge';
|
|
23
|
+
if (filePath.includes('AGENTS.md'))
|
|
24
|
+
return 'AGENTS.md';
|
|
25
|
+
if (filePath.includes('CLAUDE.md'))
|
|
26
|
+
return 'CLAUDE.md';
|
|
27
|
+
if (filePath.includes('skills/') && filePath.includes('SKILL.md'))
|
|
28
|
+
return 'skill';
|
|
29
|
+
return 'other';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Determine load source based on trigger
|
|
33
|
+
*/
|
|
34
|
+
function determineLoadSource(triggerContext) {
|
|
35
|
+
if (triggerContext.includes('SessionStart'))
|
|
36
|
+
return 'init-hook';
|
|
37
|
+
if (triggerContext.includes('Skill'))
|
|
38
|
+
return 'skill-request';
|
|
39
|
+
if (triggerContext.includes('AskUser'))
|
|
40
|
+
return 'user-request';
|
|
41
|
+
return 'auto-load';
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get file size in characters
|
|
45
|
+
*/
|
|
46
|
+
async function getFileSizeChars(filePath) {
|
|
47
|
+
try {
|
|
48
|
+
const absolutePath = path.resolve(process.cwd(), filePath);
|
|
49
|
+
const content = await fs.promises.readFile(absolutePath, 'utf-8');
|
|
50
|
+
return content.length;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Main hook function - records context resource loading
|
|
58
|
+
*/
|
|
59
|
+
export async function contextLoadHook(filePath, triggerContext = 'auto-load') {
|
|
60
|
+
const phase = process.env.ZHUANSPEC_PHASE || 'idle';
|
|
61
|
+
const changeId = process.env.ZHUANSPEC_CHANGE_ID || '';
|
|
62
|
+
// Skip if not in active phase
|
|
63
|
+
if (!changeId || phase === 'idle') {
|
|
64
|
+
return { continue: true };
|
|
65
|
+
}
|
|
66
|
+
// Determine resource type
|
|
67
|
+
const resourceType = determineResourceType(filePath);
|
|
68
|
+
// Skip 'other' type resources
|
|
69
|
+
if (resourceType === 'other') {
|
|
70
|
+
return { continue: true };
|
|
71
|
+
}
|
|
72
|
+
const cwd = process.cwd();
|
|
73
|
+
const zhuanspecDir = path.join(cwd, 'zhuanspec');
|
|
74
|
+
const metricsDir = path.join(zhuanspecDir, 'changes', changeId, 'metrics');
|
|
75
|
+
// Ensure metrics directory exists
|
|
76
|
+
await FileSystemUtils.createDirectory(metricsDir);
|
|
77
|
+
const contextPath = path.join(metricsDir, 'context_resources.json');
|
|
78
|
+
// Create resource record
|
|
79
|
+
const resourceRecord = {
|
|
80
|
+
resourceId: `ctx-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
81
|
+
type: resourceType,
|
|
82
|
+
path: filePath,
|
|
83
|
+
loadedAt: getBeijingTime(),
|
|
84
|
+
phase,
|
|
85
|
+
sizeChars: await getFileSizeChars(filePath),
|
|
86
|
+
source: determineLoadSource(triggerContext)
|
|
87
|
+
};
|
|
88
|
+
// Load existing context records or create new
|
|
89
|
+
let contextResources;
|
|
90
|
+
if (await FileSystemUtils.fileExists(contextPath)) {
|
|
91
|
+
try {
|
|
92
|
+
contextResources = JSON.parse(await FileSystemUtils.readFile(contextPath));
|
|
93
|
+
contextResources.resources.push(resourceRecord);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
contextResources = {
|
|
97
|
+
changeId,
|
|
98
|
+
sessionId: `sess-${Date.now()}`,
|
|
99
|
+
resources: [resourceRecord]
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
contextResources = {
|
|
105
|
+
changeId,
|
|
106
|
+
sessionId: `sess-${Date.now()}`,
|
|
107
|
+
resources: [resourceRecord]
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// Write context resources file
|
|
111
|
+
await FileSystemUtils.writeFile(contextPath, JSON.stringify(contextResources, null, 2));
|
|
112
|
+
return {
|
|
113
|
+
continue: true,
|
|
114
|
+
systemMessage: `📚 Context loaded: ${resourceType} (${filePath}) | Size: ${resourceRecord.sizeChars} chars`,
|
|
115
|
+
hookSpecificOutput: {
|
|
116
|
+
resourceRecorded: true,
|
|
117
|
+
resourceType,
|
|
118
|
+
resourcePath: filePath
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* CLI entry point
|
|
124
|
+
*/
|
|
125
|
+
export async function runContextLoadHook() {
|
|
126
|
+
// Read stdin for tool input
|
|
127
|
+
let stdinData = {};
|
|
128
|
+
try {
|
|
129
|
+
const stdinContent = await readStdin();
|
|
130
|
+
if (stdinContent) {
|
|
131
|
+
stdinData = JSON.parse(stdinContent);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Ignore parse errors
|
|
136
|
+
}
|
|
137
|
+
const filePath = stdinData.tool_input?.file_path || '';
|
|
138
|
+
const triggerContext = stdinData.trigger_context || 'read-tool';
|
|
139
|
+
if (!filePath) {
|
|
140
|
+
console.log(JSON.stringify({ continue: true }));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const output = await contextLoadHook(filePath, triggerContext);
|
|
144
|
+
console.log(JSON.stringify(output));
|
|
145
|
+
}
|
|
146
|
+
async function readStdin() {
|
|
147
|
+
return new Promise((resolve) => {
|
|
148
|
+
let data = '';
|
|
149
|
+
process.stdin.setEncoding('utf8');
|
|
150
|
+
process.stdin.on('data', chunk => data += chunk);
|
|
151
|
+
process.stdin.on('end', () => resolve(data));
|
|
152
|
+
process.stdin.on('error', () => resolve(null));
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
// CLI execution
|
|
156
|
+
if (require.main === module) {
|
|
157
|
+
runContextLoadHook();
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=context-load-hook.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PreToolUse Hook - Deviation Check
|
|
3
|
+
*
|
|
4
|
+
* This hook runs before Write/Edit operations to check if the modification
|
|
5
|
+
* is within the approved proposal scope. It implements the "Reverse Sync" iron law.
|
|
6
|
+
*
|
|
7
|
+
* Logic:
|
|
8
|
+
* 1. If ZHUANSPEC_PHASE is 'idle' or no change bound → allow (normal workflow)
|
|
9
|
+
* 2. If ZHUANSPEC_PHASE is 'propose' → block (no code changes in propose phase)
|
|
10
|
+
* 3. If ZHUANSPEC_PHASE is 'apply' → check if file is in proposal scope:
|
|
11
|
+
* - In scope → allow
|
|
12
|
+
* - Out of scope → block with deviation handling options
|
|
13
|
+
*/
|
|
14
|
+
interface DeviationCheckOptions {
|
|
15
|
+
json?: boolean;
|
|
16
|
+
file?: string;
|
|
17
|
+
tool?: string;
|
|
18
|
+
trigger?: 'pre-tool' | 'post-prompt' | 'resume';
|
|
19
|
+
prompt?: string;
|
|
20
|
+
}
|
|
21
|
+
declare const PITFALL_KEYWORDS: string[];
|
|
22
|
+
declare const BEST_PRACTICE_KEYWORDS: string[];
|
|
23
|
+
declare const IMPLICIT_CONVENTION_KEYWORDS: string[];
|
|
24
|
+
declare const USER_CORRECTION_KEYWORDS: string[];
|
|
25
|
+
export declare function deviationCheckHook(options: DeviationCheckOptions): Promise<void>;
|
|
26
|
+
export { PITFALL_KEYWORDS, USER_CORRECTION_KEYWORDS, BEST_PRACTICE_KEYWORDS, IMPLICIT_CONVENTION_KEYWORDS };
|
|
27
|
+
//# sourceMappingURL=deviation-check.d.ts.map
|