chati-dev 4.2.1 → 4.3.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 (222) hide show
  1. package/README.md +80 -53
  2. package/bin/chati.js +150 -5
  3. package/framework/agents/build/dev.md +509 -76
  4. package/framework/agents/deploy/devops.md +40 -25
  5. package/framework/agents/discover/brief.md +156 -22
  6. package/framework/agents/discover/brownfield-wu.md +24 -14
  7. package/framework/agents/discover/greenfield-wu.md +100 -14
  8. package/framework/agents/plan/architect-data-engineer.md +6 -6
  9. package/framework/agents/plan/architect-system.md +46 -12
  10. package/framework/agents/plan/architect.md +40 -20
  11. package/framework/agents/plan/detail.md +36 -24
  12. package/framework/agents/plan/phases.md +36 -26
  13. package/framework/agents/plan/tasks.md +114 -33
  14. package/framework/agents/plan/ux-brand-architect.md +240 -8
  15. package/framework/agents/plan/ux-component-engineer.md +28 -13
  16. package/framework/agents/plan/ux-researcher.md +7 -6
  17. package/framework/agents/plan/ux.md +55 -22
  18. package/framework/agents/quality/qa-implementation.md +143 -74
  19. package/framework/agents/quality/qa-planning.md +115 -42
  20. package/framework/agents/quality/qa-visual.md +439 -0
  21. package/framework/agents/shared/visualizer.md +128 -0
  22. package/framework/config.yaml +8 -7
  23. package/framework/constitution.md +127 -43
  24. package/framework/context/governance.md +12 -7
  25. package/framework/context/quality.md +6 -5
  26. package/framework/context/root.md +6 -6
  27. package/framework/data/entity-registry.yaml +377 -4
  28. package/framework/data/model-limits.json +19 -0
  29. package/framework/domains/agents/qa-visual.yaml +74 -0
  30. package/framework/domains/constitution.yaml +46 -2
  31. package/framework/domains/workflows/greenfield-fullstack.yaml +2 -2
  32. package/framework/hooks/advance-trigger.js +131 -0
  33. package/framework/hooks/brief-validator.js +83 -0
  34. package/framework/hooks/constitution-guard.js +91 -36
  35. package/framework/hooks/license-guard.js +129 -190
  36. package/framework/hooks/mode-governance.js +68 -16
  37. package/framework/hooks/model-governance.js +19 -8
  38. package/framework/hooks/package.json +3 -0
  39. package/framework/hooks/post-dev.js +207 -0
  40. package/framework/hooks/prism-engine.js +288 -105
  41. package/framework/hooks/read-protection.js +37 -9
  42. package/framework/hooks/reasoning-escalator.js +371 -0
  43. package/framework/hooks/reference-trigger.js +117 -0
  44. package/framework/hooks/session-digest.js +94 -20
  45. package/framework/hooks/settings.json +32 -1
  46. package/framework/hooks/style-guard.js +52 -13
  47. package/framework/hooks/team-quality-gate.js +58 -25
  48. package/framework/hooks/undercover-guard.js +34 -13
  49. package/framework/i18n/en.yaml +3 -3
  50. package/framework/i18n/es.yaml +3 -3
  51. package/framework/i18n/fr.yaml +3 -3
  52. package/framework/i18n/pt.yaml +3 -3
  53. package/framework/intelligence/context-engine.md +4 -5
  54. package/framework/intelligence/decision-engine.md +1 -1
  55. package/framework/intelligence/hooks-performance.md +3 -3
  56. package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
  57. package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
  58. package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
  59. package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
  60. package/framework/orchestrator/chati-router.js +387 -0
  61. package/framework/orchestrator/chati-update.md +40 -40
  62. package/framework/orchestrator/chati.md +318 -115
  63. package/framework/scaffold/motion-premium/README.md +89 -0
  64. package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
  65. package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
  66. package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
  67. package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
  68. package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
  69. package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
  70. package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
  71. package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
  72. package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
  73. package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
  74. package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
  75. package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
  76. package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
  77. package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
  78. package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
  79. package/framework/scaffold/motion-premium/scaffold.yaml +174 -0
  80. package/framework/scaffold/motion-premium-3d/README.md +80 -0
  81. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
  82. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
  83. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
  84. package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
  85. package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
  86. package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
  87. package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
  88. package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
  89. package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
  90. package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
  91. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
  92. package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
  93. package/framework/scaffold/motion-premium-3d/scaffold.yaml +133 -0
  94. package/framework/schemas/session.schema.json +109 -21
  95. package/framework/scripts/reference-capture.js +430 -0
  96. package/framework/scripts/visual-qa.js +674 -0
  97. package/framework/tasks/orchestrator-handoff.md +1 -1
  98. package/framework/tasks/orchestrator-resume.md +1 -1
  99. package/framework/tasks/orchestrator-route.md +1 -1
  100. package/framework/tasks/orchestrator-status.md +3 -3
  101. package/framework/tasks/qa-planning-gate-define.md +1 -1
  102. package/framework/templates/brandbook-html-tmpl.md +1 -1
  103. package/framework/templates/brandbook-tmpl.yaml +1 -1
  104. package/framework/templates/component-spec-tmpl.yaml +1 -1
  105. package/framework/templates/design-token-tmpl.yaml +1 -1
  106. package/framework/templates/icon-system-tmpl.yaml +1 -1
  107. package/framework/templates/team-planning-tasks.yaml +6 -5
  108. package/framework/workflows/brownfield-discovery.yaml +2 -2
  109. package/framework/workflows/brownfield-fullstack.yaml +15 -11
  110. package/framework/workflows/brownfield-service.yaml +14 -10
  111. package/framework/workflows/brownfield-ui.yaml +15 -11
  112. package/framework/workflows/greenfield-fullstack.yaml +16 -13
  113. package/framework/workflows/quick-flow.yaml +3 -3
  114. package/framework/workflows/standard-flow.yaml +12 -9
  115. package/package.json +10 -5
  116. package/src/autonomy/autonomous-gate.js +1 -0
  117. package/src/autonomy/build-state.js +1 -2
  118. package/src/autonomy/progress-reporter.js +1 -1
  119. package/src/config/agent-customizer.js +11 -3
  120. package/src/config/claude-settings-generator.js +226 -0
  121. package/src/config/context-file-generator.js +41 -21
  122. package/src/config/framework-adapter.js +1 -0
  123. package/src/config/gemini-hooks-generator.js +76 -6
  124. package/src/config/mcp-configs.js +1 -0
  125. package/src/context/layers/l1-global.js +2 -1
  126. package/src/dashboard/data-reader.js +4 -3
  127. package/src/dashboard/layout.js +2 -1
  128. package/src/decision/analyzer.js +6 -30
  129. package/src/decision/engine.js +4 -28
  130. package/src/decision/registry-healer.js +3 -2
  131. package/src/decision/registry-updater.js +23 -14
  132. package/src/extensions/loader.js +2 -8
  133. package/src/gates/g1-planning-complete.js +2 -1
  134. package/src/gates/g2-qa-planning.js +2 -1
  135. package/src/gates/g3-implementation.js +2 -1
  136. package/src/gates/g4-qa-implementation.js +3 -2
  137. package/src/gates/g5-deploy-ready.js +2 -1
  138. package/src/health/engine.js +4 -3
  139. package/src/installer/core.js +557 -81
  140. package/src/installer/preflight.js +131 -0
  141. package/src/installer/provider-overlay.js +3 -3
  142. package/src/installer/scaffold-applier.js +358 -0
  143. package/src/installer/templates.js +52 -31
  144. package/src/installer/validator.js +17 -12
  145. package/src/intelligence/registry-manager.js +22 -29
  146. package/src/intelligence/timeline.js +11 -6
  147. package/src/license/commands.js +1 -1
  148. package/src/license/wait.js +102 -0
  149. package/src/memory/agent-memory.js +81 -0
  150. package/src/memory/dream.js +32 -1
  151. package/src/merger/replace-merger.js +28 -15
  152. package/src/orchestrator/agent-selector.js +2 -1
  153. package/src/orchestrator/cli.js +1908 -80
  154. package/src/orchestrator/doctor.js +270 -0
  155. package/src/orchestrator/handoff-engine.js +4 -3
  156. package/src/orchestrator/index.js +2 -0
  157. package/src/orchestrator/pipeline-manager.js +306 -15
  158. package/src/orchestrator/session-manager.js +331 -6
  159. package/src/tasks/handoff.js +3 -2
  160. package/src/telemetry/config.js +4 -3
  161. package/src/telemetry/schema.js +1 -0
  162. package/src/terminal/collector.js +3 -2
  163. package/src/terminal/index.js +1 -2
  164. package/src/terminal/isolation.js +52 -18
  165. package/src/terminal/prompt-builder.js +42 -25
  166. package/src/terminal/run-parallel.js +1 -1
  167. package/src/terminal/run-team.js +3 -3
  168. package/src/terminal/team-task-list.js +43 -4
  169. package/src/upgrade/backup.js +3 -2
  170. package/src/upgrade/checker.js +3 -2
  171. package/src/upgrade/migrator.js +65 -7
  172. package/src/upgrade/tracked-files-detector.js +86 -0
  173. package/src/upgrade/user-messages.js +94 -0
  174. package/src/utils/config-parser.js +2 -1
  175. package/src/utils/feature-flags.js +2 -1
  176. package/src/utils/flatten-entities.js +69 -0
  177. package/src/utils/framework-dir.js +16 -0
  178. package/src/utils/model-id.js +85 -0
  179. package/src/utils/provider-limits.js +84 -23
  180. package/src/utils/schema-validator.js +1 -1
  181. package/src/wizard/i18n.js +5 -4
  182. package/src/wizard/index.js +14 -0
  183. package/assets/logo - c/303/263pia.png +0 -0
  184. package/assets/logo.svg +0 -42
  185. package/assets/logo2.png +0 -0
  186. package/assets/social-preview.png +0 -0
  187. package/scripts/bundle-framework.js +0 -69
  188. package/scripts/changelog-generator.js +0 -222
  189. package/scripts/codebase-mapper.js +0 -728
  190. package/scripts/commit-message-generator.js +0 -167
  191. package/scripts/coverage-analyzer.js +0 -260
  192. package/scripts/dependency-analyzer.js +0 -280
  193. package/scripts/doctor/checks/agents.js +0 -77
  194. package/scripts/doctor/checks/constitution.js +0 -41
  195. package/scripts/doctor/checks/domain-alignment.js +0 -58
  196. package/scripts/doctor/checks/prism-layers.js +0 -84
  197. package/scripts/doctor/checks/registry.js +0 -55
  198. package/scripts/doctor/checks/schemas.js +0 -61
  199. package/scripts/doctor/fixes/reference-fix.js +0 -100
  200. package/scripts/doctor/fixes/registry-fix.js +0 -56
  201. package/scripts/doctor/index.js +0 -212
  202. package/scripts/framework-analyzer.js +0 -308
  203. package/scripts/generate-constitution-domain.js +0 -253
  204. package/scripts/generate-signing-key.js +0 -33
  205. package/scripts/health-check.js +0 -481
  206. package/scripts/ide-sync.js +0 -326
  207. package/scripts/performance-analyzer.js +0 -325
  208. package/scripts/plan-tracker.js +0 -278
  209. package/scripts/populate-entity-registry.js +0 -481
  210. package/scripts/pr-review.js +0 -317
  211. package/scripts/rollback-manager.js +0 -310
  212. package/scripts/semantic-lint.js +0 -328
  213. package/scripts/sign-manifest.js +0 -53
  214. package/scripts/stuck-detector.js +0 -343
  215. package/scripts/test-quality-assessment.js +0 -257
  216. package/scripts/validate-agents.js +0 -368
  217. package/scripts/validate-package.js +0 -367
  218. package/scripts/validate-tasks.js +0 -465
  219. package/src/autonomy/worktree-manager.js +0 -250
  220. package/src/intelligence/decision-engine.js +0 -256
  221. package/src/intelligence/document-sharder.js +0 -221
  222. package/src/intelligence/elicitation.js +0 -265
