@educa-corp/sdd-framework 0.5.0 → 0.6.0

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 (115) hide show
  1. package/bin/build.js +113 -19
  2. package/bin/gate-trace.js +464 -0
  3. package/bin/index.js +418 -146
  4. package/bin/lint-trace.js +602 -0
  5. package/bin/self-check.js +376 -1
  6. package/bin/trace-schema.json +252 -2
  7. package/commands/debug.md +123 -511
  8. package/commands/debug.tmpl +3 -0
  9. package/commands/define-product.md +86 -510
  10. package/commands/dev-gen-test.md +86 -510
  11. package/commands/dev-run-test.md +86 -510
  12. package/commands/dev-smoke-test.md +86 -510
  13. package/commands/extend-prd.md +89 -510
  14. package/commands/extend-prd.tmpl +3 -0
  15. package/commands/fix-bug.md +118 -509
  16. package/commands/generate-architecture.md +94 -515
  17. package/commands/generate-architecture.tmpl +3 -0
  18. package/commands/generate-bdd.md +85 -509
  19. package/commands/generate-code.md +86 -510
  20. package/commands/generate-design-spec.md +86 -510
  21. package/commands/generate-prd.md +89 -510
  22. package/commands/generate-prd.tmpl +3 -0
  23. package/commands/generate-spec-manifest.md +86 -510
  24. package/commands/generate-tech-docs.md +86 -510
  25. package/commands/learn.md +172 -496
  26. package/commands/learn.tmpl +70 -3
  27. package/commands/map-testids.md +86 -510
  28. package/commands/propose-scenario.md +86 -510
  29. package/commands/qc-analyze.md +86 -510
  30. package/commands/qc-design-test.md +86 -510
  31. package/commands/qc-plan.md +86 -510
  32. package/commands/qc-report.md +86 -510
  33. package/commands/qc-review.md +86 -510
  34. package/commands/qc-run-test.md +86 -510
  35. package/commands/refine-prd.md +99 -520
  36. package/commands/refine-prd.tmpl +3 -0
  37. package/commands/report-bug.md +86 -510
  38. package/commands/review-code.md +123 -511
  39. package/commands/review-code.tmpl +3 -0
  40. package/commands/review-context.md +93 -514
  41. package/commands/review-context.tmpl +3 -0
  42. package/commands/review-tech-docs.md +90 -511
  43. package/commands/review-tech-docs.tmpl +3 -0
  44. package/commands/setup-ai-first.md +166 -138
  45. package/commands/setup-ai-first.tmpl +72 -0
  46. package/commands/sync.md +50 -106
  47. package/commands/sync.tmpl +48 -3
  48. package/commands/update-framework.md +16 -103
  49. package/commands/update-framework.tmpl +14 -0
  50. package/commands/validate-traces.md +153 -511
  51. package/commands/validate-traces.tmpl +67 -1
  52. package/core/FRAMEWORK_VERSION +1 -1
  53. package/core/README.md +20 -0
  54. package/core/commands/debug.md +123 -511
  55. package/core/commands/define-product.md +86 -510
  56. package/core/commands/dev-gen-test.md +86 -510
  57. package/core/commands/dev-run-test.md +86 -510
  58. package/core/commands/dev-smoke-test.md +86 -510
  59. package/core/commands/extend-prd.md +89 -510
  60. package/core/commands/fix-bug.md +118 -509
  61. package/core/commands/generate-architecture.md +94 -515
  62. package/core/commands/generate-bdd.md +85 -509
  63. package/core/commands/generate-code.md +86 -510
  64. package/core/commands/generate-design-spec.md +86 -510
  65. package/core/commands/generate-prd.md +89 -510
  66. package/core/commands/generate-spec-manifest.md +86 -510
  67. package/core/commands/generate-tech-docs.md +86 -510
  68. package/core/commands/learn.md +172 -496
  69. package/core/commands/map-testids.md +86 -510
  70. package/core/commands/propose-scenario.md +86 -510
  71. package/core/commands/qc-analyze.md +86 -510
  72. package/core/commands/qc-design-test.md +86 -510
  73. package/core/commands/qc-plan.md +86 -510
  74. package/core/commands/qc-report.md +86 -510
  75. package/core/commands/qc-review.md +86 -510
  76. package/core/commands/qc-run-test.md +86 -510
  77. package/core/commands/refine-prd.md +99 -520
  78. package/core/commands/report-bug.md +86 -510
  79. package/core/commands/review-code.md +123 -511
  80. package/core/commands/review-context.md +93 -514
  81. package/core/commands/review-tech-docs.md +90 -511
  82. package/core/commands/setup-ai-first.md +166 -138
  83. package/core/commands/sync.md +50 -106
  84. package/core/commands/update-framework.md +16 -103
  85. package/core/commands/validate-traces.md +153 -511
  86. package/core/hooks/data-guard.js +174 -83
  87. package/core/hooks/settings.json +2 -1
  88. package/core/rules/workflow.md +30 -4
  89. package/core/steps/capture-lesson.md +34 -1
  90. package/core/steps/context-loader.md +24 -3
  91. package/core/steps/gate.md +92 -35
  92. package/core/steps/report-footer.md +23 -0
  93. package/core/templates/README.md +24 -1
  94. package/core/templates/ci/trace-gate.yml +146 -0
  95. package/core/templates/hooks/pre-push +61 -0
  96. package/docs/02-concepts/architecture.md +25 -6
  97. package/docs/02-concepts/traceability.md +57 -0
  98. package/docs/03-guides/architect.md +63 -0
  99. package/docs/04-reference/commands.md +1 -1
  100. package/docs/04-reference/model-selection.md +32 -19
  101. package/docs/explain/21-validate-traces.md +2 -1
  102. package/docs/explain/27-learn.md +5 -3
  103. package/hooks/data-guard.js +174 -83
  104. package/hooks/settings.json +2 -1
  105. package/package.json +5 -2
  106. package/rules/workflow.md +30 -4
  107. package/steps/capture-lesson.md +34 -1
  108. package/steps/context-loader.md +24 -3
  109. package/steps/gate.md +92 -35
  110. package/steps/report-footer.md +23 -0
  111. package/templates/README.md +24 -1
  112. package/templates/ci/trace-gate.yml +146 -0
  113. package/templates/hooks/pre-push +61 -0
  114. package/scripts/init.sh +0 -49
  115. package/scripts/upgrade.sh +0 -94
package/bin/index.js CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const os = require('os');
6
5
 
7
6
  const ROOT = path.join(__dirname, '..');
8
7
 
