@zhuan-ai/zhuanspec 2.9.5 → 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.
Files changed (36) hide show
  1. package/dist/cli/hooks.js +8 -8
  2. package/dist/cli/index.js +1 -0
  3. package/dist/commands/design.d.ts +12 -2
  4. package/dist/commands/design.js +83 -10
  5. package/dist/commands/progress.js +11 -0
  6. package/dist/commands/review.d.ts +1 -25
  7. package/dist/commands/review.js +51 -408
  8. package/dist/core/completions/command-registry.js +4 -0
  9. package/dist/core/hooks/collect-knowledge.d.ts +13 -5
  10. package/dist/core/hooks/collect-knowledge.js +56 -164
  11. package/dist/core/hooks/deviation-check.js +159 -55
  12. package/dist/core/hooks/init.js +46 -0
  13. package/dist/core/hooks/post-apply.js +2 -0
  14. package/dist/core/hooks/pre-archive.js +13 -14
  15. package/dist/core/hooks/pre-review.d.ts +27 -0
  16. package/dist/core/hooks/pre-review.js +169 -0
  17. package/dist/core/hooks/record-progress.d.ts +25 -0
  18. package/dist/core/hooks/record-progress.js +88 -1
  19. package/dist/core/hooks/review-hooks.js +176 -7
  20. package/dist/core/hooks/review-orchestrator.js +58 -11
  21. package/dist/core/hooks/tdd-phase-hook.d.ts +59 -0
  22. package/dist/core/hooks/tdd-phase-hook.js +313 -0
  23. package/dist/core/init.d.ts +1 -0
  24. package/dist/core/init.js +45 -2
  25. package/dist/core/templates/agents-template.d.ts +1 -1
  26. package/dist/core/templates/agents-template.js +139 -23
  27. package/dist/core/templates/skill-templates.js +21 -1
  28. package/dist/core/templates/slash-command-templates.js +163 -63
  29. package/dist/core/templates/tasks-template.js +89 -0
  30. package/dist/core/update.d.ts +3 -0
  31. package/dist/core/update.js +85 -2
  32. package/dist/core/validation/strict-rules.d.ts +47 -0
  33. package/dist/core/validation/strict-rules.js +589 -42
  34. package/dist/utils/git-repo-detector.js +1 -1
  35. package/dist/utils/phase-utils.js +14 -1
  36. package/package.json +22 -20
@@ -1,10 +1,7 @@
1
- import { execSync } from 'child_process';
2
- import { promises as fs, readdirSync, existsSync } from 'fs';
1
+ import { promises as fs } from 'fs';
3
2
  import path from 'path';
4
3
  import { FileSystemUtils } from '../utils/file-system.js';
