@educa-corp/sdd-framework 0.5.0 → 0.7.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 (243) hide show
  1. package/bin/build.js +113 -19
  2. package/bin/gate-trace.js +487 -0
  3. package/bin/index.js +445 -146
  4. package/bin/lint-trace.js +643 -0
  5. package/bin/self-check.js +804 -2
  6. package/bin/trace-schema.json +621 -10
  7. package/core/FRAMEWORK_VERSION +1 -1
  8. package/core/README.md +20 -0
  9. package/core/commands/amend-prd.md +518 -0
  10. package/core/commands/debug.md +123 -511
  11. package/core/commands/define-product.md +86 -510
  12. package/core/commands/dev-gen-test.md +86 -510
  13. package/core/commands/dev-run-test.md +133 -519
  14. package/core/commands/dev-smoke-test.md +86 -510
  15. package/core/commands/extend-prd.md +128 -522
  16. package/core/commands/fix-bug.md +118 -509
  17. package/core/commands/generate-architecture.md +94 -515
  18. package/core/commands/generate-bdd.md +128 -513
  19. package/core/commands/generate-code.md +119 -510
  20. package/core/commands/generate-design-spec.md +86 -510
  21. package/core/commands/generate-prd.md +89 -510
  22. package/core/commands/generate-spec-manifest.md +86 -510
  23. package/core/commands/generate-tech-docs.md +120 -512
  24. package/core/commands/learn.md +172 -496
  25. package/core/commands/map-testids.md +86 -510
  26. package/core/commands/propose-scenario.md +86 -510
  27. package/core/commands/qc-analyze.md +86 -510
  28. package/core/commands/qc-design-test.md +86 -510
  29. package/core/commands/qc-plan.md +86 -510
  30. package/core/commands/qc-report.md +86 -510
  31. package/core/commands/qc-review.md +86 -510
  32. package/core/commands/qc-run-test.md +115 -513
  33. package/core/commands/refine-prd.md +112 -522
  34. package/core/commands/report-bug.md +86 -510
  35. package/core/commands/review-code.md +123 -511
  36. package/core/commands/review-context.md +136 -522
  37. package/core/commands/review-tech-docs.md +90 -511
  38. package/core/commands/setup-ai-first.md +166 -138
  39. package/core/commands/sync.md +155 -107
  40. package/core/commands/update-framework.md +16 -103
  41. package/core/commands/validate-traces.md +426 -511
  42. package/core/hooks/data-guard.js +174 -83
  43. package/core/hooks/settings.json +2 -1
  44. package/core/rules/workflow.md +64 -4
  45. package/core/steps/capture-lesson.md +34 -1
  46. package/core/steps/context-loader.md +50 -8
  47. package/core/steps/gate.md +92 -35
  48. package/core/steps/report-footer.md +23 -0
  49. package/core/templates/README.md +24 -1
  50. package/core/templates/ci/trace-gate.yml +146 -0
  51. package/core/templates/feature.template +1 -1
  52. package/core/templates/hooks/pre-push +61 -0
  53. package/docs/02-concepts/architecture.md +61 -6
  54. package/docs/02-concepts/traceability.md +57 -0
  55. package/docs/03-guides/architect.md +63 -0
  56. package/docs/04-reference/commands.md +148 -134
  57. package/docs/04-reference/model-selection.md +32 -19
  58. package/docs/04-reference/trace-schema.md +39 -0
  59. package/docs/explain/02b-extend-prd.md +1 -1
  60. package/docs/explain/02c-amend-prd.md +152 -0
  61. package/docs/explain/21-validate-traces.md +2 -1
  62. package/docs/explain/27-learn.md +5 -3
  63. package/docs/explain/28-sync.md +25 -0
  64. package/docs/explain/README.md +136 -135
  65. package/package.json +5 -9
  66. package/commands/debug.md +0 -917
  67. package/commands/debug.tmpl +0 -257
  68. package/commands/define-product.md +0 -862
  69. package/commands/define-product.tmpl +0 -225
  70. package/commands/dev-gen-test.md +0 -1124
  71. package/commands/dev-gen-test.tmpl +0 -490
  72. package/commands/dev-run-test.md +0 -859
  73. package/commands/dev-run-test.tmpl +0 -225
  74. package/commands/dev-smoke-test.md +0 -798
  75. package/commands/dev-smoke-test.tmpl +0 -217
  76. package/commands/extend-prd.md +0 -907
  77. package/commands/extend-prd.tmpl +0 -270
  78. package/commands/fix-bug.md +0 -910
  79. package/commands/fix-bug.tmpl +0 -197
  80. package/commands/generate-architecture.md +0 -775
  81. package/commands/generate-architecture.tmpl +0 -194
  82. package/commands/generate-bdd.md +0 -1347
  83. package/commands/generate-bdd.tmpl +0 -590
  84. package/commands/generate-code.md +0 -1283
  85. package/commands/generate-code.tmpl +0 -649
  86. package/commands/generate-design-spec.md +0 -1161
  87. package/commands/generate-design-spec.tmpl +0 -524
  88. package/commands/generate-prd.md +0 -1143
  89. package/commands/generate-prd.tmpl +0 -223
  90. package/commands/generate-spec-manifest.md +0 -745
  91. package/commands/generate-spec-manifest.tmpl +0 -164
  92. package/commands/generate-tech-docs.md +0 -1344
  93. package/commands/generate-tech-docs.tmpl +0 -273
  94. package/commands/learn.md +0 -723
  95. package/commands/learn.tmpl +0 -63
  96. package/commands/map-testids.md +0 -662
  97. package/commands/map-testids.tmpl +0 -81
  98. package/commands/propose-scenario.md +0 -783
  99. package/commands/propose-scenario.tmpl +0 -202
  100. package/commands/qc-analyze.md +0 -693
  101. package/commands/qc-analyze.tmpl +0 -112
  102. package/commands/qc-design-test.md +0 -650
  103. package/commands/qc-design-test.tmpl +0 -69
  104. package/commands/qc-plan.md +0 -630
  105. package/commands/qc-plan.tmpl +0 -49
  106. package/commands/qc-report.md +0 -641
  107. package/commands/qc-report.tmpl +0 -60
  108. package/commands/qc-review.md +0 -634
  109. package/commands/qc-review.tmpl +0 -53
  110. package/commands/qc-run-test.md +0 -750
  111. package/commands/qc-run-test.tmpl +0 -116
  112. package/commands/refine-prd.md +0 -1074
  113. package/commands/refine-prd.tmpl +0 -278
  114. package/commands/report-bug.md +0 -729
  115. package/commands/report-bug.tmpl +0 -148
  116. package/commands/review-code.md +0 -803
  117. package/commands/review-code.tmpl +0 -143
  118. package/commands/review-context.md +0 -1323
  119. package/commands/review-context.tmpl +0 -527
  120. package/commands/review-tech-docs.md +0 -982
  121. package/commands/review-tech-docs.tmpl +0 -401
  122. package/commands/setup-ai-first.md +0 -574
  123. package/commands/setup-ai-first.tmpl +0 -378
  124. package/commands/sync.md +0 -486
  125. package/commands/sync.tmpl +0 -384
  126. package/commands/update-framework.md +0 -290
  127. package/commands/update-framework.tmpl +0 -188
  128. package/commands/validate-traces.md +0 -1435
  129. package/commands/validate-traces.tmpl +0 -854
  130. package/hooks/data-guard.js +0 -141
  131. package/hooks/settings.json +0 -18
  132. package/modules/android-compose/module.yaml +0 -13
  133. package/modules/android-compose/stack-profile.yaml +0 -57
  134. package/modules/angular/architecture-snippets/component-patterns.md +0 -187
  135. package/modules/angular/module.yaml +0 -6
  136. package/modules/angular/stack-profile.yaml +0 -38
  137. package/modules/context-engineering/architecture-snippets/context-design.md +0 -119
  138. package/modules/context-engineering/module.yaml +0 -9
  139. package/modules/context-engineering/stack-profile.yaml +0 -61
  140. package/modules/dotnet/architecture-snippets/clean-arch.md +0 -160
  141. package/modules/dotnet/module.yaml +0 -6
  142. package/modules/dotnet/stack-profile.yaml +0 -50
  143. package/modules/flutter/module.yaml +0 -14
  144. package/modules/flutter/stack-profile.yaml +0 -59
  145. package/modules/golang/architecture-snippets/domain-layout.md +0 -283
  146. package/modules/golang/module.yaml +0 -6
  147. package/modules/golang/stack-profile.yaml +0 -40
  148. package/modules/ios-swiftui/module.yaml +0 -13
  149. package/modules/ios-swiftui/stack-profile.yaml +0 -55
  150. package/modules/java-spring/architecture-snippets/layered-arch.md +0 -201
  151. package/modules/java-spring/module.yaml +0 -15
  152. package/modules/java-spring/stack-profile.yaml +0 -28
  153. package/modules/nextjs/architecture-snippets/app-router-patterns.md +0 -269
  154. package/modules/nextjs/module.yaml +0 -14
  155. package/modules/nextjs/stack-profile.yaml +0 -74
  156. package/modules/nuxt/module.yaml +0 -14
  157. package/modules/nuxt/stack-profile.yaml +0 -58
  158. package/modules/phaser-game/architecture-snippets/phaser-scene-patterns.md +0 -646
  159. package/modules/phaser-game/module.yaml +0 -15
  160. package/modules/phaser-game/stack-profile.yaml +0 -90
  161. package/modules/php-laravel/architecture-snippets/service-repository.md +0 -302
  162. package/modules/php-laravel/module.yaml +0 -15
  163. package/modules/php-laravel/stack-profile.yaml +0 -56
  164. package/modules/qc-playwright/stack-profile.yaml +0 -66
  165. package/modules/react/architecture-snippets/hooks-query-patterns.md +0 -254
  166. package/modules/react/module.yaml +0 -14
  167. package/modules/react/stack-profile.yaml +0 -63
  168. package/modules/react-native/module.yaml +0 -14
  169. package/modules/react-native/stack-profile.yaml +0 -56
  170. package/modules/vue/module.yaml +0 -14
  171. package/modules/vue/stack-profile.yaml +0 -65
  172. package/rules/data-protection.md +0 -80
  173. package/rules/workflow.md +0 -73
  174. package/scripts/init.sh +0 -49
  175. package/scripts/upgrade.sh +0 -94
  176. package/skills/code/SKILL.md +0 -19
  177. package/skills/code/SKILL.tmpl +0 -19
  178. package/skills/debug/SKILL.md +0 -19
  179. package/skills/debug/SKILL.tmpl +0 -19
  180. package/skills/design-spec/SKILL.md +0 -11
  181. package/skills/design-spec/SKILL.tmpl +0 -11
  182. package/skills/discovery/SKILL.md +0 -14
  183. package/skills/discovery/SKILL.tmpl +0 -14
  184. package/skills/prd/SKILL.md +0 -19
  185. package/skills/prd/SKILL.tmpl +0 -19
  186. package/skills/qc/qa-analyst/DOC_GAPS.template.md +0 -63
  187. package/skills/qc/qa-analyst/acceptance-criteria.md +0 -60
  188. package/skills/qc/qa-analyst/business-rules.md +0 -59
  189. package/skills/qc/qa-analyst/data-flow.md +0 -64
  190. package/skills/qc/qa-analyst/spec-breakdown.md +0 -61
  191. package/skills/qc/qa-designer/e2e/journey.md +0 -41
  192. package/skills/qc/qa-designer/exploratory/charter.md +0 -68
  193. package/skills/qc/qa-designer/exploratory/explore-to-functional.md +0 -43
  194. package/skills/qc/qa-designer/functional/api.md +0 -45
  195. package/skills/qc/qa-designer/functional/gui-feature.md +0 -46
  196. package/skills/qc/qa-designer/functional/gui-screen.md +0 -52
  197. package/skills/qc/qa-designer/integration/api.md +0 -42
  198. package/skills/qc/qa-designer/integration/db.md +0 -39
  199. package/skills/qc/qa-designer/integration/gui.md +0 -40
  200. package/skills/qc/qa-designer/integration/kafka.md +0 -40
  201. package/skills/qc/qa-designer/non-functional.md +0 -40
  202. package/skills/qc/qa-planner/test-plan.md +0 -120
  203. package/skills/qc/qa-reviewer/script/e2e.md +0 -87
  204. package/skills/qc/qa-reviewer/script/exploratory.md +0 -45
  205. package/skills/qc/qa-reviewer/script/functional.md +0 -101
  206. package/skills/qc/qa-reviewer/script/integration.md +0 -91
  207. package/skills/qc/qa-reviewer/script/non-functional.md +0 -126
  208. package/skills/qc/qa-reviewer/test-case/e2e.md +0 -73
  209. package/skills/qc/qa-reviewer/test-case/exploratory.md +0 -43
  210. package/skills/qc/qa-reviewer/test-case/functional.md +0 -76
  211. package/skills/qc/qa-reviewer/test-case/integration.md +0 -69
  212. package/skills/qc/qa-reviewer/test-case/non-functional.md +0 -73
  213. package/skills/qc/qa-runner/e2e.md +0 -49
  214. package/skills/qc/qa-runner/exploratory/session.md +0 -36
  215. package/skills/qc/qa-runner/functional/api.md +0 -35
  216. package/skills/qc/qa-runner/functional/gui-feature.md +0 -51
  217. package/skills/qc/qa-runner/functional/gui-screen.md +0 -55
  218. package/skills/qc/qa-runner/integration.md +0 -47
  219. package/skills/qc/qa-runner/non-functional.md +0 -49
  220. package/skills/qc/qa-runner/report/report.md +0 -37
  221. package/skills/setup-ai-first/SKILL.md +0 -19
  222. package/skills/setup-ai-first/SKILL.tmpl +0 -19
  223. package/skills/spec/SKILL.md +0 -19
  224. package/skills/spec/SKILL.tmpl +0 -19
  225. package/skills/test/SKILL.md +0 -18
  226. package/skills/test/SKILL.tmpl +0 -18
  227. package/steps/business-language.md +0 -56
  228. package/steps/capture-lesson.md +0 -79
  229. package/steps/context-loader.md +0 -385
  230. package/steps/gate.md +0 -94
  231. package/steps/report-footer.md +0 -102
  232. package/steps/review-fanout.md +0 -159
  233. package/steps/spawn-agent.md +0 -129
  234. package/steps/trace-mirror.md +0 -53
  235. package/templates/README.md +0 -47
  236. package/templates/architecture.template.md +0 -394
  237. package/templates/design-spec.template.md +0 -217
  238. package/templates/feature.template +0 -123
  239. package/templates/platform-guide.template.md +0 -145
  240. package/templates/prd.template.md +0 -283
  241. package/templates/product-definition.template.md +0 -188
  242. package/templates/project-context.yaml +0 -212
  243. package/templates/tech-design.template.md +0 -490
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,16 +169,80 @@ 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
+ // `hasSources` LÀ phép phân biệt dev-checkout vs bản cài từ npm — đặt tên MỘT LẦN.
205
+ //
206
+ // Vì sao gọi tên nó (GAPS-v4 G59): trước đó cùng một `existsSync(commands/*.tmpl)` được viết
207
+ // ở hai chỗ với hai ý khác nhau, và `package.json → files` thì SHIP `commands/` — nên phép thử
208
+ // LUÔN đúng và consumer nhìn thấy một lời nhắc mà chú thích của chính nó nói là
209
+ // "consumer không cần biết bước này tồn tại". Đã kiểm bằng cách pack rồi chạy tarball thật.
210
+ // Giờ `files` chỉ còn bin/ core/ scripts/ docs/, nên `hasSources` phân biệt đúng.
211
+ const hasSources = fs.existsSync(path.join(ROOT, 'commands', 'generate-code.tmpl'));
212
+
213
+ if (!corePrebuilt && hasSources && fs.existsSync(buildScript)) {
214
+ // DEV CHECKOUT, core/ vắng hoặc lệch version → dựng lại từ nguồn.
155
215
  try {
156
216
  require(buildScript);
157
217
  } catch (err) {
158
218
  console.error('Build step failed:', err.message);
159
219
  process.exit(1);
160
220
  }
221
+ } else if (!corePrebuilt) {
222
+ // BẢN CÀI TỪ NPM mà core/ vắng hoặc lệch version — KHÔNG có nguồn để dựng lại.
223
+ //
224
+ // Trước G59, nhánh này rơi vào `require(buildScript)` và build.js chết với một ENOENT trên
225
+ // `commands/` — thông báo không nói được vấn đề thật. Cố build ở đây còn tệ hơn: G43 đã nêu
226
+ // rõ là nó ghi vào THƯ MỤC PACKAGE (npx cache / node_modules global), có thể read-only, và
227
+ // hai `--init` song song sẽ đua nhau. Nên: dừng với một câu nói đúng việc cần làm.
228
+ console.error('');
229
+ console.error(`❌ Bản cài này thiếu core/ dựng sẵn cho v${VERSION}.`);
230
+ console.error(' Package đã tải về bị thiếu/hỏng — KHÔNG thể dựng lại vì tarball không mang');
231
+ console.error(' commands/*.tmpl (có chủ ý: nguồn chỉ có trong dev checkout).');
232
+ console.error('');
233
+ console.error(' Xử: npx clear-npx-cache (hoặc xoá thủ công cache npx) rồi chạy lại');
234
+ console.error(` npm i -g @educa-corp/sdd-framework@${VERSION} nếu dùng bản global`);
235
+ process.exit(1);
236
+ } else if (hasSources) {
237
+ // corePrebuilt && hasSources → DEV CHECKOUT, core/ đã khớp version.
238
+ // Cần lời nhắc này vì trước G43 thì `--init` LUÔN build, nên "sửa .tmpl rồi --init" từng có
239
+ // tác 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.
240
+ console.log('');
241
+ console.log(`ℹ️ core/ đã build sẵn (v${VERSION}) — bỏ qua bước build.`);
242
+ console.log(' Vừa sửa commands/*.tmpl · steps/ · templates/ ? Chạy `npm run build` trước.');
161
243
  }
