@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.
Files changed (103) hide show
  1. package/bin/zhuanspec-hook.js +3 -0
  2. package/dist/cli/hooks.d.ts +14 -0
  3. package/dist/cli/hooks.js +465 -0
  4. package/dist/cli/index.js +100 -0
  5. package/dist/commands/artifact-workflow.js +15 -34
  6. package/dist/commands/design.d.ts +42 -0
  7. package/dist/commands/design.js +337 -0
  8. package/dist/commands/progress.d.ts +32 -0
  9. package/dist/commands/progress.js +278 -0
  10. package/dist/commands/review.d.ts +32 -0
  11. package/dist/commands/review.js +472 -0
  12. package/dist/commands/validate.d.ts +14 -0
  13. package/dist/commands/validate.js +161 -10
  14. package/dist/core/archive.d.ts +1 -0
  15. package/dist/core/archive.js +45 -3
  16. package/dist/core/completions/command-registry.js +67 -0
  17. package/dist/core/configurators/slash/amazon-q.js +32 -2
  18. package/dist/core/configurators/slash/antigravity.js +8 -2
  19. package/dist/core/configurators/slash/auggie.js +16 -1
  20. package/dist/core/configurators/slash/base.js +1 -1
  21. package/dist/core/configurators/slash/claude.d.ts +4 -0
  22. package/dist/core/configurators/slash/claude.js +56 -1
  23. package/dist/core/configurators/slash/cline.js +8 -2
  24. package/dist/core/configurators/slash/codebuddy.js +22 -1
  25. package/dist/core/configurators/slash/codex.js +21 -0
  26. package/dist/core/configurators/slash/costrict.js +15 -0
  27. package/dist/core/configurators/slash/crush.js +22 -1
  28. package/dist/core/configurators/slash/cursor.js +22 -1
  29. package/dist/core/configurators/slash/factory.js +16 -1
  30. package/dist/core/configurators/slash/gemini.js +8 -2
  31. package/dist/core/configurators/slash/github-copilot.js +19 -1
  32. package/dist/core/configurators/slash/iflow.js +22 -1
  33. package/dist/core/configurators/slash/kilocode.js +4 -1
  34. package/dist/core/configurators/slash/opencode.js +27 -0
  35. package/dist/core/configurators/slash/qoder.d.ts +4 -0
  36. package/dist/core/configurators/slash/qoder.js +59 -1
  37. package/dist/core/configurators/slash/qwen.js +8 -2
  38. package/dist/core/configurators/slash/roocode.js +8 -2
  39. package/dist/core/configurators/slash/windsurf.js +8 -2
  40. package/dist/core/dashboard/metrics.d.ts +33 -0
  41. package/dist/core/dashboard/metrics.js +114 -0
  42. package/dist/core/hooks/collect-knowledge.d.ts +16 -0
  43. package/dist/core/hooks/collect-knowledge.js +203 -0
  44. package/dist/core/hooks/context-load-hook.d.ts +25 -0
  45. package/dist/core/hooks/context-load-hook.js +159 -0
  46. package/dist/core/hooks/deviation-check.d.ts +27 -0
  47. package/dist/core/hooks/deviation-check.js +403 -0
  48. package/dist/core/hooks/deviation-handler.d.ts +43 -0
  49. package/dist/core/hooks/deviation-handler.js +98 -0
  50. package/dist/core/hooks/init.d.ts +14 -0
  51. package/dist/core/hooks/init.js +244 -0
  52. package/dist/core/hooks/notify-milestone.d.ts +14 -0
  53. package/dist/core/hooks/notify-milestone.js +170 -0
  54. package/dist/core/hooks/post-apply.d.ts +29 -0
  55. package/dist/core/hooks/post-apply.js +173 -0
  56. package/dist/core/hooks/post-archive.d.ts +7 -0
  57. package/dist/core/hooks/post-archive.js +208 -0
  58. package/dist/core/hooks/pre-apply.d.ts +34 -0
  59. package/dist/core/hooks/pre-apply.js +139 -0
  60. package/dist/core/hooks/pre-archive.d.ts +7 -0
  61. package/dist/core/hooks/pre-archive.js +50 -0
  62. package/dist/core/hooks/record-progress.d.ts +49 -0
  63. package/dist/core/hooks/record-progress.js +494 -0
  64. package/dist/core/hooks/review-hooks.d.ts +89 -0
  65. package/dist/core/hooks/review-hooks.js +345 -0
  66. package/dist/core/hooks/review-orchestrator.d.ts +40 -0
  67. package/dist/core/hooks/review-orchestrator.js +146 -0
  68. package/dist/core/hooks/summarize.d.ts +15 -0
  69. package/dist/core/hooks/summarize.js +282 -0
  70. package/dist/core/hooks/user-input-hook.d.ts +25 -0
  71. package/dist/core/hooks/user-input-hook.js +179 -0
  72. package/dist/core/init.d.ts +8 -0
  73. package/dist/core/init.js +251 -23
  74. package/dist/core/parsers/requirement-blocks.js +13 -10
  75. package/dist/core/templates/agents-template.d.ts +1 -1
  76. package/dist/core/templates/agents-template.js +510 -243
  77. package/dist/core/templates/index.d.ts +1 -0
  78. package/dist/core/templates/index.js +1 -0
  79. package/dist/core/templates/skill-templates.js +46 -152
  80. package/dist/core/templates/slash-command-templates.d.ts +1 -1
  81. package/dist/core/templates/slash-command-templates.js +352 -20
  82. package/dist/core/templates/tasks-template.d.ts +7 -0
  83. package/dist/core/templates/tasks-template.js +130 -24
  84. package/dist/core/templates/tdd-tasks-template.d.ts +3 -0
  85. package/dist/core/templates/tdd-tasks-template.js +91 -38
  86. package/dist/core/templates/test-cases-template.d.ts +41 -0
  87. package/dist/core/templates/test-cases-template.js +128 -0
  88. package/dist/core/validation/strict-rules.d.ts +44 -5
  89. package/dist/core/validation/strict-rules.js +302 -8
  90. package/dist/core/validation/validator.js +52 -2
  91. package/dist/core/view.d.ts +1 -0
  92. package/dist/core/view.js +60 -2
  93. package/dist/mcp/index.d.ts +28 -0
  94. package/dist/mcp/index.js +31 -0
  95. package/dist/utils/file-system.d.ts +1 -0
  96. package/dist/utils/file-system.js +11 -0
  97. package/dist/utils/item-discovery.js +24 -2
  98. package/dist/utils/phase-utils.d.ts +36 -0
  99. package/dist/utils/phase-utils.js +117 -0
  100. package/package.json +22 -23
  101. package/schemas/spec-driven/schema.yaml +45 -31
  102. package/schemas/spec-driven/templates/spec.md +142 -5
  103. 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
- archive: '.windsurf/workflows/zhuanspec-archive.md'
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
- archive: '归档已部署的 ZhuanSpec 变更并更新规范。'
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