@@ -1,11 +1,13 @@
1
- import { mkdirSync, writeFileSync, copyFileSync, existsSync, readFileSync, readdirSync } from 'fs';
2
- import { join, dirname } from 'path';
1
+ import { mkdirSync, writeFileSync, copyFileSync, existsSync, readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, dirname, basename } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
+ import { createRequire } from 'module';
4
5
  import { IDE_CONFIGS, IDE_TO_PROVIDER } from '../config/ide-configs.js';
5
6
  import { generateClaudeMCPConfig } from '../config/mcp-configs.js';
6
7
  import { generateSessionYaml, generateConfigYaml, generateClaudeMd, generateClaudeLocalMd, generateCodexSkill, generateGeminiRouter, generateGeminiSessionLock, generateAgentsOverrideMd, generateCodexConstitutionGuardRules, generateCodexReadProtectionRules } from './templates.js';
7
8
  import { generateContextFiles } from '../config/context-file-generator.js';
8
9
  import { adaptFrameworkFile, ADAPTABLE_FILES } from '../config/framework-adapter.js';
10
+ import { generateClaudeSettings } from '../config/claude-settings-generator.js';
9
11
  import { generateProviderOverlays } from './provider-overlay.js';
10
12
  import { verifyManifest } from './manifest.js';
11
13
 