244
+ // corePrebuilt && !hasSources → bản cài từ npm, mọi thứ đúng chỗ. IM LẶNG — đây là đường của
245
+ // consumer, và họ không cần biết bước build tồn tại.
162
246
 
163
247
  // ── --init mode: install to .agent/ + create .claude/commands/ shortcuts ─────
164
248
  if (isInit) {
@@ -205,27 +289,10 @@ if (isInit) {
205
289
  console.log('');
206
290
  console.log('Creating .claude/commands/ shortcuts ...');
207
291
  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
- }
292
+ const sc = writeShortcuts(agentDir, claudeCommandsDir, { verbose: true });
293
+ const commandFiles = sc.commandFiles;
294
+ const failedShortcuts = sc.failed;
295
+ reportShortcuts(sc, '.claude/commands/');
229
296
 
230
297
  // 3. Install module (optional --module flag)
231
298
  if (moduleName) {
@@ -383,17 +450,11 @@ if (isInit) {
383
450
  }
384
451
  }
385
452
 
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)`);
453
+ // Create .claude/commands/ shortcuts (cùng hàm với đường single-service — G44:
454
+ // trước đó hai chỗ này copy-paste nhau và chỉ một chỗ được sửa khi có thay đổi)
455
+ const svcSc = writeShortcuts(svcAgentDir, svcClaudeCmds, { verbose: false });
456
+ console.log(` ✅ ${svc.name}/.claude/commands/ (${svcSc.commandFiles.length} shortcuts)`);
457
+ reportShortcuts(svcSc, `${svc.name}/.claude/commands/`);
397
458
 
398
459
  // Generate project-context.yaml (only if not already present)
399
460
  const ctxPath = path.join(svcAgentDir, 'project-context.yaml');
@@ -510,99 +571,59 @@ if (isInit) {
510
571
  console.log(' 2. Commit .agent/ to git so your whole team has the framework');
511
572
  }
512
573
  console.log('');
513
- console.log('To upgrade later: bash scripts/upgrade.sh');
574
+ // G47 mục 1: dòng này từng in `bash scripts/upgrade.sh` sau MỌI lần cài — nhưng
575
+ // `scripts/` không bao giờ được cài vào project (coreMappings không có nó), nên
576
+ // consumer nhận "No such file or directory" ngay câu chỉ dẫn ĐẦU TIÊN họ đọc.
577
+ console.log('To upgrade later: /update-framework (trong Claude Code)');
514
578
  console.log(' (or: npx @educa-corp/sdd-framework@latest --init)');
515
579
  console.log('');
516
580
  process.exit(0);
517
581
  }
518
582
 
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
-
583
+ // ── Không phải --init không cài gì, chỉ hướng dẫn (GAPS-v3 G50) ─────────
584
+ //
585
+ // Trước đây chỗ này là hai chế độ 'legacy': `--project` và nhánh KHÔNG-CỜ (global).
586
+ // Cả hai chỉ copy commands/*.md vào .claude/commands/ và KHÔNG cài .agent/ — trong khi
587
+ // 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à
588
+ // 'Đọc .agent/project-context.yaml'. Kết quả không phải hỏng hẳn mà THOÁI HOÁ ÂM THẦM:
589
+ // • 6 lệnh QC chết (nạp skill lúc chạy từ .agent/skills/qc/)
590
+ // • 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
591
+ // và mất luật CHECKPOINT/scope. Không ai được báo.
592
+ // • /setup-ai-first Step 6b bảo copy từ .agent/templates/ — không có ở đó
593
+ // Và installer thì in '✅ Installed 32/32 commands'. Đúng lớp lỗi cả loạt GAP này chống:
594
+ // tín hiệu xanh trên thứ đã hết đúng.
595
+ //
596
+ // --init phủ MỌI ca thật (single-service · monorepo · umbrella). Legacy không giải quyết
597
+ // vấn đề nào mà --init không giải quyết — nó chỉ là di sản.
526
598
  console.log('');
527
599
  console.log('╔══════════════════════════════════════════╗');
528
600
  console.log('║ SDD Framework — CLI Installer ║');
529
601
  console.log(`║ v${VERSION} ║`);
530
602
  console.log('╚══════════════════════════════════════════╝');
531
603
  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
- }
604
+ if (isProject) {
605
+ console.log('ℹ️ `--project` đã được gỡ (v0.5.1).');
606
+ } else {
607
+ console.log('ℹ️ Chạy không kèm cờ nào thì không có gì để cài.');
553
608
  }
554
-
555
609
  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 ───────────────────────────────────────────────────────────────
610
+ console.log(' Chế độ cũ chỉ copy file lệnh vào .claude/commands/ mà KHÔNG cài .agent/.');
611
+ 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à');
612
+ console.log(' đọc .agent/project-context.yaml. Nên bản cài đó chạy MÀ KHÔNG có data-guard,');
613
+ console.log(' không có luật CHECKPOINT/scope, và cả pipeline QC không hoạt động — im lặng.');
591
614
  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
- }
615
+ console.log(' Dùng:');
616
+ console.log(' npx @educa-corp/sdd-framework --init # single-service');
617
+ console.log(' npx @educa-corp/sdd-framework --init --module java-spring --hooks');
618
+ console.log(' npx @educa-corp/sdd-framework --init --services be:java-spring,web:react');
619
+ console.log('');
620
+ console.log(' Đã lỡ cài kiểu cũ? Cứ chạy --init: nó phát hiện file lệnh cũ trong');
621
+ console.log(' .claude/commands/, backup rồi báo ra không đè im lặng.');
622
+ console.log('');
623
+ console.log(' Xem đủ tuỳ chọn: npx @educa-corp/sdd-framework --help');
605
624
  console.log('');
625
+ process.exit(0);
626
+
606
627
 
607
628
  // ── Helpers ───────────────────────────────────────────────────────────────
608
629
 
@@ -642,10 +663,15 @@ function readManifest(agentDir) {
642
663
  }
643
664
 
644
665
  /**
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
666
+ * Copy core/ → .agent/, backing up any file the project edited since the last install,
667
+ * AND removing files the framework no longer ships (GAPS-v3 G44).
668
+ *
669
+ * @returns {{ edited: string[], removed: string[], keptEdited: string[],
670
+ * backupDir: string|null, firstRun: boolean }}
671
+ * edited — file bị ghi đè VÀ có sửa đổi cục bộ (đã backup)
672
+ * removed — file framework đã bỏ, còn nguyên bản ⇒ đã xoá
673
+ * keptEdited — file framework đã bỏ NHƯNG người dùng đã sửa ⇒ GIỮ LẠI + backup
674
+ * firstRun — chưa có manifest nên lần này không phát hiện được sửa đổi
649
675
  */
650
676
  function installCore(coreDir, agentDir, version) {
651
677
  const manifest = readManifest(agentDir);
@@ -655,6 +681,12 @@ function installCore(coreDir, agentDir, version) {
655
681
  const edited = [];
656
682
  const written = {};
657
683
 
684
+ const backup = (abs, rel) => {
685
+ const bak = path.join(backupDir, rel);
686
+ fs.mkdirSync(path.dirname(bak), { recursive: true });
687
+ fs.copyFileSync(abs, bak);
688
+ };
689
+
658
690
  const walk = (src, dst) => {
659
691
  fs.mkdirSync(dst, { recursive: true });
660
692
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
@@ -666,9 +698,7 @@ function installCore(coreDir, agentDir, version) {
666
698
  // Locally edited == exists, we have a record of what we last wrote, and the
667
699
  // file no longer matches that record. Framework-only changes never match here.
668
700
  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);
701
+ backup(d, rel);
672
702
  edited.push(rel);
673
703
  }
674
704
  fs.copyFileSync(s, d);
@@ -677,33 +707,253 @@ function installCore(coreDir, agentDir, version) {
677
707
  };
678
708
  walk(coreDir, agentDir);
679
709
 
710
+ // ── Prune: gỡ file framework KHÔNG CÒN ship (G44) ──────────────────────────
711
+ //
712
+ // Trước G44, installCore chỉ copy — không có nhánh xoá nào. Nên một lệnh bị bỏ ở
713
+ // version mới nằm lại trong .agent/commands/ và .claude/commands/ của mọi project
714
+ // VĨNH VIỄN: vẫn hiện trong menu `/`, vẫn chạy được, vẫn mang logic của version cũ
715
+ // — kể cả khi framework đã bỏ nó VÌ NÓ SAI. Và `/update-framework` Step 5 thì hứa
716
+ // báo cáo "Removed", một trạng thái installer không thể tạo ra.
717
+ //
718
+ // Đây là hành vi XOÁ FILE TRONG PROJECT NGƯỜI DÙNG, nên chỉ xoá khi CẢ BA đúng:
719
+ // 1. có trong manifest cũ ⇒ do framework đặt vào, không phải người dùng tạo
720
+ // 2. không có trong core/ mới ⇒ framework đã bỏ
721
+ // 3. hash khớp manifest ⇒ nguyên bản, xoá không mất gì của ai
722
+ // Đúng (1)+(2) mà sai (3) ⇒ người dùng đã sửa ⇒ GIỮ LẠI + backup + báo. Thà để lại
723
+ // một file lạc còn hơn xoá thứ ai đó đã bỏ công viết.
724
+ const removed = [];
725
+ const keptEdited = [];
726
+ if (manifest) {
727
+ for (const rel of Object.keys(manifest.files)) {
728
+ if (written[rel]) continue; // vẫn còn ship
729
+ const abs = path.join(agentDir, rel);
730
+ if (!fs.existsSync(abs)) continue; // đã biến mất từ trước
731
+ if (sha1(abs) === manifest.files[rel]) {
732
+ fs.unlinkSync(abs);
733
+ removed.push(rel);
734
+ } else {
735
+ backup(abs, rel);
736
+ keptEdited.push(rel);
737
+ }
738
+ }
739
+ // Dọn thư mục rỗng còn lại sau khi prune (vd cả một module bị bỏ). Chỉ xoá dir
740
+ // RỖNG — không recursive, nên không thể chạm vào file nào của người dùng.
741
+ for (const rel of removed) {
742
+ let dir = path.dirname(path.join(agentDir, rel));
743
+ while (dir !== agentDir && dir.startsWith(agentDir)) {
744
+ try {
745
+ if (fs.readdirSync(dir).length) break;
746
+ fs.rmdirSync(dir);
747
+ } catch { break; }
748
+ dir = path.dirname(dir);
749
+ }
750
+ }
751
+ }
752
+
753
+ // GIỮ phần `shortcuts` của manifest cũ. installCore chạy TRƯỚC writeShortcuts, nên
754
+ // ghi một object mới chỉ có `files` sẽ xoá sạch lịch sử shortcut — và writeShortcuts
755
+ // sau đó thấy `prev = {}`, mất cả prune LẪN phát hiện trùng tên. Bug này đã xảy ra
756
+ // thật khi làm G44: .agent/commands/debug.md được gỡ nhưng shortcut của nó thì không.
680
757
  fs.writeFileSync(
681
758
  path.join(agentDir, MANIFEST_NAME),
682
- JSON.stringify({ version, generated_at: new Date().toISOString(), files: written }, null, 2),
759
+ JSON.stringify({
760
+ version,
761
+ generated_at: new Date().toISOString(),
762
+ files: written,
763
+ shortcuts: (manifest && manifest.shortcuts) || {},
764
+ }, null, 2),
683
765
  'utf8'
684
766
  );
685
767
 
686
- return { edited, backupDir: edited.length ? backupDir : null, firstRun };
768
+ return {
769
+ edited, removed, keptEdited,
770
+ backupDir: (edited.length || keptEdited.length) ? backupDir : null,
771
+ firstRun,
772
+ };
687
773
  }
688
774
 
689
775
  function reportOverwritten(result, agentLabel) {
690
776
  if (result.firstRun) {
691
777
  console.log('');
692
778
  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ê.`);