@@ -19,6 +18,8 @@ const args = process.argv.slice(2);
19
18
  const isMigrateSpecs = args.includes('--migrate-specs');
20
19
  const isMigrateBddPf = args.includes('--migrate-bdd-platform');
21
20
  const isRenamePrd = args.includes('--rename-prd-files');
21
+ const isLintTrace = args.includes('--lint-trace');
22
+ const isGateTrace = args.includes('--gate-trace');
22
23
  const isInit = args.includes('--init');
23
24
  const isProject = args.includes('--project');
24
25
  const installHooks = args.includes('--hooks');
@@ -80,12 +81,13 @@ if (showHelp) {
80
81
  console.log('');
81
82
  console.log('Install modes:');
82
83
  console.log(' --init Install framework to .agent/ + create shortcuts in .claude/commands/');
83
- console.log(' Recommended for new projects. Run upgrade.sh to upgrade later.');
84
+ console.log(' Recommended for new projects. Later: /update-framework in Claude Code.');
84
85
  console.log(' --init --umbrella Umbrella repo setup: installs framework at umbrella level only.');
85
86
  console.log(' Generates project-context.yaml with services routing.');
86
87
  console.log(' Does NOT install into service submodule directories.');
87
- console.log(' --project Install full commands to ./.claude/commands/ (project-scoped, legacy)');
88
- console.log(' (no flag) Install full commands to ~/.claude/commands/ (global, legacy)');
88
+ console.log('');
89
+ console.log(' (--project chế độ không-cờ đã được GỠ ở v0.5.1 — chúng cài lệnh mà không cài');
90
+ console.log(' .agent/, nên lệnh chạy thiếu guardrail và pipeline QC không hoạt động. Dùng --init.)');
89
91
  console.log('');
90
92
  console.log('Maintenance:');
91
93
  console.log(' --migrate-specs Migrate an existing project from the legacy artifact-type-first');
@@ -99,6 +101,23 @@ if (showHelp) {
99
101
  console.log(' else step vocabulary; inserts @trace.platform when missing. Reports');
100
102
  console.log(' filename COLLISION and LOST SPEC instead of guessing.');
101
103
  console.log(' DRY-RUN by default; add --apply to execute. Uses git mv when possible.');
104
+ console.log(' --lint-trace Kiểm SỔ TRACE THẬT (.trace/**) đối chiếu bin/trace-schema.json:');
105
+ console.log(' header 24 cột · số ô mỗi row · giá trị enum · sc_id trùng · tên file');
106
+ console.log(' {UC-ID}-{platform}.tsv · ô ngày · marker conflict git · trace-history.jsonl.');
107
+ console.log(' Read-only, KHÔNG sửa gì. Exit 1 nếu có lỗi → cắm được vào CI/pre-push.');
108
+ console.log(' Options: --trace DIR[,DIR] (mặc định .trace) · --specs DIR (mặc định specs)');
109
+ console.log(' --warn-only (luôn exit 0) · --json');
110
+ console.log(' Vì sao: self-check canh CONTRACT (file lệnh); cái này canh DỮ LIỆU.');
111
+ console.log(' Sổ trace do LLM ghi tay, 24 cột — một tab lệch là không ai thấy (G38).');
112
+ console.log('');
113
+ console.log(' --gate-trace CỔNG CHẶN PR — exit 1 khi trace có cờ 🔴 (ORPHANED · TRACE_ORPHAN ·');
114
+ console.log(' SEAM_UNWIRED · STUB_UNRESOLVED). Đây là lớp lỗi mà build xanh +');
115
+ console.log(' test từng-UC xanh KHÔNG phát hiện được. Read-only.');
116
+ console.log(' Ba tầng: sổ đúng hình dạng (gọi --lint-trace) → report còn TƯƠI');
117
+ console.log(' (đối chiếu với sổ TSV) → không có cờ 🔴.');
118
+ console.log(' Options: --trace DIR[,DIR] · --report PATH · --skip-lint');
119
+ console.log(' --no-reconcile · --warn-only · --json');
120
+ console.log('');
102
121
  console.log(' --rename-prd-files Rename feature-package PRD files from the old fixed name prd.md to the');
103
122
  console.log(' {TICKET-ID}-{prd-slug}.md convention (e.g. SEG01-segment-scoring-service.md).');
104
123
  console.log(' For projects already on the feature-package layout. DRY-RUN by default;');
@@ -125,7 +144,8 @@ if (showHelp) {
125
144
  console.log(' npx @educa-corp/sdd-framework --init --umbrella \\');
126
145
  console.log(' --spec-source free-trial-specs \\');
127
146
  console.log(' --services mass-product-web:nextjs # umbrella with one FE service');
128
- console.log(' npx @educa-corp/sdd-framework --project --hooks # legacy project install');
147
+ console.log(' npx @educa-corp/sdd-framework --lint-trace # kiểm sổ trace (CI / pre-push)');
148
+ console.log(' npx @educa-corp/sdd-framework --lint-trace --warn-only # xem nợ tồn, chưa chặn');
129
149
  process.exit(0);
130
150
  }
131
151
 
@@ -149,15 +169,52 @@ if (isRenamePrd) {
149
169
  return; // rename-prd-files.js calls process.exit itself
150
170
  }
151
171
 
152
- // ── Step 1: Build .tmpl .md ─────────────────────────────────────────────
172
+ // ── --lint-trace: kiểm sổ trace thật của người dùng (G38) ──────────────────
173
+ // Read-only, đi trước bước build vì nó làm việc trên .trace/ của project, không phải
174
+ // package. Cùng khuôn với các flag maintenance ở trên.
175
+ if (isLintTrace) {
176
+ require(path.join(__dirname, 'lint-trace.js'));
177
+ return; // lint-trace.js calls process.exit itself
178
+ }
179
+
180
+ // ── --gate-trace: cổng chặn PR — exit 1 khi có cờ 🔴 (G39) ─────────────────
181
+ // Cũng read-only. Tự gọi lint-trace ở tầng G1, nên một lệnh là đủ cho CI.
182
+ if (isGateTrace) {
183
+ require(path.join(__dirname, 'gate-trace.js'));
184
+ return; // gate-trace.js calls process.exit itself
185
+ }
186
+
187
+ // ── Step 1: Build .tmpl → .md — CHỈ khi cần ───────────────────────────────
188
+ //
189
+ // Tarball publish đã chứa `core/` dựng sẵn (npm pack: 138 entry). Build lại ở máy
190
+ // người dùng không đem lại gì và mang rủi ro thật (GAPS-v3 G43): nó ghi vào THƯ MỤC
191
+ // PACKAGE — tức npx cache, hoặc node_modules của bản cài global. Thư mục đó có thể
192
+ // read-only (CI doanh nghiệp, `sudo npm i -g` rồi chạy bằng user thường), và hai
193
+ // `--init` chạy đồng thời ở hai project dùng chung cache sẽ đua nhau trên cùng core/.
194
+ //
195
+ // Nên: build CHỈ khi core/ vắng mặt hoặc lệch version — tức chỉ trong dev checkout.
153
196
  const buildScript = path.join(__dirname, 'build.js');
154
- if (fs.existsSync(buildScript)) {
197
+ const coreVersionFile = path.join(ROOT, 'core', 'FRAMEWORK_VERSION');
198
+
199
+ let corePrebuilt = false;
200
+ try {
201
+ corePrebuilt = fs.readFileSync(coreVersionFile, 'utf8').trim() === VERSION;
202
+ } catch { /* không có file = chưa build */ }
203
+
204
+ if (!corePrebuilt && fs.existsSync(buildScript)) {
155
205
  try {
156
206
  require(buildScript);
157
207
  } catch (err) {
158
208
  console.error('Build step failed:', err.message);
159
209
  process.exit(1);
160
210
  }
211
+ } else if (corePrebuilt && fs.existsSync(path.join(ROOT, 'commands', 'generate-code.tmpl'))) {
212
+ // Chỉ nói trong DEV CHECKOUT (có .tmpl nguồn). Consumer không cần biết bước này tồn tại.
213
+ // Cần thiết vì trước G43 thì `--init` luôn build, nên "sửa .tmpl rồi --init" từng có tác
214
+ // dụng. Giờ không — và một thay đổi lặng lẽ như thế sẽ tốn của ai đó nửa tiếng.
215
+ console.log('');
216
+ console.log(`ℹ️ core/ đã build sẵn (v${VERSION}) — bỏ qua bước build.`);
217
+ console.log(' Vừa sửa commands/*.tmpl · steps/ · templates/ ? Chạy `npm run build` trước.');
161
218
  }
162
219
 
163
220
  // ── --init mode: install to .agent/ + create .claude/commands/ shortcuts ─────
@@ -205,27 +262,10 @@ if (isInit) {
205
262
  console.log('');
206
263
  console.log('Creating .claude/commands/ shortcuts ...');
207
264
  console.log('');
208
- fs.mkdirSync(claudeCommandsDir, { recursive: true });
209
-
210
- const agentCommandsDir = path.join(agentDir, 'commands');
211
- const commandFiles = fs.readdirSync(agentCommandsDir).filter(f => f.endsWith('.md'));
212
- const failedShortcuts = [];
213
-
214
- for (const cmdFile of commandFiles) {
215
- const cmdName = cmdFile.replace('.md', '');
216
- const shortcut =
217
- `# /${cmdName}\n` +
218
- `\n` +
219
- `Read the full command definition from \`.agent/commands/${cmdFile}\`` +
220
- ` and execute it with arguments: $ARGUMENTS\n`;
221
- try {
222
- fs.writeFileSync(path.join(claudeCommandsDir, cmdFile), shortcut, 'utf8');
223
- console.log(` ✅ /${cmdName}`);
224
- } catch {
225
- console.log(` ❌ /${cmdName} (failed to write shortcut)`);
226
- failedShortcuts.push(cmdName);
227
- }
228
- }
265
+ const sc = writeShortcuts(agentDir, claudeCommandsDir, { verbose: true });
266
+ const commandFiles = sc.commandFiles;
267
+ const failedShortcuts = sc.failed;
268
+ reportShortcuts(sc, '.claude/commands/');
229
269
 
230
270
  // 3. Install module (optional --module flag)
231
271
  if (moduleName) {
@@ -383,17 +423,11 @@ if (isInit) {
383
423
  }
384
424
  }
385
425
 
386
- // Create .claude/commands/ shortcuts
387
- fs.mkdirSync(svcClaudeCmds, { recursive: true });
388
- const svcCmdFiles = fs.readdirSync(path.join(svcAgentDir, 'commands')).filter(f => f.endsWith('.md'));
389
- for (const cmdFile of svcCmdFiles) {
390
- const cmdName = cmdFile.replace('.md', '');
391
- const shortcut =
392
- `# /${cmdName}\n\nRead the full command definition from \`.agent/commands/${cmdFile}\`` +
393
- ` and execute it with arguments: $ARGUMENTS\n`;
394
- fs.writeFileSync(path.join(svcClaudeCmds, cmdFile), shortcut, 'utf8');
395
- }
396
- console.log(` ✅ ${svc.name}/.claude/commands/ (${svcCmdFiles.length} shortcuts)`);
426
+ // Create .claude/commands/ shortcuts (cùng hàm với đường single-service — G44:
427
+ // trước đó hai chỗ này copy-paste nhau và chỉ một chỗ được sửa khi có thay đổi)
428
+ const svcSc = writeShortcuts(svcAgentDir, svcClaudeCmds, { verbose: false });
429
+ console.log(` ✅ ${svc.name}/.claude/commands/ (${svcSc.commandFiles.length} shortcuts)`);
430
+ reportShortcuts(svcSc, `${svc.name}/.claude/commands/`);
397
431
 
398
432
  // Generate project-context.yaml (only if not already present)
399
433
  const ctxPath = path.join(svcAgentDir, 'project-context.yaml');
@@ -510,99 +544,59 @@ if (isInit) {
510
544
  console.log(' 2. Commit .agent/ to git so your whole team has the framework');
511
545
  }
512
546
  console.log('');
513
- console.log('To upgrade later: bash scripts/upgrade.sh');
547
+ // G47 mục 1: dòng này từng in `bash scripts/upgrade.sh` sau MỌI lần cài — nhưng
548
+ // `scripts/` không bao giờ được cài vào project (coreMappings không có nó), nên
549
+ // consumer nhận "No such file or directory" ngay câu chỉ dẫn ĐẦU TIÊN họ đọc.
550
+ console.log('To upgrade later: /update-framework (trong Claude Code)');
514
551
  console.log(' (or: npx @educa-corp/sdd-framework@latest --init)');
515
552
  console.log('');
516
553
  process.exit(0);
517
554
  }
518
555
 
519
- // ── Banner (legacy --project / global modes) ──────────────────────────────
520
- const targetDir = isProject
521
- ? path.join(process.cwd(), '.claude', 'commands')
522
- : path.join(os.homedir(), '.claude', 'commands');
523
-
524
- const scope = isProject ? 'project' : 'global';
525
-
556
+ // ── Không phải --init không cài gì, chỉ hướng dẫn (GAPS-v3 G50) ─────────
557
+ //
558
+ // Trước đây chỗ này là hai chế độ 'legacy': `--project` và nhánh KHÔNG-CỜ (global).
559
+ // Cả hai chỉ copy commands/*.md vào .claude/commands/ và KHÔNG cài .agent/ — trong khi
560
+ // chính các file lệnh đó tham chiếu .agent/ ở 612 chỗ, và bước ĐẦU TIÊN của mọi lệnh là
561
+ // 'Đọc .agent/project-context.yaml'. Kết quả không phải hỏng hẳn mà THOÁI HOÁ ÂM THẦM:
562
+ // • 6 lệnh QC chết (nạp skill lúc chạy từ .agent/skills/qc/)
563
+ // • mọi lệnh còn lại VẪN CHẠY nhưng không nạp được .agent/rules/ — tức mất data-guard
564
+ // và mất luật CHECKPOINT/scope. Không ai được báo.
565
+ // • /setup-ai-first Step 6b bảo copy từ .agent/templates/ — không có ở đó
566
+ // Và installer thì in '✅ Installed 32/32 commands'. Đúng lớp lỗi cả loạt GAP này chống:
567
+ // tín hiệu xanh trên thứ đã hết đúng.
568
+ //
569
+ // --init phủ MỌI ca thật (single-service · monorepo · umbrella). Legacy không giải quyết
570
+ // vấn đề nào mà --init không giải quyết — nó chỉ là di sản.
526
571
  console.log('');
527
572
  console.log('╔══════════════════════════════════════════╗');
528
573
  console.log('║ SDD Framework — CLI Installer ║');
529
574
  console.log(`║ v${VERSION} ║`);
530
575
  console.log('╚══════════════════════════════════════════╝');
531
576
  console.log('');
532
- console.log(`Scope : ${scope}`);
533
- console.log(`Target : ${targetDir}`);
534
- if (moduleName) console.log(`Module : ${moduleName}`);
535
- if (installHooks) console.log('Hooks : enabled');
536
- console.log('');
537
-
538
- // ── Step 2: Install commands ──────────────────────────────────────────────
539
- fs.mkdirSync(targetDir, { recursive: true });
540
-
541
- const commandsDir = path.join(ROOT, 'commands');
542
- const commands = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
543
- const failed = [];
544
-
545
- for (const cmd of commands) {
546
- try {
547
- fs.copyFileSync(path.join(commandsDir, cmd), path.join(targetDir, cmd));
548
- console.log(` ✅ ${cmd.replace('.md', '')}`);
549
- } catch {
550
- console.log(` ❌ ${cmd.replace('.md', '')} (failed)`);
551
- failed.push(cmd);
552
- }
577
+ if (isProject) {
578
+ console.log('ℹ️ `--project` đã được gỡ (v0.5.1).');
579
+ } else {
580
+ console.log('ℹ️ Chạy không kèm cờ nào thì không có gì để cài.');
553
581
  }
554
-
555
582
  console.log('');
556
-
557
- if (failed.length > 0) {
558
- console.log(`⚠️ Installed ${commands.length - failed.length}/${commands.length} commands. Failed: ${failed.join(', ')}`);
559
- process.exit(1);
560
- }
561
-
562
- console.log(`✅ Installed ${commands.length}/${commands.length} commands to:`);
563
- console.log(` ${targetDir}`);
564
-
565
- // ── Step 3: Install module (optional) ─────────────────────────────────────
566
- if (moduleName) {
567
- console.log('');
568
- if (!AVAILABLE_MODULES.includes(moduleName)) {
569
- console.log(`⚠️ Unknown module: "${moduleName}"`);
570
- console.log(` Available: ${AVAILABLE_MODULES.join(', ')}`);
571
- process.exit(1);
572
- }
573
-
574
- const srcModuleDir = path.join(ROOT, 'modules', moduleName);
575
- const destModuleDir = path.join(process.cwd(), '.agent', 'modules', moduleName);
576
-
577
- fs.mkdirSync(destModuleDir, { recursive: true });
578
- copyDirRecursive(srcModuleDir, destModuleDir);
579
-
580
- console.log(`✅ Module "${moduleName}" installed to:`);
581
- console.log(` ${destModuleDir}`);
582
- }
583
-
584
- // ── Step 4: Install data-guard hook (optional, --hooks flag) ──────────────
585
- if (installHooks) {
586
- console.log('');
587
- installDataGuardHook();
588
- }
589
-
590
- // ── Summary ───────────────────────────────────────────────────────────────
583
+ console.log(' Chế độ cũ chỉ copy file lệnh vào .claude/commands/ mà KHÔNG cài .agent/.');
584
+ console.log(' Nhưng các lệnh tham chiếu .agent/ 612 chỗ — bước đầu tiên của mọi lệnh là');
585
+ console.log(' đọc .agent/project-context.yaml. Nên bản cài đó chạy MÀ KHÔNG có data-guard,');
586
+ console.log(' không có luật CHECKPOINT/scope, và cả pipeline QC không hoạt động — im lặng.');
591
587
  console.log('');
592
- console.log('Next steps:');
593
- console.log(' 1. Open your project in Claude Code');
594
- console.log(' 2. Type /setup-ai-first to initialize the workflow');
595
- console.log(' 3. Type / to browse all available commands');
596
- if (!moduleName) {
597
- console.log('');
598
- console.log(`Tip: re-run with --module <name> to install a stack profile.`);
599
- console.log(` Available: ${AVAILABLE_MODULES.join(', ')}`);
600
- }
601
- if (!installHooks) {
602
- console.log('');
603
- console.log('Tip: re-run with --hooks to install data-guard hook (sensitive file protection).');
604
- }
588
+ console.log(' Dùng:');
589
+ console.log(' npx @educa-corp/sdd-framework --init # single-service');
590
+ console.log(' npx @educa-corp/sdd-framework --init --module java-spring --hooks');
591
+ console.log(' npx @educa-corp/sdd-framework --init --services be:java-spring,web:react');
605
592
  console.log('');
593
+ console.log(' Đã lỡ cài kiểu cũ? Cứ chạy --init: nó phát hiện file lệnh cũ trong');
594
+ console.log(' .claude/commands/, backup rồi báo ra — không đè im lặng.');
595
+ console.log('');
596
+ console.log(' Xem đủ tuỳ chọn: npx @educa-corp/sdd-framework --help');
597
+ console.log('');
598
+ process.exit(0);
599
+
606
600
 
607
601
  // ── Helpers ───────────────────────────────────────────────────────────────
608
602
 
@@ -642,10 +636,15 @@ function readManifest(agentDir) {
642
636
  }
643
637
 
644
638
  /**
645
- * Copy core/ → .agent/, backing up any file the project edited since the last install.
646
- * @returns {{ edited: string[], backupDir: string|null, firstRun: boolean }}
647
- * edited — repo-relative paths (posix) that were overwritten AND had local edits
648
- * firstRun no manifest existed, so edits could not be detected this time
639
+ * Copy core/ → .agent/, backing up any file the project edited since the last install,
640
+ * AND removing files the framework no longer ships (GAPS-v3 G44).
641
+ *
642
+ * @returns {{ edited: string[], removed: string[], keptEdited: string[],
643
+ * backupDir: string|null, firstRun: boolean }}
644
+ * edited — file bị ghi đè VÀ có sửa đổi cục bộ (đã backup)
645
+ * removed — file framework đã bỏ, còn nguyên bản ⇒ đã xoá
646
+ * keptEdited — file framework đã bỏ NHƯNG người dùng đã sửa ⇒ GIỮ LẠI + backup
647
+ * firstRun — chưa có manifest nên lần này không phát hiện được sửa đổi
649
648
  */
650
649
  function installCore(coreDir, agentDir, version) {
651
650
  const manifest = readManifest(agentDir);
@@ -655,6 +654,12 @@ function installCore(coreDir, agentDir, version) {
655
654
  const edited = [];
656
655
  const written = {};
657
656
 
657
+ const backup = (abs, rel) => {
658
+ const bak = path.join(backupDir, rel);
659
+ fs.mkdirSync(path.dirname(bak), { recursive: true });
660
+ fs.copyFileSync(abs, bak);
661
+ };
662
+
658
663
  const walk = (src, dst) => {
659
664
  fs.mkdirSync(dst, { recursive: true });
660
665
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
@@ -666,9 +671,7 @@ function installCore(coreDir, agentDir, version) {
666
671
  // Locally edited == exists, we have a record of what we last wrote, and the
667
672
  // file no longer matches that record. Framework-only changes never match here.
668
673
  if (manifest && fs.existsSync(d) && manifest.files[rel] && sha1(d) !== manifest.files[rel]) {
669
- const bak = path.join(backupDir, rel);
670
- fs.mkdirSync(path.dirname(bak), { recursive: true });
671
- fs.copyFileSync(d, bak);
674
+ backup(d, rel);
672
675
  edited.push(rel);
673
676
  }
674
677
  fs.copyFileSync(s, d);
@@ -677,33 +680,253 @@ function installCore(coreDir, agentDir, version) {
677
680
  };
678
681
  walk(coreDir, agentDir);
679
682
 
683
+ // ── Prune: gỡ file framework KHÔNG CÒN ship (G44) ──────────────────────────
684
+ //
685
+ // Trước G44, installCore chỉ copy — không có nhánh xoá nào. Nên một lệnh bị bỏ ở
686
+ // version mới nằm lại trong .agent/commands/ và .claude/commands/ của mọi project
687
+ // VĨNH VIỄN: vẫn hiện trong menu `/`, vẫn chạy được, vẫn mang logic của version cũ
688
+ // — kể cả khi framework đã bỏ nó VÌ NÓ SAI. Và `/update-framework` Step 5 thì hứa
689
+ // báo cáo "Removed", một trạng thái installer không thể tạo ra.
690
+ //
691
+ // Đây là hành vi XOÁ FILE TRONG PROJECT NGƯỜI DÙNG, nên chỉ xoá khi CẢ BA đúng:
692
+ // 1. có trong manifest cũ ⇒ do framework đặt vào, không phải người dùng tạo
693
+ // 2. không có trong core/ mới ⇒ framework đã bỏ
694
+ // 3. hash khớp manifest ⇒ nguyên bản, xoá không mất gì của ai
695
+ // Đúng (1)+(2) mà sai (3) ⇒ người dùng đã sửa ⇒ GIỮ LẠI + backup + báo. Thà để lại
696
+ // một file lạc còn hơn xoá thứ ai đó đã bỏ công viết.
697
+ const removed = [];
698
+ const keptEdited = [];
699
+ if (manifest) {
700
+ for (const rel of Object.keys(manifest.files)) {
701
+ if (written[rel]) continue; // vẫn còn ship
702
+ const abs = path.join(agentDir, rel);
703
+ if (!fs.existsSync(abs)) continue; // đã biến mất từ trước
704
+ if (sha1(abs) === manifest.files[rel]) {
705
+ fs.unlinkSync(abs);
706
+ removed.push(rel);
707
+ } else {
708
+ backup(abs, rel);
709
+ keptEdited.push(rel);
710
+ }
711
+ }
712
+ // Dọn thư mục rỗng còn lại sau khi prune (vd cả một module bị bỏ). Chỉ xoá dir
713
+ // RỖNG — không recursive, nên không thể chạm vào file nào của người dùng.
714
+ for (const rel of removed) {
715
+ let dir = path.dirname(path.join(agentDir, rel));
716
+ while (dir !== agentDir && dir.startsWith(agentDir)) {
717
+ try {
718
+ if (fs.readdirSync(dir).length) break;
719
+ fs.rmdirSync(dir);
720
+ } catch { break; }
721
+ dir = path.dirname(dir);
722
+ }
723
+ }
724
+ }
725
+
726
+ // GIỮ phần `shortcuts` của manifest cũ. installCore chạy TRƯỚC writeShortcuts, nên
727
+ // ghi một object mới chỉ có `files` sẽ xoá sạch lịch sử shortcut — và writeShortcuts
728
+ // sau đó thấy `prev = {}`, mất cả prune LẪN phát hiện trùng tên. Bug này đã xảy ra
729
+ // thật khi làm G44: .agent/commands/debug.md được gỡ nhưng shortcut của nó thì không.
680
730
  fs.writeFileSync(
681
731
  path.join(agentDir, MANIFEST_NAME),
682
- JSON.stringify({ version, generated_at: new Date().toISOString(), files: written }, null, 2),
732
+ JSON.stringify({
733
+ version,
734
+ generated_at: new Date().toISOString(),
735
+ files: written,
736
+ shortcuts: (manifest && manifest.shortcuts) || {},
737
+ }, null, 2),
683
738
  'utf8'
684
739
  );
685
740
 
686
- return { edited, backupDir: edited.length ? backupDir : null, firstRun };
741
+ return {
742
+ edited, removed, keptEdited,
743
+ backupDir: (edited.length || keptEdited.length) ? backupDir : null,
744
+ firstRun,
745
+ };
687
746
  }
688
747
 
689
748
  function reportOverwritten(result, agentLabel) {
690
749
  if (result.firstRun) {
691
750
  console.log('');
692
751
  console.log(` ℹ️ ${agentLabel}${MANIFEST_NAME} created. Từ lần nâng cấp SAU, mọi file bạn`);
693
- console.log(` sửa trong ${agentLabel} mà bị ghi đè sẽ được backup + liệt kê.`);
752
+ console.log(` sửa trong ${agentLabel} mà bị ghi đè sẽ được backup + liệt kê,`);
753
+ console.log(` và file framework đã bỏ sẽ được gỡ khỏi ${agentLabel}.`);
694
754
  return;
695
755
  }
696
- if (!result.edited.length) return;
697
756
 
698
- const rel = path.relative(process.cwd(), result.backupDir).replace(/\\/g, '/');
699
- console.log('');
700
- console.log(` ⚠️ ${result.edited.length} file trong ${agentLabel} đã bị bạn sửa và VỪA BỊ GHI ĐÈ:`);
701
- for (const f of result.edited.slice(0, 12)) console.log(` ${agentLabel}${f}`);
702
- if (result.edited.length > 12) console.log(` … và ${result.edited.length - 12} file nữa`);
703
- console.log(` Bản đã lưu ở: ${rel}/`);
704
- console.log(` ${agentLabel} là bản mirror SINH RA — sửa trực tiếp ở đó không bền.`);
705
- console.log(` Muốn giữ thay đổi: chuyển nó vào repo framework rồi phát hành,`);
706
- console.log(` hoặc áp lại thủ công sau khi xem diff. Xoá thư mục backup khi xong.`);
757
+ const bak = result.backupDir
758
+ ? path.relative(process.cwd(), result.backupDir).replace(/\\/g, '/')
759
+ : null;
760
+ const list = (arr, n = 12) => {
761
+ for (const f of arr.slice(0, n)) console.log(` ${agentLabel}${f}`);
762
+ if (arr.length > n) console.log(` ${arr.length - n} file nữa`);
763
+ };
764
+
765
+ if (result.edited.length) {
766
+ console.log('');
767
+ console.log(` ⚠️ ${result.edited.length} file trong ${agentLabel} đã bị bạn sửa và VỪA BỊ GHI ĐÈ:`);
768
+ list(result.edited);
769
+ console.log(` Bản cũ đã lưu ở: ${bak}/`);
770
+ console.log(` ${agentLabel} là bản mirror SINH RA — sửa trực tiếp ở đó không bền.`);
771
+ console.log(` Muốn giữ thay đổi: chuyển nó vào repo framework rồi phát hành,`);
772
+ console.log(` hoặc áp lại thủ công sau khi xem diff. Xoá thư mục backup khi xong.`);
773
+ }
774
+
775
+ // G44 — nói ra thứ đã bị GỠ. Trong một diff nâng cấp lớn, file biến mất là thứ dễ
776
+ // trôi nhất; và nếu không báo thì người dùng chỉ phát hiện khi gõ một slash command
777
+ // giờ đã không còn.
778
+ if (result.removed.length) {
779
+ console.log('');
780
+ console.log(` 🗑️ ${result.removed.length} file framework đã bị BỎ ở version này — đã gỡ khỏi ${agentLabel}:`);
781
+ list(result.removed, 8);
782
+ console.log(` Chúng còn nguyên bản (không ai sửa) nên gỡ đi không mất gì.`);
783
+ console.log(` Lệnh nào biến mất khỏi menu / là vì vậy.`);
784
+ }
785
+
786
+ if (result.keptEdited.length) {
787
+ console.log('');
788
+ console.log(` ⚠️ ${result.keptEdited.length} file framework đã bị bỏ NHƯNG bạn đã sửa — GIỮ LẠI:`);
789
+ list(result.keptEdited, 8);
790
+ console.log(` Đã backup ở: ${bak}/`);
791
+ console.log(` Framework không dùng chúng nữa. Xoá tay khi bạn chắc không cần,`);
792
+ console.log(` hoặc chuyển thay đổi đó vào repo framework nếu nó còn giá trị.`);
793
+ }
794
+ }
795
+
796
+ // ── Shortcut .claude/commands/ — có manifest, có prune, có cảnh báo trùng tên ──
797
+ //
798
+ // Vì sao cần (GAPS-v3 G44): bản cũ ghi 32 file vào .claude/commands/ VÔ ĐIỀU KIỆN,
799
+ // không backup, không cảnh báo, và không bao giờ gỡ shortcut của lệnh đã bị bỏ.
800
+ // Hai hệ quả:
801
+ // • Tên lệnh rất chung — /debug, /sync, /learn, /code-review — và trùng cả với
802
+ // skill/plugin sẵn có của Claude Code. Project đã có /sync riêng thì MẤT nó,
803
+ // im lặng, ngay lần cài đầu. Bảo vệ G24 chỉ áp cho .agent/, không tới đây.
804
+ // • Lệnh bị bỏ ở version mới vẫn hiện trong menu `/` mãi mãi, và khi ai đó gõ nó
805
+ // thì file .agent/commands/ đích đã không còn.
806
+ //
807
+ // Shortcut được ghi vào manifest ở khoá `shortcuts` (path tương đối gốc project),
808
+ // tách khỏi `files` (tương đối .agent/). Manifest cũ chỉ có `files` → vẫn đọc được.
809
+ function shortcutBody(cmdFile) {
810
+ const cmdName = cmdFile.replace(/\.md$/, '');
811
+ return `# /${cmdName}\n\nRead the full command definition from \`.agent/commands/${cmdFile}\``
812
+ + ` and execute it with arguments: $ARGUMENTS\n`;
813
+ }
814
+
815
+ function writeShortcuts(agentDir, claudeCommandsDir, { verbose }) {
816
+ const manifest = readManifest(agentDir) || {};
817
+ const prev = manifest.shortcuts || {};
818
+ const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
819
+
820
+ fs.mkdirSync(claudeCommandsDir, { recursive: true });
821
+ const commandFiles = fs.readdirSync(path.join(agentDir, 'commands')).filter(f => f.endsWith('.md'));
822
+
823
+ const written = {};
824
+ const failed = [];
825
+ const collided = [];
826
+ const removed = [];
827
+ const keptEdited = [];
828
+ const legacyFound = []; // file lệnh ĐẦY ĐỦ từ bản cài legacy — KHÁC lệnh của project
829
+ const sha1s = (str) => require('crypto').createHash('sha1').update(str).digest('hex');
830
+
831
+ for (const cmdFile of commandFiles) {
832
+ const abs = path.join(claudeCommandsDir, cmdFile);
833
+ const body = shortcutBody(cmdFile);
834
+ const key = cmdFile;
835
+
836
+ // Đã tồn tại, KHÔNG do lần cài trước của framework tạo, và nội dung khác ⇒ backup
837
+ // rồi báo — đừng lặng lẽ đè. Nhưng PHÂN BIỆT HAI CA, vì thông điệp phải khác nhau:
838
+ //
839
+ // (a) BẢN CÀI FRAMEWORK CŨ (legacy). Legacy mode copy file lệnh ĐẦY ĐỦ (60–110 KB)
840
+ // vào .claude/commands/ với đúng 32 tên đó. G50 gỡ legacy nên mọi project đã cài
841
+ // kiểu cũ SẼ đi qua đường này. Gọi nó là "slash command của bạn" là SAI: người
842
+ // dùng sẽ tưởng vừa mất 32 lệnh tự viết, đi mở backup tìm, rồi phát hiện đó chỉ
843
+ // là bản framework cũ.
844
+ // (b) slash command THẬT của project — tên lệnh framework rất chung (/debug /sync
845
+ // /learn) nên trùng là chuyện có thật.
846
+ //
847
+ // Nhận diện: shortcut ~150 B · bản legacy 60–110 KB · lệnh project thường < 5 KB.
848
+ // Chênh ~500 lần, không cần đoán. Ngưỡng 20 KB nằm giữa 5 và 60 nên rất an toàn;
849
+ // `## Gate` là dấu hiệu thứ hai (mọi file lệnh built đều có, shortcut thì không).
850
+ if (fs.existsSync(abs) && !prev[key]) {
851
+ const cur = fs.readFileSync(abs, 'utf8');
852
+ if (cur !== body) {
853
+ const bak = path.join(agentDir, `.overwritten-${stamp}-shortcuts`, cmdFile);
854
+ try {
855
+ fs.mkdirSync(path.dirname(bak), { recursive: true });
856
+ fs.copyFileSync(abs, bak);
857
+ (cur.length > 20 * 1024 || cur.includes('## Gate') ? legacyFound : collided).push(cmdFile);
858
+ } catch { /* backup fail thì vẫn báo ở dưới */ }
859
+ }
860
+ }
861
+
862
+ try {
863
+ fs.writeFileSync(abs, body, 'utf8');
864
+ written[key] = sha1s(body);
865
+ if (verbose) console.log(` ✅ /${cmdFile.replace(/\.md$/, '')}`);
866
+ } catch {
867
+ if (verbose) console.log(` ❌ /${cmdFile.replace(/\.md$/, '')} (failed to write shortcut)`);
868
+ failed.push(cmdFile.replace(/\.md$/, ''));
869
+ }
870
+ }
871
+
872
+ // Prune — cùng ba điều kiện như installCore: có trong manifest cũ, không còn ship,
873
+ // và còn nguyên bản. Người dùng sửa shortcut thì giữ lại.
874
+ for (const key of Object.keys(prev)) {
875
+ if (written[key]) continue;
876
+ const abs = path.join(claudeCommandsDir, key);
877
+ if (!fs.existsSync(abs)) continue;
878
+ if (sha1s(fs.readFileSync(abs, 'utf8')) === prev[key]) {
879
+ try { fs.unlinkSync(abs); removed.push(key); } catch { /* bỏ qua */ }
880
+ } else {
881
+ keptEdited.push(key);
882
+ }
883
+ }
884
+
885
+ // Ghi lại manifest (installCore đã ghi `files` trước đó — nối thêm `shortcuts`)
886
+ try {
887
+ const mPath = path.join(agentDir, MANIFEST_NAME);
888
+ const m = readManifest(agentDir) || { files: {} };
889
+ m.shortcuts = written;
890
+ fs.writeFileSync(mPath, JSON.stringify(m, null, 2), 'utf8');
891
+ } catch { /* manifest không ghi được thì lần sau coi như firstRun — không chặn cài */ }
892
+
893
+ return { commandFiles, failed, collided, legacyFound, removed, keptEdited };
894
+ }
895
+
896
+ function reportShortcuts(sc, label) {
897
+ // Bản cài legacy cũ — KHÔNG phải slash command của người dùng. Nói đúng tên nó, vì
898
+ // G50 gỡ legacy nên mọi project đã cài kiểu cũ sẽ đi qua đây, và gọi sai tên làm họ
899
+ // tưởng vừa mất 32 lệnh tự viết rồi đi mở backup tìm một thứ không tồn tại.
900
+ if ((sc.legacyFound || []).length) {
901
+ console.log('');
902
+ console.log(` 🔄 ${sc.legacyFound.length} file trong ${label} là BẢN CÀI FRAMEWORK CŨ (legacy)`);
903
+ console.log(` — đã thay bằng shortcut. Đây KHÔNG phải slash command bạn tự viết.`);
904
+ console.log(` Bản cũ vẫn được backup ở .agent/.overwritten-*-shortcuts/, nhưng bạn gần như`);
905
+ console.log(` chắc chắn không cần nó — xoá thư mục đó khi thấy \`/\` vẫn hoạt động bình thường.`);
906
+ console.log(` (Chế độ cài legacy đã được gỡ ở v0.5.1: nó cài lệnh mà không cài .agent/,`);
907
+ console.log(` nên lệnh chạy thiếu guardrail và pipeline QC không hoạt động.)`);
908
+ }
909
+ if (sc.collided.length) {
910
+ console.log('');
911
+ console.log(` ⚠️ ${sc.collided.length} slash command CÓ SẴN của project vừa bị shortcut framework ghi đè:`);
912
+ for (const f of sc.collided.slice(0, 10)) console.log(` ${label}${f}`);
913
+ if (sc.collided.length > 10) console.log(` … và ${sc.collided.length - 10} file nữa`);
914
+ console.log(` Bản cũ đã lưu trong .agent/.overwritten-*-shortcuts/`);
915
+ console.log(` Tên lệnh của framework rất chung (/debug /sync /learn) nên dễ trùng.`);
916
+ console.log(` Muốn giữ lệnh của bạn: đổi tên nó rồi khôi phục từ backup.`);
917
+ }
918
+ if (sc.removed.length) {
919
+ console.log('');
920
+ console.log(` 🗑️ ${sc.removed.length} shortcut của lệnh đã bị bỏ — đã gỡ khỏi ${label}`);
921
+ for (const f of sc.removed.slice(0, 8)) console.log(` /${f.replace(/\.md$/, '')}`);
922
+ if (sc.removed.length > 8) console.log(` … và ${sc.removed.length - 8} nữa`);
923
+ }
924
+ if (sc.keptEdited.length) {
925
+ console.log('');
926
+ console.log(` ⚠️ ${sc.keptEdited.length} shortcut của lệnh đã bị bỏ NHƯNG bạn đã sửa — giữ lại:`);
927
+ for (const f of sc.keptEdited.slice(0, 8)) console.log(` ${label}${f}`);
928
+ console.log(` Lệnh đích trong .agent/commands/ đã không còn — shortcut này sẽ lỗi khi gọi.`);
929
+ }
707
930
  }
708
931
 
709
932
  function installDataGuardHook() {
@@ -719,34 +942,83 @@ function installDataGuardHook() {
719
942
  console.log(` ✅ Hook copied to: .claude/hooks/data-guard.js`);
720
943
 
721
944
  // 2. Update .claude/settings.json
945
+ //
946
+ // settings.json HỎNG thì DỪNG, đừng ghi đè. Bản cũ `catch {}` rồi đi tiếp với
947
+ // `settings = {}`, và bước writeFileSync bên dưới ghi đè TOÀN BỘ file bằng đúng
948
+ // một entry hook — tức một dấu phẩy thừa trong settings.json làm mất sạch cấu hình
949
+ // Claude Code của project, im lặng.
722
950
  let settings = {};
723
951
  if (fs.existsSync(settingsPath)) {
724
- try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch {}
952
+ const raw = fs.readFileSync(settingsPath, 'utf8');
953
+ try {
954
+ settings = JSON.parse(raw);
955
+ } catch (err) {
956
+ console.log('');
957
+ console.log(` ❌ .claude/settings.json không phải JSON hợp lệ: ${err.message}`);
958
+ console.log(' KHÔNG đăng ký hook — ghi vào đây bây giờ sẽ xoá sạch cấu hình hiện có.');
959
+ console.log(' Sửa file rồi chạy lại với --hooks.');
960
+ console.log('');
961
+ console.log(' ℹ️ Script hook đã được copy; chỉ còn thiếu bước đăng ký. Mẫu:');
962
+ console.log(' .agent/hooks/settings.json');
963
+ return;
964
+ }
965
+ if (settings === null || typeof settings !== 'object' || Array.isArray(settings)) {
966
+ console.log('');
967
+ console.log(' ❌ .claude/settings.json không phải một JSON object — KHÔNG đăng ký hook.');
968
+ console.log('');
969
+ return;
970
+ }
725
971
  }
726
972
 
727
973
  // Merge: add PreToolUse hook if not already present
728
974
  if (!settings.hooks) settings.hooks = {};
729
975
  if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
730
976
 
977
+ // `Grep` BẮT BUỘC có mặt: Grep với output_mode:"content" ĐỌC ĐƯỢC nội dung file,
978
+ // nên thiếu nó thì việc chặn Read chỉ là cửa trước, cửa sau mở toang (GAPS-v3 G42a).
731
979
  const hookEntry = {
732
- matcher: 'Read|Write|Edit|Bash',
980
+ matcher: 'Read|Write|Edit|Grep|NotebookEdit|Bash',
733
981
  hooks: [{ type: 'command', command: 'node .claude/hooks/data-guard.js' }],
734
982
  };
735
983
 
736
- const alreadyRegistered = settings.hooks.PreToolUse.some(
984
+ // Tìm entry đã có (nếu có) — KHÔNG chỉ để bỏ qua, mà để NÂNG CẤP matcher.
985
+ //
986
+ // Bản cũ chỉ hỏi "có đăng ký chưa?" rồi bỏ qua nếu có. Nghĩa là mọi project đã cài
987
+ // hook TRƯỚC bản sửa G42 sẽ giữ matcher cũ `Read|Write|Edit|Bash` — thiếu `Grep` —
988
+ // VĨNH VIỄN, và không lần `--hooks` nào sau đó chữa được. Lỗ bảo mật sống sót qua
989
+ // đúng cái lệnh đáng ra phải bịt nó.
990
+ const existing = settings.hooks.PreToolUse.find(
737
991
  h => h.hooks && h.hooks.some(hh => hh.command && hh.command.includes('data-guard.js'))
738
992
  );
739
993
 
740
- if (!alreadyRegistered) {
994
+ const REQUIRED_TOOLS = hookEntry.matcher.split('|');
995
+
996
+ if (!existing) {
741
997
  settings.hooks.PreToolUse.push(hookEntry);
742
998
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
743
- console.log(` ✅ Hook registered in: .claude/settings.json`);
999
+ console.log(' ✅ Hook registered in: .claude/settings.json');
744
1000
  } else {
745
- console.log(` ℹ️ Hook already registered in .claude/settings.json`);
1001
+ const have = String(existing.matcher || '').split('|').map(s => s.trim());
1002
+ const missing = REQUIRED_TOOLS.filter(t => !have.includes(t));
1003
+ if (missing.length) {
1004
+ existing.matcher = hookEntry.matcher;
1005
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
1006
+ console.log(` ✅ Hook matcher nâng cấp: thêm ${missing.join(', ')}`);
1007
+ if (missing.includes('Grep')) {
1008
+ console.log(' (thiếu Grep = lỗ đọc file: Grep với output_mode:"content" trả về nội dung)');
1009
+ }
1010
+ } else {
1011
+ console.log(' ℹ️ Hook already registered in .claude/settings.json');
1012
+ }
746
1013
  }
747
1014
 
748
1015
  console.log('');
749
- console.log(' 🔒 Data guard active AI will be blocked from reading:');
750
- console.log(' .env*, *.key, *.pem, *secret*, *password*, *credential*');
751
- console.log(' application-prod.*, appsettings.Production.json, ...');
1016
+ console.log(' 🔒 Data guard đang canh các tool: Read · Write · Edit · Grep · NotebookEdit · Bash');
1017
+ console.log(' Chặn: .env* · *.key/.pem/.p12/.jks · application-prod.* · .ssh/ .aws/ secrets/');
1018
+ console.log(' file cấu hình/dữ liệu có tên chứa secret|credential|password|token|api-key');
1019
+ console.log('');
1020
+ console.log(' KHÔNG chặn (có chủ ý — đây là công việc của framework):');
1021
+ console.log(' • specs/ .trace/ docs/ .agent/ feedback/ — vùng làm việc, không chứa secret thật');
1022
+ console.log(' • file mã nguồn (.java/.ts/.dart/.kt…) — PasswordResetController.java là code');
1023
+ console.log(' Guard không canh được đường đưa dữ liệu ra ngoài (WebFetch…) — nó là file guard.');
752
1024
  }