@@ -21,7 +23,15 @@ const FRAMEWORK_SOURCE = existsSync(BUNDLED_SOURCE) ? BUNDLED_SOURCE : MONOREPO_
21
23
  * Install Chati.dev framework into target directory
22
24
  */
23
25
  export async function installFramework(config) {
24
- const { targetDir, projectType, language, selectedIDEs, selectedMCPs, projectName, version, llmProvider, allProviders } = config;
26
+ const { targetDir, projectType, language, selectedIDEs, selectedMCPs, projectName, llmProvider, allProviders } = config;
27
+ // Always derive version from package.json (single source of truth)
28
+ let { version } = config;
29
+ if (!version) {
30
+ try {
31
+ const pkgPath = join(__dirname, '..', '..', 'package.json');
32
+ version = JSON.parse(readFileSync(pkgPath, 'utf-8')).version;
33
+ } catch { version = 'unknown'; }
34
+ }
25
35
 
26
36
  // 0. Verify framework signature (supply chain protection)
27
37
  const manifestPath = join(FRAMEWORK_SOURCE, 'manifest.json');
@@ -39,17 +49,36 @@ export async function installFramework(config) {
39
49
 
40
50
  // 1. Create .chati/ session directory
41
51
  createDir(join(targetDir, '.chati'));
42
- writeFileSync(
43
- join(targetDir, '.chati', 'session.yaml'),
44
- generateSessionYaml({ projectName, projectType, language, selectedIDEs, selectedMCPs, llmProvider, allProviders }),
45
- 'utf-8'
46
- );
52
+ const sessionPath = join(targetDir, '.chati', 'session.yaml');
53
+ // Preserve existing session.yaml on reinstall/recovery (don't destroy pipeline state).
54
+ // Only write fresh session if one does not exist.
55
+ if (!existsSync(sessionPath)) {
56
+ writeFileSync(
57
+ sessionPath,
58
+ generateSessionYaml({ projectName, projectType, language, selectedIDEs, selectedMCPs, llmProvider, allProviders }),
59
+ 'utf-8'
60
+ );
61
+ }
47
62
 
48
- // 2. Create chati.dev/ framework directory (copy from source)
49
- const frameworkDir = join(targetDir, 'chati.dev');
63
+ // 2. Create .chati.dev/ framework directory (hidden dot prefix)
64
+ // The source is chati.dev/ in the monorepo; installed as .chati.dev/ in user projects.
65
+ // Text replacement during copy handles all internal path references.
66
+ const frameworkDir = join(targetDir, '.chati.dev');
50
67
  createDir(frameworkDir);
51
68
 
52
- // Copy framework structure
69
+ // 2b. Create artifacts/ at PROJECT ROOT (visible to user — this is their output)
70
+ const artifactsDir = join(targetDir, 'artifacts');
71
+ const artifactDirs = [
72
+ '0-WU', '1-Brief', '2-PRD',
73
+ '3-Architecture', '4-UX', '5-Phases',
74
+ '6-Tasks', '7-QA-Planning', '8-QA-Visual', '9-QA-Implementation', '10-Deploy',
75
+ 'handoffs', 'decisions',
76
+ ];
77
+ for (const dir of artifactDirs) {
78
+ createDir(join(artifactsDir, dir));
79
+ }
80
+
81
+ // Copy framework structure (everything EXCEPT artifacts, which live at root)
53
82
  const frameworkDirs = [
54
83
  'orchestrator',
55
84
  'agents/discover', 'agents/plan', 'agents/quality', 'agents/build', 'agents/deploy',
@@ -57,10 +86,6 @@ export async function installFramework(config) {
57
86
  'schemas', 'frameworks', 'intelligence', 'patterns',
58
87
  'hooks', 'domains', 'domains/agents', 'domains/workflows',
59
88
  'i18n', 'migrations', 'data', 'context',
60
- 'artifacts/0-WU', 'artifacts/1-Brief', 'artifacts/2-PRD',
61
- 'artifacts/3-Architecture', 'artifacts/4-UX', 'artifacts/5-Phases',
62
- 'artifacts/6-Tasks', 'artifacts/7-QA-Planning', 'artifacts/9-QA-Implementation', 'artifacts/10-Deploy',
63
- 'artifacts/handoffs', 'artifacts/decisions',
64
89
  ];
65
90
 
66
91
  for (const dir of frameworkDirs) {
@@ -81,6 +106,7 @@ export async function installFramework(config) {
81
106
  'tasks/durable', 'tasks/daily',
82
107
  'qa-planning/durable', 'qa-planning/daily',
83
108
  'qa-implementation/durable', 'qa-implementation/daily',
109
+ 'qa-visual/durable', 'qa-visual/daily',
84
110
  'dev/durable', 'dev/daily',
85
111
  'devops/durable', 'devops/daily',
86
112
  ];
@@ -96,6 +122,31 @@ export async function installFramework(config) {
96
122
  generateProviderOverlays(targetDir, FRAMEWORK_SOURCE, llmProvider || 'claude', allProviders);
97
123
  }
98
124
 
125
+ // Bundle the CLI source into the framework dir so chati-router.js can run
126
+ // commands locally without depending on `npx chati-dev` (which fetches from
127
+ // the npm registry — version drift causes inconsistent behavior).
128
+ // Result: every install is self-contained; no network/cache dependency at runtime.
129
+ copyCliSource(
130
+ join(__dirname, '..'), // packages/chati-dev/src/
131
+ join(frameworkDir, '_cli') // .chati.dev/_cli/
132
+ );
133
+
134
+ // The bundled CLI imports ../../framework/hooks/model-governance.js relative
135
+ // to _cli/terminal/prompt-builder.js. Starting at .chati.dev/_cli/terminal/,
136
+ // `../../` resolves to .chati.dev/ — so the hooks copy must land at
137
+ // .chati.dev/framework/hooks/ to satisfy the import path.
138
+ copyDirVerbatim(
139
+ join(__dirname, '..', '..', 'framework', 'hooks'),
140
+ join(frameworkDir, 'framework', 'hooks')
141
+ );
142
+
143
+ // Also bundle production deps so the CLI can import js-yaml/chalk/etc. at runtime
144
+ // without relying on a node_modules tree in the user's project.
145
+ copyCliDependencies(
146
+ join(__dirname, '..', '..'), // packages/chati-dev/
147
+ join(frameworkDir, '_cli', 'node_modules') // .chati.dev/_cli/node_modules/
148
+ );
149
+
99
150
  // Write config.yaml
100
151
  writeFileSync(
101
152
  join(frameworkDir, 'config.yaml'),
@@ -109,8 +160,8 @@ export async function installFramework(config) {
109
160
  const ideProvider = IDE_TO_PROVIDER[ideKey];
110
161
  const isSecondary = ideProvider && ideProvider !== primary && allProviders && allProviders.length > 1;
111
162
  const orchestratorPath = isSecondary
112
- ? `chati.dev/.adapted/${ideProvider}/orchestrator/chati.md`
113
- : 'chati.dev/orchestrator/chati.md';
163
+ ? `.chati.dev/.adapted/${ideProvider}/orchestrator/chati.md`
164
+ : '.chati.dev/orchestrator/chati.md';
114
165
  await configureIDE(targetDir, ideKey, selectedMCPs, { orchestratorPath, isSecondary, providerName: ideProvider });
115
166
  }
116
167
 
@@ -128,11 +179,22 @@ export async function installFramework(config) {
128
179
  for (const file of contextFiles) {
129
180
  const src = join(FRAMEWORK_SOURCE, 'context', file);
130
181
  if (existsSync(src)) {
131
- copyFileSync(src, join(claudeRulesDir, file));
182
+ // Apply path replacement so rules reference .chati.dev/ not chati.dev/
183
+ let content = readFileSync(src, 'utf-8');
184
+ content = applyPathReplacement(content);
185
+ writeFileSync(join(claudeRulesDir, file), content, 'utf-8');
132
186
  }
133
187
  }
134
188
 
135
- writeFileSync(join(targetDir, 'CLAUDE.md'), baseContent, 'utf-8');
189
+ // Preserve existing CLAUDE.md on reinstall — user or magic-docs may have
190
+ // edited it. Section-aware merge if exists; fresh write if not.
191
+ const claudeMdPath = join(targetDir, 'CLAUDE.md');
192
+ if (existsSync(claudeMdPath)) {
193
+ const existingClaudeMd = readFileSync(claudeMdPath, 'utf-8');
194
+ writeFileSync(claudeMdPath, mergeClaudeMd(existingClaudeMd, baseContent), 'utf-8');
195
+ } else {
196
+ writeFileSync(claudeMdPath, baseContent, 'utf-8');
197
+ }
136
198
  writeFileSync(join(targetDir, 'CLAUDE.local.md'), generateClaudeLocalMd(), 'utf-8');
137
199
  }
138
200
 
@@ -171,6 +233,8 @@ const FRAMEWORK_DIRS_TO_COPY = [
171
233
  'context',
172
234
  'tasks',
173
235
  'presets',
236
+ 'scripts',
237
+ 'scaffold',
174
238
  ];
175
239
 
176
240
  /**
@@ -181,8 +245,39 @@ const FRAMEWORK_ROOT_FILES = [
181
245
  ];
182
246
 
183
247
  /**
184
- * Recursively copy a directory from source to destination, applying provider
185
- * adaptation for files listed in ADAPTABLE_FILES when the provider is not Claude.
248
+ * Text file extensions that get path replacement during copy.
249
+ * Binary files (.png, .woff2, etc.) are copied without modification.
250
+ */
251
+ const TEXT_EXTENSIONS = new Set(['.md', '.js', '.yaml', '.yml', '.json', '.html', '.toml', '.txt', '.rules']);
252
+
253
+ /**
254
+ * Apply installed-path replacement to file content.
255
+ *
256
+ * Source uses `chati.dev/` everywhere. Installed project uses:
257
+ * - `.chati.dev/` for framework files (hidden)
258
+ * - `artifacts/` at project root for user-visible output
259
+ *
260
+ * Order matters: replace `chati.dev/artifacts/` FIRST (→ `artifacts/`),
261
+ * then `chati.dev/` (→ `.chati.dev/`). Reversing would produce `.chati.dev/artifacts/`.
262
+ */
263
+ function applyPathReplacement(content) {
264
+ return content
265
+ // Step 1: artifacts at root (must be before generic replace)
266
+ .replace(/chati\.dev\/artifacts\//g, 'artifacts/')
267
+ // Step 2: path strings with slash — but NOT if already prefixed with dot
268
+ // (negative lookbehind for any char that would create `.chati.dev` already)
269
+ .replace(/(?<!\.)chati\.dev\//g, '.chati.dev/')
270
+ // Step 3: quoted dir name in JS join() calls (WITHOUT slash)
271
+ // Only match 'chati.dev' (the DIRECTORY, with dot) not 'chati-dev' (the NPM PACKAGE, with dash)
272
+ // Also: avoid double-prefixing if already '.chati.dev' in source
273
+ .replace(/(?<!\.)'chati\.dev'/g, "'.chati.dev'")
274
+ .replace(/(?<!\.)"chati\.dev"/g, '".chati.dev"');
275
+ }
276
+
277
+ /**
278
+ * Recursively copy a directory from source to destination, applying:
279
+ * 1. Path replacement (chati.dev/ → .chati.dev/, artifacts/ at root)
280
+ * 2. Provider adaptation for non-Claude providers
186
281
  */
187
282
  function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
188
283
  if (!existsSync(srcDir)) return;
@@ -195,14 +290,24 @@ function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
195
290
  const relPath = relBase ? join(relBase, entry.name) : entry.name;
196
291
 
197
292
  if (entry.isDirectory()) {
293
+ // Skip artifacts/ — those live at project root, not inside .chati.dev/
294
+ if (entry.name === 'artifacts') continue;
198
295
  copyDirRecursive(srcPath, destPath, provider, relPath);
199
296
  } else if (entry.isFile()) {
200
- if (provider !== 'claude' && ADAPTABLE_FILES.has(relPath)) {
201
- // Non-Claude provider: read, adapt, write
202
- const content = readFileSync(srcPath, 'utf-8');
203
- writeFileSync(destPath, adaptFrameworkFile(content, relPath, provider), 'utf-8');
297
+ const ext = entry.name.includes('.') ? '.' + entry.name.split('.').pop() : '';
298
+ const isText = TEXT_EXTENSIONS.has(ext);
299
+
300
+ if (isText) {
301
+ let content = readFileSync(srcPath, 'utf-8');
302
+ // Always apply path replacement for installed location
303
+ content = applyPathReplacement(content);
304
+ // Provider adaptation on top (for non-Claude)
305
+ if (provider !== 'claude' && ADAPTABLE_FILES.has(relPath)) {
306
+ content = adaptFrameworkFile(content, relPath, provider);
307
+ }
308
+ writeFileSync(destPath, content, 'utf-8');
204
309
  } else {
205
- // Claude or non-adaptable: direct copy
310
+ // Binary file: direct copy
206
311
  copyFileSync(srcPath, destPath);
207
312
  }
208
313
  }
@@ -221,18 +326,18 @@ function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
221
326
  export function copyFrameworkFiles(destDir, provider = 'claude') {
222
327
  if (!existsSync(FRAMEWORK_SOURCE)) return;
223
328
 
224
- // Copy loose root files
329
+ // Copy loose root files (with path replacement)
225
330
  for (const file of FRAMEWORK_ROOT_FILES) {
226
331
  const src = join(FRAMEWORK_SOURCE, file);
227
332
  if (!existsSync(src)) continue;
228
333
  const dest = join(destDir, file);
229
334
  createDir(dirname(dest));
335
+ let content = readFileSync(src, 'utf-8');
336
+ content = applyPathReplacement(content);
230
337
  if (provider !== 'claude' && ADAPTABLE_FILES.has(file)) {
231
- const content = readFileSync(src, 'utf-8');
232
- writeFileSync(dest, adaptFrameworkFile(content, file, provider), 'utf-8');
233
- } else {
234
- copyFileSync(src, dest);
338
+ content = adaptFrameworkFile(content, file, provider);
235
339
  }
340
+ writeFileSync(dest, content, 'utf-8');
236
341
  }
237
342
 
238
343
  // Copy each framework directory recursively
@@ -256,40 +361,47 @@ async function configureIDE(targetDir, ideKey, selectedMCPs, options = {}) {
256
361
 
257
362
  if (ideKey === 'claude-code') {
258
363
  // Thin router
259
- const routerContent = `# /chati — Thin Router
260
-
261
- ## CRITICAL — Language Override
364
+ const routerContent = `# /chati — Deterministic Router
262
365
 
263
- Read \`.chati/session.yaml\` field \`language\` BEFORE anything else.
264
- ALL responses MUST be in this language. This overrides any global IDE language setting.
366
+ ## STEP 0 Run the deterministic pipeline router (MUST be first)
265
367
 
266
- | Value | Language |
267
- |-------|----------|
268
- | \`en\` | English |
269
- | \`pt\` | Portugues |
270
- | \`es\` | Espanol |
271
- | \`fr\` | Francais |
368
+ Run via Bash:
369
+ \`\`\`
370
+ node .chati.dev/orchestrator/chati-router.js
371
+ \`\`\`
272
372
 
273
- If session.yaml does not exist or has no language field, default to English.
373
+ Parse the JSON output. This single command already handled: license validation, integrity check, session state, pipeline routing, and team registration (if needed). You do NOT need to run any of these separately.
274
374
 
275
- ---
375
+ ### If \`ok\` is false:
276
376
 
277
- ## Load
377
+ | \`error\` value | What to tell the user |
378
+ |---|---|
379
+ | \`license_invalid\` | Display \`license.reason\` as a friendly message. STOP. |
380
+ | \`license_guard_missing\` | "License enforcement is missing. Reinstall with: npx chati-dev init". STOP. |
381
+ | \`integrity_failed\` | "License enforcement was tampered with. Reinstall with: npx chati-dev init". STOP. |
382
+ | anything else | Display error. STOP. |
278
383
 
279
- Read and execute the full orchestrator at \`${orchestratorPath || 'chati.dev/orchestrator/chati.md'}\`.
384
+ ### If \`ok\` is true:
280
385
 
281
- Pass through all context: session state, handoffs, artifacts, and user input.
386
+ 1. Use \`session.language\` for ALL responses (pt = Português, en = English, es = Español, fr = Français).
387
+ 2. Read the full orchestrator at \`${orchestratorPath || '.chati.dev/orchestrator/chati.md'}\`.
388
+ 3. The \`action\` field tells you what to do. Go to the matching "Action:" section in the orchestrator.
389
+ 4. **If \`action\` is \`spawn_team\`**: the team is ALREADY registered (Step 1 already ran as code). The \`team\` object contains \`team_id\`, \`task_list_path\`, \`mailbox_path\`, \`members\`. Go directly to "Action: Spawn Team" Step 2 and use these values in the agent prompts. DO NOT run \`orchestrate spawn-team\` again — it already ran.
390
+ 5. **If \`action\` is \`setup\`**: this is a new project. Follow "Action: Setup".
391
+ 6. For any other action: follow the matching section in the orchestrator.
282
392
 
283
- **Context to pass:**
284
- - \`.chati/session.yaml\` (session state — includes language)
285
- - \`CLAUDE.local.md\` (runtime state — session lock, current agent)
286
- - \`chati.dev/artifacts/handoffs/\` (latest handoff)
287
- - \`chati.dev/config.yaml\` (version info)
393
+ **The \`pipeline\` object contains the full JSON from \`orchestrate next\`.** Use its fields (agent_file, spawn_command, handoff_status, etc.) as documented in the orchestrator.
288
394
 
289
395
  **User input:** $ARGUMENTS
290
396
  `;
291
397
  writeFileSync(join(targetDir, '.claude', 'commands', 'chati.md'), routerContent, 'utf-8');
292
398
 
399
+ // .claude/settings.json — wires the 10 chati.dev hooks into Claude Code's
400
+ // hook system and applies a baseline permission policy. THIS IS THE FILE
401
+ // CLAUDE CODE READS — without it, every hook in chati.dev/hooks/ is dormant.
402
+ // (Pre-v4.2.2 bug: this file was never written, hooks never ran.)
403
+ writeClaudeSettingsWithMerge(join(targetDir, '.claude', 'settings.json'));
404
+
293
405
  // MCP config
294
406
  if (selectedMCPs.length > 0) {
295
407
  const mcpConfig = generateClaudeMCPConfig(selectedMCPs);
@@ -307,10 +419,35 @@ Pass through all context: session state, handoffs, artifacts, and user input.
307
419
  // Session lock override file (equivalent to CLAUDE.local.md)
308
420
  writeFileSync(join(targetDir, 'AGENTS.override.md'), generateAgentsOverrideMd(), 'utf-8');
309
421
 
310
- // Starlark execution policies (equivalent to Claude hooks for constitution guard + read protection)
422
+ // Starlark execution policies (Codex sandboxed rule engine)
311
423
  createDir(join(targetDir, '.codex', 'rules'));
312
424
  writeFileSync(join(targetDir, '.codex', 'rules', 'constitution-guard.rules'), generateCodexConstitutionGuardRules(), 'utf-8');
313
425
  writeFileSync(join(targetDir, '.codex', 'rules', 'read-protection.rules'), generateCodexReadProtectionRules(), 'utf-8');
426
+
427
+ // .codex/hooks.json — license-guard wired to UserPromptSubmit (per-turn enforcement).
428
+ // Codex hooks.json is experimental (requires [features] codex_hooks = true in
429
+ // codex config). Without it, license enforcement only happens at slash command
430
+ // entry — a long-running terminal session would not be re-validated.
431
+ writeFileSync(
432
+ join(targetDir, '.codex', 'hooks.json'),
433
+ JSON.stringify({
434
+ $comment: 'chati.dev v4.2.2 — experimental Codex hooks. Requires [features] codex_hooks = true.',
435
+ hooks: {
436
+ UserPromptSubmit: [
437
+ {
438
+ matcher: '.*',
439
+ hooks: [
440
+ {
441
+ type: 'command',
442
+ command: 'node .chati.dev/hooks/license-guard.js',
443
+ },
444
+ ],
445
+ },
446
+ ],
447
+ },
448
+ }, null, 2) + '\n',
449
+ 'utf-8'
450
+ );
314
451
  } else if (ideKey === 'gemini-cli') {
315
452
  // Gemini CLI: TOML command file (native format for /chati command)
316
453
  writeFileSync(join(targetDir, '.gemini', 'commands', 'chati.toml'), generateGeminiRouter({ orchestratorPath }), 'utf-8');
@@ -357,32 +494,31 @@ function generateProviderInstructions(providerName) {
357
494
  # This file configures ${providerName} to work with Chati.dev
358
495
 
359
496
  ## System Location
360
- All system content is in the \`chati.dev/\` directory.
497
+ All system content is in the \`.chati.dev/\` directory (hidden). User artifacts are in \`artifacts/\`.
361
498
 
362
499
  ## Session State
363
500
  Runtime session state is in \`.chati/session.yaml\` (IDE-agnostic).
364
501
 
365
502
  ## Getting Started
366
- The orchestrator is at \`chati.dev/orchestrator/chati.md\`.
367
- Read it to understand routing, session management, and agent activation.
503
+ The orchestrator is at \`.chati.dev/orchestrator/chati.md\`.
368
504
 
369
505
  ## Constitution
370
- Governance rules are in \`chati.dev/constitution.md\` (19 Articles).
506
+ Governance rules are in \`.chati.dev/constitution.md\`.
371
507
 
372
508
  ## Pipeline
373
509
  \`\`\`
374
510
  DISCOVER: WU -> Brief
375
- PLAN: Detail -> Architect -> UX -> Phases -> Tasks -> QA-Planning
376
- BUILD: Dev -> QA-Implementation
511
+ PLAN: [Planning Team: Detail + Architect + UX + QA-Planning] -> Phases -> Tasks
512
+ BUILD: [Build Team: Dev + QA-Implementation] -> QA-Visual
377
513
  DEPLOY: DevOps
378
514
  \`\`\`
379
515
 
380
516
  ## Agents
381
- - DISCOVER: chati.dev/agents/discover/ (3 agents)
382
- - PLAN: chati.dev/agents/plan/ (5 agents)
383
- - Quality: chati.dev/agents/quality/ (2 agents)
384
- - BUILD: chati.dev/agents/build/ (1 agent)
385
- - DEPLOY: chati.dev/agents/deploy/ (1 agent)
517
+ - DISCOVER: .chati.dev/agents/discover/
518
+ - PLAN: .chati.dev/agents/plan/
519
+ - Quality: .chati.dev/agents/quality/
520
+ - BUILD: .chati.dev/agents/build/
521
+ - DEPLOY: .chati.dev/agents/deploy/
386
522
  `;
387
523
  }
388
524
 
@@ -390,36 +526,165 @@ DEPLOY: DevOps
390
526
  * Append chati.dev runtime entries to .gitignore.
391
527
  * Session lock files are runtime-only and should never be committed.
392
528
  */
393
- function updateGitignore(targetDir, selectedIDEs) {
394
- const entries = [
529
+ export function updateGitignore(targetDir, selectedIDEs) {
530
+ const START_MARKER = '# BEGIN Chati.dev — do not edit between markers';
531
+ const END_MARKER = '# END Chati.dev';
532
+ // Legacy marker from pre-4.3.0 installs — detected for migration
533
+ const LEGACY_MARKER = '# Chati.dev runtime files';
534
+
535
+ const block = [
536
+ START_MARKER,
537
+ '# Framework files — generated by chati-dev, not part of your project.',
538
+ '# Do NOT remove these entries. If a framework file is already in git history,',
539
+ '# run: git rm -r --cached <path> after upgrading.',
540
+ '',
541
+ '# Framework (all code, prompts, hooks, orchestrator, data)',
542
+ '.chati.dev/',
395
543
  '',
396
- '# Chati.dev runtime files (session lock not committed)',
544
+ '# Runtime state (session, teams, timeline, transient memories)',
545
+ '.chati/session.yaml',
546
+ '.chati/teams/',
547
+ '.chati/timeline.json',
397
548
  '.chati/memories/*/session/',
549
+ '# NOTE: durable/ and daily/ memories ARE committed — they contain project knowledge',
550
+ '',
551
+ '# Claude Code integration (framework portions only)',
552
+ '.claude/commands/chati.md',
553
+ '.claude/rules/chati/',
554
+ '.claude/settings.local.json',
555
+ '# NOTE: .claude/settings.json IS committed — it is shared team config (hooks, MCPs)',
556
+ '',
557
+ '# Session locks (runtime, per-machine)',
558
+ 'CLAUDE.local.md',
559
+ '.gemini/session-lock.md',
560
+ 'AGENTS.override.md',
398
561
  ];
399
562
 
400
- if (selectedIDEs.includes('claude-code')) {
401
- entries.push('CLAUDE.local.md');
402
- }
403
563
  if (selectedIDEs.includes('gemini-cli')) {
404
- entries.push('.gemini/session-lock.md');
564
+ block.push(
565
+ '',
566
+ '# Gemini provider framework files',
567
+ '.gemini/commands/',
568
+ '.gemini/context/',
569
+ '.gemini/hooks/',
570
+ '.gemini/settings.json',
571
+ );
405
572
  }
573
+
406
574
  if (selectedIDEs.includes('codex-cli')) {
407
- entries.push('AGENTS.override.md');
575
+ block.push(
576
+ '',
577
+ '# Codex provider framework files',
578
+ '.codex/',
579
+ '.agents/skills/chati/',
580
+ );
408
581
  }
409
582
 
410
- entries.push('');
583
+ block.push('', END_MARKER);
411
584
 
585
+ const newBlock = block.join('\n');
412
586
  const gitignorePath = join(targetDir, '.gitignore');
413
- const marker = '# Chati.dev runtime files';
414
587
 
415
- if (existsSync(gitignorePath)) {
416
- const existing = readFileSync(gitignorePath, 'utf-8');
417
- // Don't duplicate if already present
418
- if (existing.includes(marker)) return;
419
- writeFileSync(gitignorePath, existing.trimEnd() + '\n' + entries.join('\n'), 'utf-8');
420
- } else {
421
- writeFileSync(gitignorePath, entries.join('\n'), 'utf-8');
588
+ if (!existsSync(gitignorePath)) {
589
+ writeFileSync(gitignorePath, newBlock + '\n', 'utf-8');
590
+ return;
591
+ }
592
+
593
+ let existing = readFileSync(gitignorePath, 'utf-8');
594
+
595
+ // Strip new-style block if present (idempotency)
596
+ const newStartIdx = existing.indexOf(START_MARKER);
597
+ const newEndIdx = existing.indexOf(END_MARKER);
598
+ if (newStartIdx !== -1 && newEndIdx !== -1) {
599
+ existing = existing.slice(0, newStartIdx) + existing.slice(newEndIdx + END_MARKER.length);
600
+ existing = existing.replace(/\n{3,}/g, '\n\n').trimEnd();
601
+ }
602
+
603
+ // Strip legacy block if present (pre-4.3.0)
604
+ if (existing.includes(LEGACY_MARKER)) {
605
+ const lines = existing.split('\n');
606
+ const legacyIdx = lines.findIndex(l => l.includes(LEGACY_MARKER));
607
+ if (legacyIdx !== -1) {
608
+ // Remove legacy marker line + the short known-legacy entries that followed
609
+ const legacyEntries = new Set([
610
+ '.chati/memories/*/session/',
611
+ 'CLAUDE.local.md',
612
+ '.gemini/session-lock.md',
613
+ 'AGENTS.override.md',
614
+ '',
615
+ ]);
616
+ let endIdx = legacyIdx + 1;
617
+ while (endIdx < lines.length && legacyEntries.has(lines[endIdx].trim())) endIdx++;
618
+ lines.splice(legacyIdx, endIdx - legacyIdx);
619
+ existing = lines.join('\n').trimEnd();
620
+ }
621
+ }
622
+
623
+ writeFileSync(gitignorePath, existing.trimEnd() + '\n\n' + newBlock + '\n', 'utf-8');
624
+ }
625
+
626
+ /**
627
+ * Section-aware merge for CLAUDE.md: preserves user-authored sections and
628
+ * framework-managed sections (those with markers). Framework sections are
629
+ * re-rendered from the fresh template; everything else is kept as-is.
630
+ */
631
+ function mergeClaudeMd(existing, fresh) {
632
+ // Framework-managed sections — these come from the install template AND from
633
+ // magic-docs runtime updates. Replace these from fresh template; preserve all others.
634
+ const frameworkSections = [
635
+ // Install template (templates.js generateClaudeMd)
636
+ '## Project Context',
637
+ '## Chati.dev',
638
+ // Magic-docs runtime sections (when project starts)
639
+ '## Current State',
640
+ '## Session Lock',
641
+ '## Recent Decisions',
642
+ '## Framework',
643
+ '## How to Resume',
644
+ ];
645
+
646
+ // If existing has no framework sections at all, user has a custom CLAUDE.md;
647
+ // just append the framework template separator + fresh template below.
648
+ const hasAnyFwSection = frameworkSections.some(h => existing.includes(h));
649
+ if (!hasAnyFwSection) {
650
+ return existing.trimEnd() + '\n\n<!-- Chati.dev framework context (auto-managed) -->\n\n' + fresh;
651
+ }
652
+
653
+ // Otherwise, do section-level replacement — for each framework section in fresh,
654
+ // replace the corresponding section in existing (or append if missing).
655
+ let merged = existing;
656
+ for (const header of frameworkSections) {
657
+ const freshSection = extractSection(fresh, header);
658
+ if (!freshSection) continue;
659
+ if (merged.includes(header)) {
660
+ merged = replaceSection(merged, header, freshSection);
661
+ } else {
662
+ merged = merged.trimEnd() + '\n\n' + freshSection;
663
+ }
422
664
  }
665
+ return merged;
666
+ }
667
+
668
+ function extractSection(content, header) {
669
+ const lines = content.split('\n');
670
+ const startIdx = lines.findIndex(l => l.trim() === header);
671
+ if (startIdx === -1) return null;
672
+ let endIdx = startIdx + 1;
673
+ while (endIdx < lines.length && !lines[endIdx].match(/^##\s/)) endIdx++;
674
+ return lines.slice(startIdx, endIdx).join('\n');
675
+ }
676
+
677
+ function replaceSection(content, header, newSection) {
678
+ const lines = content.split('\n');
679
+ const startIdx = lines.findIndex(l => l.trim() === header);
680
+ if (startIdx === -1) return content;
681
+ let endIdx = startIdx + 1;
682
+ while (endIdx < lines.length && !lines[endIdx].match(/^##\s/)) endIdx++;
683
+ return [
684
+ ...lines.slice(0, startIdx),
685
+ newSection,
686
+ ...lines.slice(endIdx),
687
+ ].join('\n');
423
688
  }
424
689
 
425
690
  /**
@@ -430,3 +695,214 @@ function createDir(dir) {
430
695
  mkdirSync(dir, { recursive: true });
431
696
  }
432
697
  }
698
+
699
+ /**
700
+ * Recursively copy CLI source (excluding tests, node_modules, hidden files).
701
+ * Used to bundle the CLI source into the user's .chati.dev/_cli/ folder so
702
+ * the framework runs self-contained (no `npx chati-dev` dependency).
703
+ */
704
+ function copyCliSource(srcDir, destDir) {
705
+ if (!existsSync(srcDir)) return;
706
+ createDir(destDir);
707
+ const entries = readdirSync(srcDir);
708
+ for (const entry of entries) {
709
+ if (entry.startsWith('.')) continue;
710
+ if (entry === 'node_modules' || entry === 'test' || entry === 'tests') continue;
711
+ const srcPath = join(srcDir, entry);
712
+ const destPath = join(destDir, entry);
713
+ let stat;
714
+ try { stat = statSync(srcPath); } catch { continue; }
715
+ if (stat.isDirectory()) {
716
+ copyCliSource(srcPath, destPath);
717
+ } else {
718
+ copyFileSync(srcPath, destPath);
719
+ }
720
+ }
721
+ }
722
+
723
+ /**
724
+ * Recursively copy a directory verbatim (used for bundling node_modules trees).
725
+ */
726
+ function copyDirVerbatim(srcDir, destDir) {
727
+ if (!existsSync(srcDir)) return;
728
+ createDir(destDir);
729
+ for (const entry of readdirSync(srcDir)) {
730
+ const srcPath = join(srcDir, entry);
731
+ const destPath = join(destDir, entry);
732
+ let stat;
733
+ try { stat = statSync(srcPath); } catch { continue; }
734
+ if (stat.isDirectory()) {
735
+ copyDirVerbatim(srcPath, destPath);
736
+ } else {
737
+ try { copyFileSync(srcPath, destPath); } catch { /* skip unreadable */ }
738
+ }
739
+ }
740
+ }
741
+
742
+ /**
743
+ * Bundle chati-dev's production dependencies into .chati.dev/_cli/node_modules/
744
+ * so the bundled CLI can import js-yaml, chalk, etc. without relying on a
745
+ * project-level node_modules tree.
746
+ *
747
+ * Uses createRequire to resolve each dep from the installed chati-dev package,
748
+ * which works whether the package is hoisted or nested in the user's
749
+ * node_modules (npx cache, npm install, etc.).
750
+ */
751
+ function copyCliDependencies(pkgDir, destNodeModules) {
752
+ let pkg;
753
+ try {
754
+ pkg = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf-8'));
755
+ } catch { return; }
756
+ const deps = [
757
+ ...Object.keys(pkg.dependencies || {}),
758
+ ...Object.keys(pkg.optionalDependencies || {}),
759
+ ];
760
+ if (deps.length === 0) return;
761
+
762
+ // Resolve each dep from the package.json location (works for hoisted + nested layouts).
763
+ // eslint-disable-next-line no-undef
764
+ const require = createRequire(join(pkgDir, 'package.json'));
765
+ createDir(destNodeModules);
766
+
767
+ const visited = new Set();
768
+ const queue = [...deps];
769
+
770
+ while (queue.length > 0) {
771
+ const depName = queue.shift();
772
+ if (visited.has(depName)) continue;
773
+ visited.add(depName);
774
+
775
+ let depDir;
776
+ try {
777
+ // Try direct package.json resolve first (fast path)
778
+ depDir = dirname(require.resolve(`${depName}/package.json`));
779
+ } catch {
780
+ // Fallback: resolve the main entry then walk up to find package root.
781
+ // Needed for packages with strict "exports" (chalk, ora, etc.).
782
+ try {
783
+ let p = require.resolve(depName);
784
+ while (p && p !== dirname(p)) {
785
+ p = dirname(p);
786
+ if (existsSync(join(p, 'package.json')) && basename(p) === depName.split('/').pop()) {
787
+ depDir = p;
788
+ break;
789
+ }
790
+ }
791
+ } catch { continue; }
792
+ if (!depDir) continue;
793
+ }
794
+ const depPkgJsonPath = join(depDir, 'package.json');
795
+ copyDirVerbatim(depDir, join(destNodeModules, depName));
796
+
797
+ // Include transitive deps
798
+ try {
799
+ const depPkg = JSON.parse(readFileSync(depPkgJsonPath, 'utf-8'));
800
+ for (const t of Object.keys(depPkg.dependencies || {})) {
801
+ if (!visited.has(t)) queue.push(t);
802
+ }
803
+ } catch { /* continue */ }
804
+ }
805
+ }
806
+
807
+ /**
808
+ * Write .claude/settings.json with safe merge against existing user content.
809
+ *
810
+ * If the file does not exist: write fresh chati settings.
811
+ * If the file exists and is valid JSON: merge — chati hooks/permissions are
812
+ * UNIONED into existing values rather than replacing them. User customizations
813
+ * (env, statusLine, plugins, language, alwaysThinkingEnabled, etc.) are preserved.
814
+ * If the file exists but is malformed: leave it alone, log a warning. We will
815
+ * not silently destroy user data.
816
+ */
817
+ function writeClaudeSettingsWithMerge(settingsPath) {
818
+ createDir(dirname(settingsPath));
819
+ const fresh = JSON.parse(generateClaudeSettings());
820
+
821
+ if (!existsSync(settingsPath)) {
822
+ writeFileSync(settingsPath, JSON.stringify(fresh, null, 2) + '\n', 'utf-8');
823
+ return;
824
+ }
825
+
826
+ let existing;
827
+ try {
828
+ existing = JSON.parse(readFileSync(settingsPath, 'utf-8'));
829
+ } catch (err) {
830
+ process.stderr.write(`[chati] WARNING: .claude/settings.json exists but is not valid JSON (${err.message}). Skipping merge — chati hooks will NOT be wired. Fix the file and re-run install.\n`);
831
+ return;
832
+ }
833
+
834
+ const merged = mergeClaudeSettings(existing, fresh);
835
+ writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
836
+ }
837
+
838
+ /**
839
+ * Merge two Claude Code settings.json objects.
840
+ *
841
+ * - permissions.allow / permissions.deny: array union (de-duplicated)
842
+ * - permissions.defaultMode: keep existing if set, else use fresh
843
+ * - hooks: per-event union — chati hook commands are appended to existing
844
+ * matcher groups; user's hooks for the same events are preserved
845
+ * - $schema: prefer fresh (always our canonical URL)
846
+ * - all other top-level keys (env, statusLine, plugins, language, etc.):
847
+ * keep existing untouched
848
+ */
849
+ export function mergeClaudeSettings(existing, fresh) {
850
+ const merged = { ...existing };
851
+
852
+ // $schema — chati's canonical URL wins
853
+ if (fresh.$schema) merged.$schema = fresh.$schema;
854
+
855
+ // permissions
856
+ const existingPerms = existing.permissions || {};
857
+ const freshPerms = fresh.permissions || {};
858
+ merged.permissions = {
859
+ ...existingPerms,
860
+ allow: unionArrays(existingPerms.allow, freshPerms.allow),
861
+ deny: unionArrays(existingPerms.deny, freshPerms.deny),
862
+ defaultMode: existingPerms.defaultMode || freshPerms.defaultMode || 'default',
863
+ };
864
+
865
+ // hooks — per-event merge
866
+ const existingHooks = existing.hooks || {};
867
+ const freshHooks = fresh.hooks || {};
868
+ merged.hooks = { ...existingHooks };
869
+
870
+ for (const [eventName, freshGroups] of Object.entries(freshHooks)) {
871
+ const existingGroups = existingHooks[eventName] || [];
872
+ // Append fresh groups; we don't dedup at the group level (different
873
+ // matchers are different groups). Within a single fresh group, the
874
+ // hook commands are unique to chati so duplication risk is minimal.
875
+ // To be safe, dedup by command string across the merged event.
876
+ const seen = new Set();
877
+ const collected = [];
878
+ for (const group of [...existingGroups, ...freshGroups]) {
879
+ const dedupedHooks = (group.hooks || []).filter(h => {
880
+ const key = `${group.matcher || ''}|${h.command}`;
881
+ if (seen.has(key)) return false;
882
+ seen.add(key);
883
+ return true;
884
+ });
885
+ if (dedupedHooks.length > 0) {
886
+ collected.push({
887
+ ...(group.matcher !== undefined ? { matcher: group.matcher } : {}),
888
+ hooks: dedupedHooks,
889
+ });
890
+ }
891
+ }
892
+ merged.hooks[eventName] = collected;
893
+ }
894
+
895
+ return merged;
896
+ }
897
+
898
+ function unionArrays(a, b) {
899
+ const out = [];
900
+ const seen = new Set();
901
+ for (const item of [...(a || []), ...(b || [])]) {
902
+ if (!seen.has(item)) {
903
+ seen.add(item);
904
+ out.push(item);
905
+ }
906
+ }
907
+ return out;
908
+ }