779
+ console.log(` sửa trong ${agentLabel} mà bị ghi đè sẽ được backup + liệt kê,`);
780
+ console.log(` và file framework đã bỏ sẽ được gỡ khỏi ${agentLabel}.`);
694
781
  return;
695
782
  }
696
- if (!result.edited.length) return;
697
783
 
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.`);
784
+ const bak = result.backupDir
785
+ ? path.relative(process.cwd(), result.backupDir).replace(/\\/g, '/')
786
+ : null;
787
+ const list = (arr, n = 12) => {
788
+ for (const f of arr.slice(0, n)) console.log(` ${agentLabel}${f}`);
789
+ if (arr.length > n) console.log(` ${arr.length - n} file nữa`);
790
+ };
791
+
792
+ if (result.edited.length) {
793
+ console.log('');
794
+ console.log(` ⚠️ ${result.edited.length} file trong ${agentLabel} đã bị bạn sửa và VỪA BỊ GHI ĐÈ:`);
795
+ list(result.edited);
796
+ console.log(` Bản cũ đã lưu ở: ${bak}/`);
797
+ console.log(` ${agentLabel} là bản mirror SINH RA — sửa trực tiếp ở đó không bền.`);
798
+ console.log(` Muốn giữ thay đổi: chuyển nó vào repo framework rồi phát hành,`);
799
+ console.log(` hoặc áp lại thủ công sau khi xem diff. Xoá thư mục backup khi xong.`);
800
+ }
801
+
802
+ // 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ễ
803
+ // 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
804
+ // giờ đã không còn.
805
+ if (result.removed.length) {
806
+ console.log('');
807
+ console.log(` 🗑️ ${result.removed.length} file framework đã bị BỎ ở version này — đã gỡ khỏi ${agentLabel}:`);
808
+ list(result.removed, 8);
809
+ console.log(` Chúng còn nguyên bản (không ai sửa) nên gỡ đi không mất gì.`);
810
+ console.log(` Lệnh nào biến mất khỏi menu / là vì vậy.`);
811
+ }
812
+
813
+ if (result.keptEdited.length) {
814
+ console.log('');
815
+ console.log(` ⚠️ ${result.keptEdited.length} file framework đã bị bỏ NHƯNG bạn đã sửa — GIỮ LẠI:`);
816
+ list(result.keptEdited, 8);
817
+ console.log(` Đã backup ở: ${bak}/`);
818
+ console.log(` Framework không dùng chúng nữa. Xoá tay khi bạn chắc không cần,`);
819
+ console.log(` hoặc chuyển thay đổi đó vào repo framework nếu nó còn giá trị.`);
820
+ }
821
+ }
822
+
823
+ // ── Shortcut .claude/commands/ — có manifest, có prune, có cảnh báo trùng tên ──
824
+ //
825
+ // Vì sao cần (GAPS-v3 G44): bản cũ ghi 32 file vào .claude/commands/ VÔ ĐIỀU KIỆN,
826
+ // không backup, không cảnh báo, và không bao giờ gỡ shortcut của lệnh đã bị bỏ.
827
+ // Hai hệ quả:
828
+ // • Tên lệnh rất chung — /debug, /sync, /learn, /code-review — và trùng cả với
829
+ // skill/plugin sẵn có của Claude Code. Project đã có /sync riêng thì MẤT nó,
830
+ // im lặng, ngay lần cài đầu. Bảo vệ G24 chỉ áp cho .agent/, không tới đây.
831
+ // • Lệnh bị bỏ ở version mới vẫn hiện trong menu `/` mãi mãi, và khi ai đó gõ nó
832
+ // thì file .agent/commands/ đích đã không còn.
833
+ //
834
+ // Shortcut được ghi vào manifest ở khoá `shortcuts` (path tương đối gốc project),
835
+ // tách khỏi `files` (tương đối .agent/). Manifest cũ chỉ có `files` → vẫn đọc được.
836
+ function shortcutBody(cmdFile) {
837
+ const cmdName = cmdFile.replace(/\.md$/, '');
838
+ return `# /${cmdName}\n\nRead the full command definition from \`.agent/commands/${cmdFile}\``
839
+ + ` and execute it with arguments: $ARGUMENTS\n`;
840
+ }
841
+
842
+ function writeShortcuts(agentDir, claudeCommandsDir, { verbose }) {
843
+ const manifest = readManifest(agentDir) || {};
844
+ const prev = manifest.shortcuts || {};
845
+ const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
846
+
847
+ fs.mkdirSync(claudeCommandsDir, { recursive: true });
848
+ const commandFiles = fs.readdirSync(path.join(agentDir, 'commands')).filter(f => f.endsWith('.md'));
849
+
850
+ const written = {};
851
+ const failed = [];
852
+ const collided = [];
853
+ const removed = [];
854
+ const keptEdited = [];
855
+ const legacyFound = []; // file lệnh ĐẦY ĐỦ từ bản cài legacy — KHÁC lệnh của project
856
+ const sha1s = (str) => require('crypto').createHash('sha1').update(str).digest('hex');
857
+
858
+ for (const cmdFile of commandFiles) {
859
+ const abs = path.join(claudeCommandsDir, cmdFile);
860
+ const body = shortcutBody(cmdFile);
861
+ const key = cmdFile;
862
+
863
+ // Đã 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
864
+ // 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:
865
+ //
866
+ // (a) BẢN CÀI FRAMEWORK CŨ (legacy). Legacy mode copy file lệnh ĐẦY ĐỦ (60–110 KB)
867
+ // vào .claude/commands/ với đúng 32 tên đó. G50 gỡ legacy nên mọi project đã cài
868
+ // kiểu cũ SẼ đi qua đường này. Gọi nó là "slash command của bạn" là SAI: người
869
+ // 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ỉ
870
+ // là bản framework cũ.
871
+ // (b) slash command THẬT của project — tên lệnh framework rất chung (/debug /sync
872
+ // /learn) nên trùng là chuyện có thật.
873
+ //
874
+ // Nhận diện: shortcut ~150 B · bản legacy 60–110 KB · lệnh project thường < 5 KB.
875
+ // 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;
876
+ // `## Gate` là dấu hiệu thứ hai (mọi file lệnh built đều có, shortcut thì không).
877
+ if (fs.existsSync(abs) && !prev[key]) {
878
+ const cur = fs.readFileSync(abs, 'utf8');
879
+ if (cur !== body) {
880
+ const bak = path.join(agentDir, `.overwritten-${stamp}-shortcuts`, cmdFile);
881
+ try {
882
+ fs.mkdirSync(path.dirname(bak), { recursive: true });
883
+ fs.copyFileSync(abs, bak);
884
+ (cur.length > 20 * 1024 || cur.includes('## Gate') ? legacyFound : collided).push(cmdFile);
885
+ } catch { /* backup fail thì vẫn báo ở dưới */ }
886
+ }
887
+ }
888
+
889
+ try {
890
+ fs.writeFileSync(abs, body, 'utf8');
891
+ written[key] = sha1s(body);
892
+ if (verbose) console.log(` ✅ /${cmdFile.replace(/\.md$/, '')}`);
893
+ } catch {
894
+ if (verbose) console.log(` ❌ /${cmdFile.replace(/\.md$/, '')} (failed to write shortcut)`);
895
+ failed.push(cmdFile.replace(/\.md$/, ''));
896
+ }
897
+ }
898
+
899
+ // Prune — cùng ba điều kiện như installCore: có trong manifest cũ, không còn ship,
900
+ // và còn nguyên bản. Người dùng sửa shortcut thì giữ lại.
901
+ for (const key of Object.keys(prev)) {
902
+ if (written[key]) continue;
903
+ const abs = path.join(claudeCommandsDir, key);
904
+ if (!fs.existsSync(abs)) continue;
905
+ if (sha1s(fs.readFileSync(abs, 'utf8')) === prev[key]) {
906
+ try { fs.unlinkSync(abs); removed.push(key); } catch { /* bỏ qua */ }
907
+ } else {
908
+ keptEdited.push(key);
909
+ }
910
+ }
911
+
912
+ // Ghi lại manifest (installCore đã ghi `files` trước đó — nối thêm `shortcuts`)
913
+ try {
914
+ const mPath = path.join(agentDir, MANIFEST_NAME);
915
+ const m = readManifest(agentDir) || { files: {} };
916
+ m.shortcuts = written;
917
+ fs.writeFileSync(mPath, JSON.stringify(m, null, 2), 'utf8');
918
+ } catch { /* manifest không ghi được thì lần sau coi như firstRun — không chặn cài */ }
919
+
920
+ return { commandFiles, failed, collided, legacyFound, removed, keptEdited };
921
+ }
922
+
923
+ function reportShortcuts(sc, label) {
924
+ // 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ì
925
+ // 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ọ
926
+ // 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.
927
+ if ((sc.legacyFound || []).length) {
928
+ console.log('');
929
+ console.log(` 🔄 ${sc.legacyFound.length} file trong ${label} là BẢN CÀI FRAMEWORK CŨ (legacy)`);
930
+ console.log(` — đã thay bằng shortcut. Đây KHÔNG phải slash command bạn tự viết.`);
931
+ console.log(` Bản cũ vẫn được backup ở .agent/.overwritten-*-shortcuts/, nhưng bạn gần như`);
932
+ 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.`);
933
+ console.log(` (Chế độ cài legacy đã được gỡ ở v0.5.1: nó cài lệnh mà không cài .agent/,`);
934
+ console.log(` nên lệnh chạy thiếu guardrail và pipeline QC không hoạt động.)`);
935
+ }
936
+ if (sc.collided.length) {
937
+ console.log('');
938
+ console.log(` ⚠️ ${sc.collided.length} slash command CÓ SẴN của project vừa bị shortcut framework ghi đè:`);
939
+ for (const f of sc.collided.slice(0, 10)) console.log(` ${label}${f}`);
940
+ if (sc.collided.length > 10) console.log(` … và ${sc.collided.length - 10} file nữa`);
941
+ console.log(` Bản cũ đã lưu trong .agent/.overwritten-*-shortcuts/`);
942
+ console.log(` Tên lệnh của framework rất chung (/debug /sync /learn) nên dễ trùng.`);
943
+ console.log(` Muốn giữ lệnh của bạn: đổi tên nó rồi khôi phục từ backup.`);
944
+ }
945
+ if (sc.removed.length) {
946
+ console.log('');
947
+ console.log(` 🗑️ ${sc.removed.length} shortcut của lệnh đã bị bỏ — đã gỡ khỏi ${label}`);
948
+ for (const f of sc.removed.slice(0, 8)) console.log(` /${f.replace(/\.md$/, '')}`);
949
+ if (sc.removed.length > 8) console.log(` … và ${sc.removed.length - 8} nữa`);
950
+ }
951
+ if (sc.keptEdited.length) {
952
+ console.log('');
953
+ console.log(` ⚠️ ${sc.keptEdited.length} shortcut của lệnh đã bị bỏ NHƯNG bạn đã sửa — giữ lại:`);
954
+ for (const f of sc.keptEdited.slice(0, 8)) console.log(` ${label}${f}`);
955
+ console.log(` Lệnh đích trong .agent/commands/ đã không còn — shortcut này sẽ lỗi khi gọi.`);
956
+ }
707
957
  }
708
958
 
709
959
  function installDataGuardHook() {
@@ -719,34 +969,83 @@ function installDataGuardHook() {
719
969
  console.log(` ✅ Hook copied to: .claude/hooks/data-guard.js`);
720
970
 
721
971
  // 2. Update .claude/settings.json
972
+ //
973
+ // settings.json HỎNG thì DỪNG, đừng ghi đè. Bản cũ `catch {}` rồi đi tiếp với
974
+ // `settings = {}`, và bước writeFileSync bên dưới ghi đè TOÀN BỘ file bằng đúng
975
+ // 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
976
+ // Claude Code của project, im lặng.
722
977
  let settings = {};
723
978
  if (fs.existsSync(settingsPath)) {
724
- try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch {}
979
+ const raw = fs.readFileSync(settingsPath, 'utf8');
980
+ try {
981
+ settings = JSON.parse(raw);
982
+ } catch (err) {
983
+ console.log('');
984
+ console.log(` ❌ .claude/settings.json không phải JSON hợp lệ: ${err.message}`);
985
+ 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ó.');
986
+ console.log(' Sửa file rồi chạy lại với --hooks.');
987
+ console.log('');
988
+ console.log(' ℹ️ Script hook đã được copy; chỉ còn thiếu bước đăng ký. Mẫu:');
989
+ console.log(' .agent/hooks/settings.json');
990
+ return;
991
+ }
992
+ if (settings === null || typeof settings !== 'object' || Array.isArray(settings)) {
993
+ console.log('');
994
+ console.log(' ❌ .claude/settings.json không phải một JSON object — KHÔNG đăng ký hook.');
995
+ console.log('');
996
+ return;
997
+ }
725
998
  }
726
999
 
727
1000
  // Merge: add PreToolUse hook if not already present
728
1001
  if (!settings.hooks) settings.hooks = {};
729
1002
  if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
730
1003
 
1004
+ // `Grep` BẮT BUỘC có mặt: Grep với output_mode:"content" ĐỌC ĐƯỢC nội dung file,
1005
+ // 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
1006
  const hookEntry = {
732
- matcher: 'Read|Write|Edit|Bash',
1007
+ matcher: 'Read|Write|Edit|Grep|NotebookEdit|Bash',
733
1008
  hooks: [{ type: 'command', command: 'node .claude/hooks/data-guard.js' }],
734
1009
  };
735
1010
 
736
- const alreadyRegistered = settings.hooks.PreToolUse.some(
1011
+ // Tìm entry đã có (nếu có) — KHÔNG chỉ để bỏ qua, mà để NÂNG CẤP matcher.
1012
+ //
1013
+ // 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
1014
+ // hook TRƯỚC bản sửa G42 sẽ giữ matcher cũ `Read|Write|Edit|Bash` — thiếu `Grep` —
1015
+ // 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
1016
+ // đúng cái lệnh đáng ra phải bịt nó.
1017
+ const existing = settings.hooks.PreToolUse.find(
737
1018
  h => h.hooks && h.hooks.some(hh => hh.command && hh.command.includes('data-guard.js'))
738
1019
  );
739
1020
 
740
- if (!alreadyRegistered) {
1021
+ const REQUIRED_TOOLS = hookEntry.matcher.split('|');
1022
+
1023
+ if (!existing) {
741
1024
  settings.hooks.PreToolUse.push(hookEntry);
742
1025
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
743
- console.log(` ✅ Hook registered in: .claude/settings.json`);
1026
+ console.log(' ✅ Hook registered in: .claude/settings.json');
744
1027
  } else {
745
- console.log(` ℹ️ Hook already registered in .claude/settings.json`);
1028
+ const have = String(existing.matcher || '').split('|').map(s => s.trim());
1029
+ const missing = REQUIRED_TOOLS.filter(t => !have.includes(t));
1030
+ if (missing.length) {
1031
+ existing.matcher = hookEntry.matcher;
1032
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
1033
+ console.log(` ✅ Hook matcher nâng cấp: thêm ${missing.join(', ')}`);
1034
+ if (missing.includes('Grep')) {
1035
+ console.log(' (thiếu Grep = lỗ đọc file: Grep với output_mode:"content" trả về nội dung)');
1036
+ }
1037
+ } else {
1038
+ console.log(' ℹ️ Hook already registered in .claude/settings.json');
1039
+ }
746
1040
  }
747
1041
 
748
1042
  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, ...');
1043
+ console.log(' 🔒 Data guard đang canh các tool: Read · Write · Edit · Grep · NotebookEdit · Bash');
1044
+ console.log(' Chặn: .env* · *.key/.pem/.p12/.jks · application-prod.* · .ssh/ .aws/ secrets/');
1045
+ console.log(' file cấu hình/dữ liệu có tên chứa secret|credential|password|token|api-key');
1046
+ console.log('');
1047
+ console.log(' KHÔNG chặn (có chủ ý — đây là công việc của framework):');
1048
+ console.log(' • specs/ .trace/ docs/ .agent/ feedback/ — vùng làm việc, không chứa secret thật');
1049
+ console.log(' • file mã nguồn (.java/.ts/.dart/.kt…) — PasswordResetController.java là code');
1050
+ console.log(' Guard không canh được đường đưa dữ liệu ra ngoài (WebFetch…) — nó là file guard.');
752
1051
  }