5
- import { GitRepoDetector } from '../utils/git-repo-detector.js';
6
- import { mcp__sonar__get_Sonar_Query } from '../mcp/index.js';
7
- import { writeCodeReviewOutput, writeUnitTestOutput, writeSpecConsistencyOutput, codeReviewResultCheck, unitTestResultCheck, specConsistencyResultCheck, generateReviewReport, } from '../core/hooks/review-hooks.js';
4
+ import { codeReviewResultCheck, unitTestResultCheck, specConsistencyResultCheck, generateReviewReport, } from '../core/hooks/review-hooks.js';
8
5
  export class ReviewCommand {
9
6
  async execute(changeName, options) {
10
7
  if (!changeName) {
@@ -16,136 +13,46 @@ export class ReviewCommand {
16
13
  }
17
14
  const cwd = process.cwd();
18
15
  const changeDir = path.join(cwd, 'zhuanspec', 'changes', changeName);
19
- const metricsDir = path.join(changeDir, 'metrics');
16
+ const reviewDir = path.join(changeDir, 'review');
20
17
  const proposalPath = path.join(changeDir, 'proposal.md');
21
18
  const tasksPath = path.join(changeDir, 'tasks.md');
22
- const changeSpecsDir = path.join(changeDir, 'specs');
19
+ // Check proposal and tasks exist
23
20
  const proposalExists = await FileSystemUtils.fileExists(proposalPath);
24
21
  const tasksExists = await FileSystemUtils.fileExists(tasksPath);
25
22
  if (!proposalExists || !tasksExists) {
26
23
  throw new Error(`Change '${changeName}' is not ready for review. Missing proposal.md or tasks.md`);
27
24
  }
28
- // Detect all git repositories (parent + children)
29
- const allRepos = GitRepoDetector.detectAllGitRepos(cwd);
30
- const issues = [];
31
- const requirementNames = await this.collectRequirementNames(changeSpecsDir);
32
- // Use GitRepoDetector to get all changed files across all detected repos
33
- const touchedFiles = GitRepoDetector.getAllChanges(cwd).filter((file) => !file.startsWith('zhuanspec/changes/archive/'));
34
- const touchedContent = await this.collectTouchedFileContent(cwd, touchedFiles);
35
- // Track whether every requirement has at least one weak signal in changed code.
36
- for (const req of requirementNames) {
37
- const normalized = req.toLowerCase();
38
- const matched = touchedContent.some((content) => content.includes(normalized));
39
- if (!matched) {
40
- issues.push({
41
- severity: 'important',
42
- message: `Requirement may not be mapped to implementation: ${req}`,
43
- source: 'specCompliance',
44
- });
45
- }
46
- }
47
- const tasksContent = await fs.readFile(tasksPath, 'utf-8');
48
- const incompleteTasks = (tasksContent.match(/^-\s*\[\s\]\s+/gm) ?? []).length;
49
- if (incompleteTasks > 0) {
50
- issues.push({
51
- severity: 'critical',
52
- message: `Found ${incompleteTasks} incomplete tasks in tasks.md`,
53
- source: 'specCompliance',
54
- });
55
- }
56
- // Very lightweight code-quality checks.
57
- // touchedFiles now contains absolute paths from GitRepoDetector
58
- for (const file of touchedFiles) {
59
- if (file.endsWith('.ts') || (file.endsWith('.js') && !file.includes('node_modules'))) {
60
- const content = await fs.readFile(file, 'utf-8');
61
- if (content.includes('TODO') || content.includes('FIXME')) {
62
- issues.push({
63
- severity: 'minor',
64
- message: `Found TODO/FIXME in ${file}`,
65
- source: 'codeQuality',
66
- });
67
- }
68
- }
69
- }
70
- // Sonar MCP integration - fetch issues from SonarQube (use first detected repo if available)
71
- const sonarIssues = await this.fetchSonarIssues(allRepos);
72
- for (const issue of sonarIssues) {
73
- const severity = issue.type === 'BUG' || issue.type === 'VULNERABILITY' ? 'critical' : 'important';
74
- issues.push({
75
- severity,
76
- message: `Sonar ${issue.type}: ${issue.message} (${issue.file}${issue.line ? ':' + issue.line : ''})`,
77
- source: 'codeQuality',
78
- });
79
- }
80
- // Scenario→Test mapping validation
81
- const scenarios = await this.collectScenarios(changeSpecsDir);
82
- const testFiles = await this.findTestFiles(cwd);
83
- for (const scenario of scenarios) {
84
- const testPatterns = this.scenarioToTestMethodNames(scenario.name);
85
- const matched = testFiles.some(tf => testPatterns.some(p => tf.toLowerCase().includes(p.toLowerCase())));
86
- if (!matched) {
87
- issues.push({
88
- severity: 'important',
89
- message: `Scenario "${scenario.name}" 未找到对应测试文件`,
90
- source: 'specCompliance',
91
- });
92
- }
93
- }
94
- const testCommand = options?.testCommand?.trim() || 'npm test';
95
- const testResult = this.runTests(cwd, testCommand);
96
- const coverageThreshold = Number(options?.coverageThreshold ?? 80);
97
- if (testResult.coverage !== null && testResult.coverage < coverageThreshold) {
98
- issues.push({
99
- severity: 'important',
100
- message: `Coverage ${testResult.coverage}% is below threshold ${coverageThreshold}%`,
101
- source: 'unitTests',
102
- });
103
- }
104
- if (testResult.failed > 0) {
105
- issues.push({
106
- severity: 'critical',
107
- message: `Unit tests failed: ${testResult.failed}`,
108
- source: 'unitTests',
109
- });
110
- }
111
- const summary = this.buildSummary(issues, testResult, testCommand);
112
- const consistency = this.buildSpecConsistency(requirementNames, scenarios, touchedFiles, testFiles);
113
- // Persist skill-compatible outputs so hooks and command share one result chain.
114
- await writeCodeReviewOutput(changeName, {
115
- criticalCount: summary.summary.critical,
116
- importantCount: summary.summary.important,
117
- infoCount: summary.summary.minor,
118
- issues: summary.issues.map((issue) => ({
119
- level: issue.severity === 'minor' ? 'info' : issue.severity,
120
- file: issue.source,
121
- message: issue.message,
122
- })),
123
- sonarStatus: sonarIssues.length > 0 ? 'pass' : 'skip',
124
- loopCount: 0,
125
- });
126
- await writeUnitTestOutput(changeName, {
127
- testPassed: testResult.failed === 0,
128
- passRate: testResult.failed === 0 ? 100 : 0,
129
- coverage: testResult.coverage ?? 0,
130
- coverageThreshold,
131
- newTestsGenerated: [],
132
- failedTests: testResult.failed > 0 ? ['test-command-failed'] : [],
133
- loopCount: 0,
134
- });
135
- await writeSpecConsistencyOutput(changeName, {
136
- consistencyRate: consistency.consistencyRate,
137
- totalRequirements: consistency.totalRequirements,
138
- totalScenarios: consistency.totalScenarios,
139
- coveredScenarios: consistency.coveredScenarios,
140
- uncoveredScenarios: consistency.uncoveredScenarios.map((scenario, idx) => ({
141
- requirementId: `REQ-${String(idx + 1).padStart(3, '0')}`,
142
- scenarioId: `SC-${String(idx + 1).padStart(3, '0')}`,
143
- description: scenario.name,
144
- })),
145
- mapping: consistency.mapping,
146
- loopCount: 0,
147
- });
148
- // Build unified review-report.md from hook checkers.
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
+ }
38
+ // Check skill output JSON files exist
39
+ const codeReviewJsonPath = path.join(reviewDir, 'code-review-result.json');
40
+ const unitTestJsonPath = path.join(reviewDir, 'unit-test-result.json');
41
+ const missingFiles = [];
42
+ if (!(await FileSystemUtils.fileExists(codeReviewJsonPath))) {
43
+ missingFiles.push('code-review-result.json(需先运行 code-review-expert skill)');
44
+ }
45
+ if (!(await FileSystemUtils.fileExists(unitTestJsonPath))) {
46
+ missingFiles.push('unit-test-result.json(需先运行 generate-mockito-unit-test skill)');
47
+ }
48
+ if (missingFiles.length > 0) {
49
+ console.error(`❌ skill 输出缺失,请先执行以下步骤:`);
50
+ missingFiles.forEach((f, i) => console.error(` ${i + 1}. ${f}`));
51
+ console.error(` ${missingFiles.length + 1}. spec-consistency-result.json(运行 zhuanspec validate <id> --strict 后由 AI 写入)`);
52
+ process.exitCode = 1;
53
+ return;
54
+ }
55
+ // Read hook results and generate report
149
56
  const codeReviewResult = await codeReviewResultCheck(changeName);
150
57
  const unitTestResult = await unitTestResultCheck(changeName);
151
58
  const specConsistencyResult = await specConsistencyResultCheck(changeName);
@@ -154,16 +61,25 @@ export class ReviewCommand {
154
61
  unitTest: unitTestResult,
155
62
  specConsistency: specConsistencyResult,
156
63
  });
157
- await FileSystemUtils.createDirectory(metricsDir);
158
- await FileSystemUtils.writeFile(path.join(metricsDir, 'review.json'), JSON.stringify(summary, null, 2));
159
- await FileSystemUtils.writeFile(path.join(metricsDir, 'tests.json'), JSON.stringify(testResult, null, 2));
64
+ // Output results
65
+ const overallPass = codeReviewResult.pass && unitTestResult.pass && specConsistencyResult.pass;
160
66
  if (options?.json) {
161
- console.log(JSON.stringify(summary, null, 2));
67
+ console.log(JSON.stringify({
68
+ passed: overallPass,
69
+ codeReview: codeReviewResult,
70
+ unitTest: unitTestResult,
71
+ specConsistency: specConsistencyResult,
72
+ }, null, 2));
162
73
  }
163
74
  else {
164
- this.printSummary(changeName, summary);
165
- }
166
- process.exitCode = summary.passed ? 0 : 1;
75
+ console.log(`Review report for ${changeName}`);
76
+ console.log(`Overall: ${overallPass ? 'PASS ✅' : 'FAIL ❌'}`);
77
+ console.log(`Code Review: ${codeReviewResult.pass ? 'PASS' : 'FAIL'} (critical=${codeReviewResult.metrics.criticalCount ?? 0})`);
78
+ console.log(`Unit Test: ${unitTestResult.pass ? 'PASS' : 'FAIL'} (coverage=${unitTestResult.metrics.coverage ?? 0}%)`);
79
+ console.log(`Spec-Code: ${specConsistencyResult.pass ? 'PASS' : 'FAIL'} (consistency=${specConsistencyResult.metrics.consistencyRate ?? 0}%)`);
80
+ console.log(`Report: ${path.join('zhuanspec/changes', changeName, 'review/review-report.md')}`);
81
+ }
82
+ process.exitCode = overallPass ? 0 : 1;
167
83
  }
168
84
  async selectChangeInteractively() {
169
85
  const changesDir = path.join(process.cwd(), 'zhuanspec', 'changes');
@@ -195,278 +111,5 @@ export class ReviewCommand {
195
111
  return null;
196
112
  }
197
113
  }
198
- async collectRequirementNames(changeSpecsDir) {
199
- const requirements = [];
200
- const walk = async (dir) => {
201
- let entries = [];
202
- try {
203
- entries = await fs.readdir(dir, { withFileTypes: true });
204
- }
205
- catch {
206
- return;
207
- }
208
- for (const entry of entries) {
209
- const full = path.join(dir, entry.name);
210
- if (entry.isDirectory()) {
211
- await walk(full);
212
- continue;
213
- }
214
- if (entry.isFile() && entry.name === 'spec.md') {
215
- const content = await fs.readFile(full, 'utf-8');
216
- const matches = content.match(/^###\s+(Requirement|需求):\s+(.+)$/gim) ?? [];
217
- for (const line of matches) {
218
- const parts = line.split(':');
219
- const name = parts.slice(1).join(':').trim();
220
- if (name)
221
- requirements.push(name);
222
- }
223
- }
224
- }
225
- };
226
- await walk(changeSpecsDir);
227
- return Array.from(new Set(requirements));
228
- }
229
- async collectTouchedFileContent(repoRoot, files) {
230
- const result = [];
231
- for (const file of files) {
232
- try {
233
- const full = path.isAbsolute(file) ? file : path.join(repoRoot, file);
234
- const stat = await fs.stat(full);
235
- if (!stat.isFile())
236
- continue;
237
- const content = await fs.readFile(full, 'utf-8');
238
- result.push(content.toLowerCase());
239
- }
240
- catch {
241
- // ignore unreadable files
242
- }
243
- }
244
- return result;
245
- }
246
- runTests(repoRoot, command) {
247
- try {
248
- const output = execSync(command, { cwd: repoRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
249
- const coverageMatch = output.match(/(\d+(?:\.\d+)?)%\s*(?:\|)?\s*Statements/i);
250
- return {
251
- passed: 1,
252
- failed: 0,
253
- coverage: coverageMatch ? Number(coverageMatch[1]) : null,
254
- };
255
- }
256
- catch (error) {
257
- const stdout = error?.stdout?.toString?.() ?? '';
258
- const stderr = error?.stderr?.toString?.() ?? '';
259
- const combined = `${stdout}\n${stderr}`;
260
- const coverageMatch = combined.match(/(\d+(?:\.\d+)?)%\s*(?:\|)?\s*Statements/i);
261
- return {
262
- passed: 0,
263
- failed: 1,
264
- coverage: coverageMatch ? Number(coverageMatch[1]) : null,
265
- };
266
- }
267
- }
268
- buildSummary(issues, testResult, command) {
269
- const critical = issues.filter((i) => i.severity === 'critical').length;
270
- const important = issues.filter((i) => i.severity === 'important').length;
271
- const minor = issues.filter((i) => i.severity === 'minor').length;
272
- return {
273
- passed: critical === 0 && testResult.failed === 0,
274
- summary: { critical, important, minor },
275
- checks: {
276
- specCompliance: !issues.some((i) => i.source === 'specCompliance' && i.severity === 'critical'),
277
- codeQuality: !issues.some((i) => i.source === 'codeQuality' && i.severity === 'critical'),
278
- unitTests: testResult.failed === 0,
279
- },
280
- test: {
281
- passed: testResult.passed,
282
- failed: testResult.failed,
283
- coverage: testResult.coverage,
284
- command,
285
- },
286
- issues,
287
- generatedAt: new Date().toISOString(),
288
- };
289
- }
290
- buildSpecConsistency(requirementNames, scenarios, touchedFiles, testFiles) {
291
- const normalizedTouched = touchedFiles.map((file) => file.toLowerCase());
292
- const normalizedTests = testFiles.map((file) => file.toLowerCase());
293
- const mapping = [];
294
- const uncoveredScenarios = [];
295
- scenarios.forEach((scenario, idx) => {
296
- const patterns = this.scenarioToTestMethodNames(scenario.name).map((p) => p.toLowerCase());
297
- const testCovered = normalizedTests.some((testFile) => patterns.some((p) => testFile.includes(p)));
298
- const codeCovered = normalizedTouched.some((codeFile) => patterns.some((p) => codeFile.includes(p)));
299
- const status = testCovered && codeCovered
300
- ? 'covered'
301
- : (testCovered || codeCovered ? 'partial' : 'missing');
302
- mapping.push({
303
- requirementId: `REQ-${String((idx % Math.max(requirementNames.length, 1)) + 1).padStart(3, '0')}`,
304
- scenarioId: `SC-${String(idx + 1).padStart(3, '0')}`,
305
- codeFile: touchedFiles[0] || '',
306
- status,
307
- });
308
- if (status !== 'covered') {
309
- uncoveredScenarios.push(scenario);
310
- }
311
- });
312
- const totalScenarios = scenarios.length;
313
- const coveredScenarios = totalScenarios - uncoveredScenarios.length;
314
- const consistencyRate = totalScenarios === 0 ? 100 : Math.round((coveredScenarios / totalScenarios) * 100);
315
- return {
316
- consistencyRate,
317
- totalRequirements: requirementNames.length,
318
- totalScenarios,
319
- coveredScenarios,
320
- uncoveredScenarios,
321
- mapping,
322
- };
323
- }
324
- printSummary(changeName, summary) {
325
- console.log(`Review report for ${changeName}`);
326
- console.log(`Passed: ${summary.passed ? 'YES' : 'NO'}`);
327
- console.log(`Issues: critical=${summary.summary.critical}, important=${summary.summary.important}, minor=${summary.summary.minor}`);
328
- console.log(`Tests: passed=${summary.test.passed}, failed=${summary.test.failed}, coverage=${summary.test.coverage ?? 'N/A'}`);
329
- if (summary.issues.length > 0) {
330
- console.log('Findings:');
331
- for (const item of summary.issues) {
332
- console.log(`- [${item.severity}] (${item.source}) ${item.message}`);
333
- }
334
- }
335
- }
336
- /**
337
- * Fetch Sonar issues using MCP
338
- * Uses the first detected repository's branch for SonarQube queries
339
- */
340
- async fetchSonarIssues(allRepos) {
341
- // If no repos detected, skip Sonar queries
342
- if (allRepos.length === 0) {
343
- return [];
344
- }
345
- try {
346
- // Use the first detected repository's branch
347
- const primaryRepo = allRepos[0];
348
- const branch = execSync('git branch --show-current', {
349
- cwd: primaryRepo,
350
- encoding: 'utf-8',
351
- stdio: ['pipe', 'pipe', 'ignore'],
352
- }).trim();
353
- // Query different types
354
- const types = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
355
- const allIssues = [];
356
- for (const type of types) {
357
- try {
358
- const result = await mcp__sonar__get_Sonar_Query({
359
- branchName: branch,
360
- staticType: type,
361
- });
362
- if (result && Array.isArray(result)) {
363
- for (const issue of result) {
364
- allIssues.push({
365
- type: type,
366
- severity: issue.severity || 'MAJOR',
367
- message: issue.message || '',
368
- file: issue.file || '',
369
- line: issue.line,
370
- });
371
- }
372
- }
373
- }
374
- catch {
375
- // Sonar MCP might not be configured, skip silently
376
- }
377
- }
378
- return allIssues;
379
- }
380
- catch {
381
- // Git command failed or Sonar not available
382
- return [];
383
- }
384
- }
385
- /**
386
- * Collect all Scenarios from delta specs
387
- */
388
- async collectScenarios(changeSpecsDir) {
389
- const scenarios = [];
390
- const walk = async (dir) => {
391
- let entries = [];
392
- try {
393
- entries = await fs.readdir(dir, { withFileTypes: true });
394
- }
395
- catch {
396
- return;
397
- }
398
- for (const entry of entries) {
399
- const full = path.join(dir, entry.name);
400
- if (entry.isDirectory()) {
401
- await walk(full);
402
- continue;
403
- }
404
- if (entry.isFile() && entry.name === 'spec.md') {
405
- const content = await fs.readFile(full, 'utf-8');
406
- // Match Scenario headers
407
- const matches = content.matchAll(/^####\s+(Scenario|场景)[::]\s+(.+)$/gim);
408
- for (const match of matches) {
409
- const name = match[2].trim();
410
- scenarios.push({ name, specFile: full });
411
- }
412
- }
413
- }
414
- };
415
- await walk(changeSpecsDir);
416
- return scenarios;
417
- }
418
- /**
419
- * Find test files in the project
420
- */
421
- async findTestFiles(repoRoot) {
422
- const testFiles = [];
423
- const testDirs = ['test', 'tests', 'src/test', 'spec', '__tests__'];
424
- for (const testDir of testDirs) {
425
- const fullDir = path.join(repoRoot, testDir);
426
- try {
427
- if (!existsSync(fullDir))
428
- continue;
429
- const walk = (dir) => {
430
- const entries = readdirSync(dir, { withFileTypes: true });
431
- for (const entry of entries) {
432
- const full = path.join(dir, entry.name);
433
- if (entry.isDirectory()) {
434
- walk(full);
435
- }
436
- else if (entry.isFile() && (entry.name.endsWith('.test.ts') ||
437
- entry.name.endsWith('.test.js') ||
438
- entry.name.endsWith('.spec.ts') ||
439
- entry.name.endsWith('.spec.js') ||
440
- entry.name.includes('Test'))) {
441
- testFiles.push(path.relative(repoRoot, full));
442
- }
443
- }
444
- };
445
- walk(fullDir);
446
- }
447
- catch {
448
- // Directory doesn't exist or can't be read
449
- }
450
- }
451
- return testFiles;
452
- }
453
- /**
454
- * Convert Scenario name to possible test method names
455
- */
456
- scenarioToTestMethodNames(scenarioName) {
457
- const name = scenarioName.replace(/[::]/g, '').trim();
458
- return [
459
- // snake_case
460
- name.replace(/\s+/g, '_').toLowerCase(),
461
- // camelCase
462
- name.replace(/\s+/g, '').toLowerCase(),
463
- // PascalCase
464
- name.split(/\s+/).map(w => w[0]?.toUpperCase() + w.slice(1).toLowerCase()).join(''),
465
- // Remove Chinese characters for keyword matching
466
- name.replace(/[^\w\s]/g, '').trim().toLowerCase(),
467
- // Key words from scenario name
468
- ...name.split(/\s+/).filter(w => w.length > 3),
469
- ].filter(n => n.length > 0);
470
- }
471
114
  }
472
115
  //# sourceMappingURL=review.js.map
@@ -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)',
@@ -1,16 +1,24 @@
1
1
  /**
2
2
  * PostToolUse Hook - Collect Knowledge
3
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
4
+ * Fires after Write/Edit in Apply phase. Injects a lightweight prompt
5
+ * into Claude's context so the LLM can decide (with user confirmation)
6
+ * whether the change is worth capturing as project knowledge.
7
+ *
8
+ * Design principle: no keyword matching, no auto-save — the LLM decides.
8
9
  */
9
10
  interface CollectKnowledgeOptions {
10
11
  json?: boolean;
11
12
  file?: string;
12
- content?: string;
13
+ }
14
+ interface HookOutput {
15
+ continue: boolean;
16
+ systemMessage?: string;
17
+ hookSpecificOutput?: {
18
+ additionalContext?: string;
19
+ };
13
20
  }
14
21
  export declare function collectKnowledgeHook(options: CollectKnowledgeOptions): Promise<void>;
22
+ export declare function runCollectKnowledge(filePath: string): Promise<HookOutput>;
15
23
  export {};
16
24
  //# sourceMappingURL=collect-knowledge.d.ts.map