chati-dev 4.2.2 → 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 (220) 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 +7 -6
  23. package/framework/constitution.md +127 -44
  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 +24 -5
  35. package/framework/hooks/license-guard.js +62 -27
  36. package/framework/hooks/mode-governance.js +13 -2
  37. package/framework/hooks/model-governance.js +1 -0
  38. package/framework/hooks/post-dev.js +207 -0
  39. package/framework/hooks/prism-engine.js +274 -105
  40. package/framework/hooks/reasoning-escalator.js +371 -0
  41. package/framework/hooks/reference-trigger.js +117 -0
  42. package/framework/hooks/session-digest.js +50 -1
  43. package/framework/hooks/settings.json +32 -1
  44. package/framework/hooks/style-guard.js +25 -6
  45. package/framework/hooks/team-quality-gate.js +19 -12
  46. package/framework/hooks/undercover-guard.js +4 -2
  47. package/framework/i18n/en.yaml +3 -3
  48. package/framework/i18n/es.yaml +3 -3
  49. package/framework/i18n/fr.yaml +3 -3
  50. package/framework/i18n/pt.yaml +3 -3
  51. package/framework/intelligence/context-engine.md +4 -5
  52. package/framework/intelligence/decision-engine.md +1 -1
  53. package/framework/intelligence/hooks-performance.md +3 -3
  54. package/framework/migrations/v1.0-to-v1.1.yaml +1 -1
  55. package/framework/migrations/v1.4-to-v2.0.yaml +11 -11
  56. package/framework/migrations/v4.0-to-v4.1.yaml +2 -2
  57. package/framework/migrations/v4.2-to-v4.3.yaml +29 -0
  58. package/framework/orchestrator/chati-router.js +387 -0
  59. package/framework/orchestrator/chati-update.md +40 -40
  60. package/framework/orchestrator/chati.md +294 -94
  61. package/framework/scaffold/motion-premium/README.md +89 -0
  62. package/framework/scaffold/motion-premium/app/globals.css.template +400 -0
  63. package/framework/scaffold/motion-premium/app/layout.tsx.template +110 -0
  64. package/framework/scaffold/motion-premium/components/animation/BackgroundCrossfadeProvider.tsx.template +170 -0
  65. package/framework/scaffold/motion-premium/components/animation/LenisProvider.tsx.template +49 -0
  66. package/framework/scaffold/motion-premium/components/animation/PageTransitionWrapper.tsx.template +83 -0
  67. package/framework/scaffold/motion-premium/components/animation/Preloader.tsx.template +171 -0
  68. package/framework/scaffold/motion-premium/components/ui/Container.tsx.template +69 -0
  69. package/framework/scaffold/motion-premium/components/ui/PageSection.tsx.template +74 -0
  70. package/framework/scaffold/motion-premium/lib/animations/gsap.ts.template +112 -0
  71. package/framework/scaffold/motion-premium/lib/animations/refreshCoordinator.ts.template +75 -0
  72. package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +119 -0
  73. package/framework/scaffold/motion-premium/lib/animations/useGsapContext.ts.template +92 -0
  74. package/framework/scaffold/motion-premium/lib/animations/useScrollSnapStepper.ts.template +265 -0
  75. package/framework/scaffold/motion-premium/lib/animations/useSmoothScroll.ts.template +67 -0
  76. package/framework/scaffold/motion-premium/lib/brand.ts.template +43 -0
  77. package/framework/scaffold/motion-premium/scaffold.yaml +174 -0
  78. package/framework/scaffold/motion-premium-3d/README.md +80 -0
  79. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoCanvas.tsx.template +81 -0
  80. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/ScrollDemoClient.tsx.template +75 -0
  81. package/framework/scaffold/motion-premium-3d/app/(3d)/scroll-demo/page.tsx.template +26 -0
  82. package/framework/scaffold/motion-premium-3d/components/3d/CameraRig.tsx.template +100 -0
  83. package/framework/scaffold/motion-premium-3d/components/3d/CanvasProvider.tsx.template +85 -0
  84. package/framework/scaffold/motion-premium-3d/components/3d/InvalidateOnScroll.tsx.template +51 -0
  85. package/framework/scaffold/motion-premium-3d/components/3d/MeshCrossfade.tsx.template +79 -0
  86. package/framework/scaffold/motion-premium-3d/components/3d/ScrollCrossfade.tsx.template +88 -0
  87. package/framework/scaffold/motion-premium-3d/components/3d/ScrollScene.tsx.template +121 -0
  88. package/framework/scaffold/motion-premium-3d/components/webgl/SceneFallback.tsx.template +65 -0
  89. package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +68 -0
  90. package/framework/scaffold/motion-premium-3d/lib/webgl/detect.ts.template +69 -0
  91. package/framework/scaffold/motion-premium-3d/scaffold.yaml +133 -0
  92. package/framework/schemas/session.schema.json +109 -21
  93. package/framework/scripts/reference-capture.js +430 -0
  94. package/framework/scripts/visual-qa.js +674 -0
  95. package/framework/tasks/orchestrator-handoff.md +1 -1
  96. package/framework/tasks/orchestrator-resume.md +1 -1
  97. package/framework/tasks/orchestrator-route.md +1 -1
  98. package/framework/tasks/orchestrator-status.md +3 -3
  99. package/framework/tasks/qa-planning-gate-define.md +1 -1
  100. package/framework/templates/brandbook-html-tmpl.md +1 -1
  101. package/framework/templates/brandbook-tmpl.yaml +1 -1
  102. package/framework/templates/component-spec-tmpl.yaml +1 -1
  103. package/framework/templates/design-token-tmpl.yaml +1 -1
  104. package/framework/templates/icon-system-tmpl.yaml +1 -1
  105. package/framework/templates/team-planning-tasks.yaml +6 -5
  106. package/framework/workflows/brownfield-discovery.yaml +2 -2
  107. package/framework/workflows/brownfield-fullstack.yaml +15 -11
  108. package/framework/workflows/brownfield-service.yaml +14 -10
  109. package/framework/workflows/brownfield-ui.yaml +15 -11
  110. package/framework/workflows/greenfield-fullstack.yaml +16 -13
  111. package/framework/workflows/quick-flow.yaml +3 -3
  112. package/framework/workflows/standard-flow.yaml +12 -9
  113. package/package.json +10 -5
  114. package/src/autonomy/autonomous-gate.js +1 -0
  115. package/src/autonomy/build-state.js +1 -2
  116. package/src/autonomy/progress-reporter.js +1 -1
  117. package/src/config/agent-customizer.js +11 -3
  118. package/src/config/claude-settings-generator.js +27 -7
  119. package/src/config/context-file-generator.js +41 -21
  120. package/src/config/framework-adapter.js +1 -0
  121. package/src/config/gemini-hooks-generator.js +19 -7
  122. package/src/config/mcp-configs.js +1 -0
  123. package/src/context/layers/l1-global.js +2 -1
  124. package/src/dashboard/data-reader.js +4 -3
  125. package/src/dashboard/layout.js +2 -1
  126. package/src/decision/analyzer.js +6 -30
  127. package/src/decision/engine.js +4 -28
  128. package/src/decision/registry-healer.js +3 -2
  129. package/src/decision/registry-updater.js +23 -14
  130. package/src/extensions/loader.js +2 -8
  131. package/src/gates/g1-planning-complete.js +2 -1
  132. package/src/gates/g2-qa-planning.js +2 -1
  133. package/src/gates/g3-implementation.js +2 -1
  134. package/src/gates/g4-qa-implementation.js +3 -2
  135. package/src/gates/g5-deploy-ready.js +2 -1
  136. package/src/health/engine.js +4 -3
  137. package/src/installer/core.js +422 -81
  138. package/src/installer/preflight.js +131 -0
  139. package/src/installer/provider-overlay.js +3 -3
  140. package/src/installer/scaffold-applier.js +358 -0
  141. package/src/installer/templates.js +46 -29
  142. package/src/installer/validator.js +17 -12
  143. package/src/intelligence/registry-manager.js +22 -29
  144. package/src/intelligence/timeline.js +11 -6
  145. package/src/license/commands.js +1 -1
  146. package/src/license/wait.js +102 -0
  147. package/src/memory/agent-memory.js +81 -0
  148. package/src/memory/dream.js +32 -1
  149. package/src/merger/replace-merger.js +28 -15
  150. package/src/orchestrator/agent-selector.js +2 -1
  151. package/src/orchestrator/cli.js +1869 -71
  152. package/src/orchestrator/doctor.js +270 -0
  153. package/src/orchestrator/handoff-engine.js +4 -3
  154. package/src/orchestrator/index.js +2 -0
  155. package/src/orchestrator/pipeline-manager.js +306 -15
  156. package/src/orchestrator/session-manager.js +331 -6
  157. package/src/tasks/handoff.js +3 -2
  158. package/src/telemetry/config.js +4 -3
  159. package/src/telemetry/schema.js +1 -0
  160. package/src/terminal/collector.js +3 -2
  161. package/src/terminal/index.js +1 -2
  162. package/src/terminal/isolation.js +52 -18
  163. package/src/terminal/prompt-builder.js +42 -25
  164. package/src/terminal/run-parallel.js +1 -1
  165. package/src/terminal/run-team.js +3 -3
  166. package/src/terminal/team-task-list.js +43 -4
  167. package/src/upgrade/backup.js +3 -2
  168. package/src/upgrade/checker.js +3 -2
  169. package/src/upgrade/migrator.js +65 -7
  170. package/src/upgrade/tracked-files-detector.js +86 -0
  171. package/src/upgrade/user-messages.js +94 -0
  172. package/src/utils/config-parser.js +2 -1
  173. package/src/utils/feature-flags.js +2 -1
  174. package/src/utils/flatten-entities.js +69 -0
  175. package/src/utils/framework-dir.js +16 -0
  176. package/src/utils/model-id.js +85 -0
  177. package/src/utils/provider-limits.js +84 -23
  178. package/src/utils/schema-validator.js +1 -1
  179. package/src/wizard/i18n.js +5 -4
  180. package/src/wizard/index.js +14 -0
  181. package/assets/logo - c/303/263pia.png +0 -0
  182. package/assets/logo.svg +0 -42
  183. package/assets/logo2.png +0 -0
  184. package/assets/social-preview.png +0 -0
  185. package/scripts/bundle-framework.js +0 -69
  186. package/scripts/changelog-generator.js +0 -222
  187. package/scripts/codebase-mapper.js +0 -728
  188. package/scripts/commit-message-generator.js +0 -167
  189. package/scripts/coverage-analyzer.js +0 -260
  190. package/scripts/dependency-analyzer.js +0 -280
  191. package/scripts/doctor/checks/agents.js +0 -77
  192. package/scripts/doctor/checks/constitution.js +0 -41
  193. package/scripts/doctor/checks/domain-alignment.js +0 -58
  194. package/scripts/doctor/checks/prism-layers.js +0 -84
  195. package/scripts/doctor/checks/registry.js +0 -55
  196. package/scripts/doctor/checks/schemas.js +0 -61
  197. package/scripts/doctor/fixes/reference-fix.js +0 -100
  198. package/scripts/doctor/fixes/registry-fix.js +0 -56
  199. package/scripts/doctor/index.js +0 -212
  200. package/scripts/framework-analyzer.js +0 -308
  201. package/scripts/generate-constitution-domain.js +0 -253
  202. package/scripts/generate-signing-key.js +0 -33
  203. package/scripts/health-check.js +0 -481
  204. package/scripts/ide-sync.js +0 -326
  205. package/scripts/performance-analyzer.js +0 -325
  206. package/scripts/plan-tracker.js +0 -278
  207. package/scripts/populate-entity-registry.js +0 -481
  208. package/scripts/pr-review.js +0 -317
  209. package/scripts/rollback-manager.js +0 -310
  210. package/scripts/semantic-lint.js +0 -328
  211. package/scripts/sign-manifest.js +0 -53
  212. package/scripts/stuck-detector.js +0 -343
  213. package/scripts/test-quality-assessment.js +0 -257
  214. package/scripts/validate-agents.js +0 -368
  215. package/scripts/validate-package.js +0 -505
  216. package/scripts/validate-tasks.js +0 -465
  217. package/src/autonomy/worktree-manager.js +0 -250
  218. package/src/intelligence/decision-engine.js +0 -256
  219. package/src/intelligence/document-sharder.js +0 -221
  220. package/src/intelligence/elicitation.js +0 -265
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { existsSync, readFileSync, readdirSync } from 'fs';
12
12
  import { join } from 'path';
13
+ import { resolveFrameworkDir } from '../utils/framework-dir.js';
13
14
  import { runPrism } from '../context/engine.js';
14
15
  import { loadHandoff, formatHandoff } from '../tasks/handoff.js';
15
16
  import { getWriteScope } from './isolation.js';
@@ -24,23 +25,41 @@ import { PROVIDER_LIMITS as PROVIDER_TOKEN_LIMITS } from '../utils/provider-limi
24
25
  import { AGENT_MODELS } from '../../framework/hooks/model-governance.js';
25
26
 
26
27
  /**
27
- * Map of agent names to their definition file paths (relative to project root).
28
+ * Agent definition file paths relative to framework directory.
29
+ * Used internally — callers should use getAgentFile(agent, projectDir).
30
+ * @private
28
31
  */
29
- export const AGENT_FILE_MAP = {
30
- 'greenfield-wu': 'chati.dev/agents/discover/greenfield-wu.md',
31
- 'brownfield-wu': 'chati.dev/agents/discover/brownfield-wu.md',
32
- brief: 'chati.dev/agents/discover/brief.md',
33
- detail: 'chati.dev/agents/plan/detail.md',
34
- architect: 'chati.dev/agents/plan/architect.md',
35
- ux: 'chati.dev/agents/plan/ux.md',
36
- phases: 'chati.dev/agents/plan/phases.md',
37
- tasks: 'chati.dev/agents/plan/tasks.md',
38
- 'qa-planning': 'chati.dev/agents/quality/qa-planning.md',
39
- dev: 'chati.dev/agents/build/dev.md',
40
- 'qa-implementation': 'chati.dev/agents/quality/qa-implementation.md',
41
- devops: 'chati.dev/agents/deploy/devops.md',
32
+ const AGENT_FILE_PATHS = {
33
+ 'greenfield-wu': 'agents/discover/greenfield-wu.md',
34
+ 'brownfield-wu': 'agents/discover/brownfield-wu.md',
35
+ brief: 'agents/discover/brief.md',
36
+ detail: 'agents/plan/detail.md',
37
+ architect: 'agents/plan/architect.md',
38
+ ux: 'agents/plan/ux.md',
39
+ phases: 'agents/plan/phases.md',
40
+ tasks: 'agents/plan/tasks.md',
41
+ 'qa-planning': 'agents/quality/qa-planning.md',
42
+ dev: 'agents/build/dev.md',
43
+ 'qa-implementation': 'agents/quality/qa-implementation.md',
44
+ 'qa-visual': 'agents/quality/qa-visual.md',
45
+ devops: 'agents/deploy/devops.md',
42
46
  };
43
47
 
48
+ /**
49
+ * Resolve an agent's definition file path relative to the project root.
50
+ * Handles both .chati.dev/ (new) and chati.dev/ (legacy) structures.
51
+ *
52
+ * @param {string} agentName - Agent name (e.g. 'detail')
53
+ * @param {string} projectDir - Project root directory
54
+ * @returns {string|null} Relative path from project root, or null if unknown
55
+ */
56
+ export function getAgentFile(agentName, projectDir) {
57
+ const rel = AGENT_FILE_PATHS[agentName];
58
+ if (!rel) return null;
59
+ return `${resolveFrameworkDir(projectDir)}/${rel}`;
60
+ }
61
+
62
+
44
63
  /**
45
64
  * 3-Tier Tool Mesh — Token-aware tool loading profiles per agent.
46
65
  *
@@ -63,6 +82,7 @@ export const TOOL_PROFILES = {
63
82
  'qa-planning': { T1: ['Read', 'Glob', 'Grep'], T2: [], T3: [] },
64
83
  dev: { T1: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep'], T2: ['WebSearch', 'WebFetch', 'Task'], T3: ['playwright'] },
65
84
  'qa-implementation': { T1: ['Read', 'Bash', 'Glob', 'Grep'], T2: ['Task'], T3: [] },
85
+ 'qa-visual': { T1: ['Read', 'Bash', 'Glob', 'Grep'], T2: ['Write'], T3: [] },
66
86
  devops: { T1: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep'], T2: ['WebSearch'], T3: ['docker'] },
67
87
  };
68
88
 
@@ -220,7 +240,7 @@ export function buildAgentPrompt(config) {
220
240
  * Spawned terminals always start FRESH (remainingPercent: 100).
221
241
  */
222
242
  function buildPrismSection(config) {
223
- const domainsDir = join(config.projectDir, 'chati.dev', 'domains');
243
+ const domainsDir = join(config.projectDir, resolveFrameworkDir(config.projectDir), 'domains');
224
244
 
225
245
  if (!existsSync(domainsDir)) {
226
246
  return { xml: null, layerCount: 0 };
@@ -262,17 +282,14 @@ function buildPrismSection(config) {
262
282
  * For non-primary providers, resolves via overlay directory.
263
283
  */
264
284
  function loadAgentDefinition(agent, projectDir, provider = null) {
265
- const relativePath = AGENT_FILE_MAP[agent];
266
- if (!relativePath) return null;
267
-
268
- // Strip 'chati.dev/' prefix for overlay resolution
269
- const relToFramework = relativePath.replace(/^chati\.dev\//, '');
285
+ const relToFramework = AGENT_FILE_PATHS[agent];
286
+ if (!relToFramework) return null;
270
287
 
271
288
  let fullPath;
272
289
  if (provider && provider !== 'claude') {
273
290
  fullPath = resolveOverlayPath(projectDir, relToFramework, provider);
274
291
  } else {
275
- fullPath = join(projectDir, relativePath);
292
+ fullPath = join(projectDir, resolveFrameworkDir(projectDir), relToFramework);
276
293
  }
277
294
 
278
295
  if (!existsSync(fullPath)) return null;
@@ -317,7 +334,7 @@ function buildHandoffSection(config) {
317
334
  * respected by the spawned Claude process.
318
335
  */
319
336
  function buildWriteScopeSection(config) {
320
- const scope = config.writeScope || getWriteScope(config.agent);
337
+ const scope = config.writeScope || getWriteScope(config.agent, config.projectDir);
321
338
 
322
339
  if (scope.length === 0) {
323
340
  return (
@@ -502,9 +519,9 @@ export function loadPreset(stack, projectDir) {
502
519
  return { loaded: false, preset: null, stack: stack || '' };
503
520
  }
504
521
 
505
- // Check both deployed (chati.dev/presets/) and package (framework/presets/) locations
522
+ // Check both deployed (chati.dev/presets/ or .chati.dev/presets/) and package (framework/presets/) locations
506
523
  const locations = [
507
- join(projectDir, 'chati.dev', 'presets'),
524
+ join(projectDir, resolveFrameworkDir(projectDir), 'presets'),
508
525
  join(projectDir, 'framework', 'presets'),
509
526
  ];
510
527
 
@@ -535,7 +552,7 @@ export function detectPreset(projectDir) {
535
552
 
536
553
  // Check both deployed and package locations
537
554
  const locations = [
538
- join(projectDir, 'chati.dev', 'presets'),
555
+ join(projectDir, resolveFrameworkDir(projectDir), 'presets'),
539
556
  join(projectDir, 'framework', 'presets'),
540
557
  ];
541
558
 
@@ -210,7 +210,7 @@ async function main() {
210
210
  summary: r.handoff?.summary || '',
211
211
  }));
212
212
 
213
- const merged = mergeHandoffs(mergeInput);
213
+ const merged = mergeHandoffs(mergeInput, projectDir);
214
214
  const nextAgent = determineNextAgent(agents);
215
215
  const consolidated = buildConsolidatedHandoff(merged, nextAgent);
216
216
 
@@ -36,13 +36,13 @@ import {
36
36
  // ---------------------------------------------------------------------------
37
37
 
38
38
  const TEAM_MEMBERS = {
39
- planning: ['detail', 'architect', 'ux'],
39
+ planning: ['detail', 'architect', 'ux', 'qa-planning'],
40
40
  build: ['dev', 'qa-implementation'],
41
41
  };
42
42
 
43
43
  // Task IDs match the Shared Task List template (team-planning-tasks.yaml / team-build-tasks.yaml)
44
44
  const TEAM_TASK_IDS = {
45
- planning: ['TT-PLN-001', 'TT-PLN-002', 'TT-PLN-003'],
45
+ planning: ['TT-PLN-001', 'TT-PLN-002', 'TT-PLN-003', 'TT-PLN-005'],
46
46
  build: ['TT-BLD-001-DEV', 'TT-BLD-001-QA'],
47
47
  };
48
48
 
@@ -276,7 +276,7 @@ async function main() {
276
276
  summary: r.handoff?.summary || '',
277
277
  }));
278
278
 
279
- const merged = mergeHandoffs(mergeInput);
279
+ const merged = mergeHandoffs(mergeInput, projectDir);
280
280
  const nextAgent = determineNextAgent(teamType);
281
281
  const consolidated = buildConsolidatedHandoff(merged, nextAgent);
282
282
 
@@ -8,11 +8,35 @@
8
8
  * when multiple team members update concurrently.
9
9
  */
10
10
 
11
- import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
12
- import { join, dirname } from 'path';
11
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, realpathSync } from 'fs';
12
+ import { join, dirname, resolve, relative } from 'path';
13
13
  import yaml from 'js-yaml';
14
14
  import { withLock } from '../utils/file-lock.js';
15
15
 
16
+ // ---------------------------------------------------------------------------
17
+ // Path Validation
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /**
21
+ * Assert that a target path is within .chati/teams/ to prevent path traversal.
22
+ * @param {string} targetPath - Absolute or relative path to validate
23
+ * @param {string} projectDir - Project root directory
24
+ * @throws {Error} If targetPath resolves outside .chati/teams/
25
+ */
26
+ function assertWithinTeamsDir(targetPath, projectDir) {
27
+ const teamsRoot = resolve(projectDir, '.chati', 'teams');
28
+ // Resolve symlinks so a symlink inside teams/ pointing outside (e.g., /etc/passwd)
29
+ // cannot bypass the boundary check. Fall back to plain resolve() if file doesn't exist
30
+ // yet (which is normal — we're about to create it).
31
+ const safeRealpath = (p) => { try { return realpathSync(p); } catch { return resolve(p); } };
32
+ const realTeamsRoot = safeRealpath(teamsRoot);
33
+ const absPath = safeRealpath(targetPath);
34
+ const rel = relative(realTeamsRoot, absPath);
35
+ if (rel.startsWith('..') || rel.startsWith('/')) {
36
+ throw new Error(`Path ${targetPath} is outside .chati/teams/ — refusing write`);
37
+ }
38
+ }
39
+
16
40
  // ---------------------------------------------------------------------------
17
41
  // Task List Management
18
42
  // ---------------------------------------------------------------------------
@@ -25,6 +49,9 @@ import { withLock } from '../utils/file-lock.js';
25
49
  * @returns {{ created: boolean, path: string }}
26
50
  */
27
51
  export function initTaskList(destPath, templatePath, overrides = {}) {
52
+ // Derive projectDir: destPath is <projectDir>/.chati/teams/<teamId>/<file>
53
+ const projectDir = resolve(destPath, '..', '..', '..', '..');
54
+ assertWithinTeamsDir(destPath, projectDir);
28
55
  mkdirSync(dirname(destPath), { recursive: true });
29
56
 
30
57
  if (!existsSync(templatePath)) {
@@ -61,6 +88,9 @@ export function readTaskList(path) {
61
88
  * @returns {Promise<{ updated: boolean, task: object|null }>}
62
89
  */
63
90
  export async function updateTaskStatus(path, taskId, updates) {
91
+ // Derive projectDir: path is <projectDir>/.chati/teams/<teamId>/<file>
92
+ const projectDir = resolve(path, '..', '..', '..', '..');
93
+ assertWithinTeamsDir(path, projectDir);
64
94
  return withLock(path, () => {
65
95
  const taskList = readTaskList(path);
66
96
  if (!taskList || !taskList.tasks) return { updated: false, task: null };
@@ -68,6 +98,8 @@ export async function updateTaskStatus(path, taskId, updates) {
68
98
  const task = taskList.tasks.find(t => t.id === taskId);
69
99
  if (!task) return { updated: false, task: null };
70
100
 
101
+ // Normalize 'completed' → 'done' (CLI uses 'completed', team YAML uses 'done')
102
+ if (updates.status === 'completed') updates.status = 'done';
71
103
  Object.assign(task, updates, { last_updated: new Date().toISOString() });
72
104
  if (updates.status === 'done' && !task.completed_at) {
73
105
  task.completed_at = new Date().toISOString();
@@ -93,6 +125,10 @@ export function getTeamProgress(path) {
93
125
  const tasks = taskList.tasks;
94
126
  const memberProgress = {};
95
127
 
128
+ // Normalize: both 'done' and 'completed' indicate task completion.
129
+ // CLI uses AGENT_STATUS.COMPLETED = 'completed'; team task YAML traditionally uses 'done'.
130
+ const isDone = (status) => status === 'done' || status === 'completed';
131
+
96
132
  for (const task of tasks) {
97
133
  const assignee = task.assigned_to;
98
134
  if (!assignee) continue;
@@ -100,7 +136,7 @@ export function getTeamProgress(path) {
100
136
  memberProgress[assignee] = { status: 'pending', score: null, tasks: 0, completed: 0 };
101
137
  }
102
138
  memberProgress[assignee].tasks++;
103
- if (task.status === 'done') {
139
+ if (isDone(task.status)) {
104
140
  memberProgress[assignee].completed++;
105
141
  memberProgress[assignee].score = task.score;
106
142
  } else if (task.status === 'in_progress') {
@@ -119,7 +155,7 @@ export function getTeamProgress(path) {
119
155
 
120
156
  return {
121
157
  total: tasks.length,
122
- done: tasks.filter(t => t.status === 'done').length,
158
+ done: tasks.filter(t => isDone(t.status)).length,
123
159
  pending: tasks.filter(t => t.status === 'pending').length,
124
160
  blocked: tasks.filter(t => t.status === 'blocked').length,
125
161
  inProgress: tasks.filter(t => t.status === 'in_progress').length,
@@ -142,6 +178,9 @@ export function getTeamProgress(path) {
142
178
  * @returns {{ written: boolean, path: string }}
143
179
  */
144
180
  export function writeMailboxMessage(mailboxDir, from, to, type, payload) {
181
+ // Derive projectDir: mailboxDir is <projectDir>/.chati/teams/<teamId>/mailbox
182
+ const projectDir = resolve(mailboxDir, '..', '..', '..', '..');
183
+ assertWithinTeamsDir(mailboxDir, projectDir);
145
184
  mkdirSync(mailboxDir, { recursive: true });
146
185
 
147
186
  const timestamp = Date.now();
@@ -1,12 +1,13 @@
1
1
  import { existsSync, mkdirSync, cpSync, rmSync } from 'fs';
2
2
  import { join } from 'path';
3
+ import { resolveFrameworkDir } from '../utils/framework-dir.js';
3
4
 
4
5
  /**
5
6
  * Create backup of chati.dev/ directory before upgrade
6
7
  * Backup location: chati.dev/.backup-v{version}/
7
8
  */
8
9
  export function createBackup(targetDir, currentVersion) {
9
- const frameworkDir = join(targetDir, 'chati.dev');
10
+ const frameworkDir = join(targetDir, resolveFrameworkDir(targetDir));
10
11
  const backupDir = join(frameworkDir, `.backup-v${currentVersion}`);
11
12
 
12
13
  if (existsSync(backupDir)) {
@@ -60,7 +61,7 @@ export function createBackup(targetDir, currentVersion) {
60
61
  * Restore from backup (rollback)
61
62
  */
62
63
  export function restoreFromBackup(targetDir, version) {
63
- const frameworkDir = join(targetDir, 'chati.dev');
64
+ const frameworkDir = join(targetDir, resolveFrameworkDir(targetDir));
64
65
  const backupDir = join(frameworkDir, `.backup-v${version}`);
65
66
 
66
67
  if (!existsSync(backupDir)) {
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, writeFileSync, existsSync } from 'fs';
2
2
  import { join } from 'path';
3
+ import { resolveFrameworkDir } from '../utils/framework-dir.js';
3
4
  import yaml from 'js-yaml';
4
5
  import semver from 'semver';
5
6
 
@@ -7,7 +8,7 @@ import semver from 'semver';
7
8
  * Read current installed version from config.yaml
8
9
  */
9
10
  export function getCurrentVersion(targetDir) {
10
- const configPath = join(targetDir, 'chati.dev', 'config.yaml');
11
+ const configPath = join(targetDir, resolveFrameworkDir(targetDir), 'config.yaml');
11
12
  if (!existsSync(configPath)) return null;
12
13
 
13
14
  try {
@@ -74,7 +75,7 @@ function getChangelog(fromVersion, toVersion) {
74
75
  * Update config.yaml with the new version after successful upgrade
75
76
  */
76
77
  export function updateConfigVersion(targetDir, newVersion) {
77
- const configPath = join(targetDir, 'chati.dev', 'config.yaml');
78
+ const configPath = join(targetDir, resolveFrameworkDir(targetDir), 'config.yaml');
78
79
  if (!existsSync(configPath)) return false;
79
80
 
80
81
  try {
@@ -1,14 +1,37 @@
1
1
  import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync } from 'fs';
2
- import { join } from 'path';
2
+ import { dirname, join } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { resolveFrameworkDir } from '../utils/framework-dir.js';
3
5
  import yaml from 'js-yaml';
4
6
  import semver from 'semver';
5
7
 
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ // Bundled framework source — always contains the latest migrations from the CLI package
10
+ const BUNDLED_MIGRATIONS = join(__dirname, '..', '..', 'framework', 'migrations');
11
+ const MONOREPO_MIGRATIONS = join(__dirname, '..', '..', '..', '..', 'chati.dev', 'migrations');
12
+
6
13
  /**
7
- * Find applicable migrations between two versions
14
+ * Find applicable migrations between two versions.
15
+ *
16
+ * Reads from the BUNDLED framework (shipped with the CLI package), NOT from
17
+ * the installed copy. This is critical because on upgrade, the installed
18
+ * framework still holds the OLD migrations — new ones aren't copied until
19
+ * after migrations run. Without this, new migrations would never execute on
20
+ * the first upgrade to the version that introduced them.
8
21
  */
9
22
  export function findMigrations(targetDir, fromVersion, toVersion) {
10
- const migrationsDir = join(targetDir, 'chati.dev', 'migrations');
11
- if (!existsSync(migrationsDir)) return [];
23
+ // Prefer bundled (npm package) source; fall back to monorepo dev layout;
24
+ // last resort, the already-installed copy (legacy behavior).
25
+ let migrationsDir = null;
26
+ if (existsSync(BUNDLED_MIGRATIONS)) {
27
+ migrationsDir = BUNDLED_MIGRATIONS;
28
+ } else if (existsSync(MONOREPO_MIGRATIONS)) {
29
+ migrationsDir = MONOREPO_MIGRATIONS;
30
+ } else {
31
+ const installedDir = join(targetDir, resolveFrameworkDir(targetDir), 'migrations');
32
+ if (existsSync(installedDir)) migrationsDir = installedDir;
33
+ }
34
+ if (!migrationsDir) return [];
12
35
 
13
36
  const files = readdirSync(migrationsDir)
14
37
  .filter(f => f.endsWith('.yaml'))
@@ -52,7 +75,7 @@ export function findMigrations(targetDir, fromVersion, toVersion) {
52
75
  /**
53
76
  * Execute a single migration step
54
77
  */
55
- function executeMigrationStep(targetDir, step) {
78
+ async function executeMigrationStep(targetDir, step) {
56
79
  switch (step.type) {
57
80
  case 'create_directory': {
58
81
  const dir = join(targetDir, step.path);
@@ -117,6 +140,41 @@ function executeMigrationStep(targetDir, step) {
117
140
  break;
118
141
  }
119
142
 
143
+ case 'update_gitignore': {
144
+ // Delegates to installer's updateGitignore via a dynamic import to keep
145
+ // concerns separated. The selectedIDEs come from the installed config.
146
+ // Best-effort: if anything fails, we skip silently so migration doesn't break.
147
+ try {
148
+ const configPath = join(targetDir, resolveFrameworkDir(targetDir), 'config.yaml');
149
+ let selectedIDEs = ['claude-code'];
150
+ if (existsSync(configPath)) {
151
+ const cfg = yaml.load(readFileSync(configPath, 'utf-8')) || {};
152
+ if (Array.isArray(cfg.ides)) selectedIDEs = cfg.ides;
153
+ }
154
+ // Inline call (avoid circular import with core.js in tests).
155
+ // We replicate the updateGitignore logic here is not ideal, so instead
156
+ // call through a dedicated helper: core.js exports updateGitignore.
157
+ const core = await import('../installer/core.js');
158
+ if (typeof core.updateGitignore === 'function') {
159
+ core.updateGitignore(targetDir, selectedIDEs);
160
+ }
161
+ } catch { /* expected: skip if installer helper not available */ }
162
+ break;
163
+ }
164
+
165
+ case 'warn_tracked_files': {
166
+ // Non-blocking warning — detects already-tracked framework files.
167
+ try {
168
+ const { detectTrackedFrameworkFiles } = await import('./tracked-files-detector.js');
169
+ const { renderTrackedFilesWarning } = await import('./user-messages.js');
170
+ const tracked = detectTrackedFrameworkFiles(targetDir);
171
+ if (tracked.length > 0) {
172
+ process.stdout.write(renderTrackedFilesWarning(tracked));
173
+ }
174
+ } catch { /* expected: skip if git unavailable */ }
175
+ break;
176
+ }
177
+
120
178
  default:
121
179
  // Unknown step type - skip
122
180
  break;
@@ -138,7 +196,7 @@ export async function runMigrations(targetDir, fromVersion, toVersion) {
138
196
  for (const migration of migrations) {
139
197
  try {
140
198
  for (const step of migration.steps) {
141
- executeMigrationStep(targetDir, step);
199
+ await executeMigrationStep(targetDir, step);
142
200
  }
143
201
  results.push({ file: migration.file, success: true });
144
202
  } catch (err) {
@@ -147,7 +205,7 @@ export async function runMigrations(targetDir, fromVersion, toVersion) {
147
205
  // Attempt rollback for this migration
148
206
  try {
149
207
  for (const step of migration.rollback) {
150
- executeMigrationStep(targetDir, step);
208
+ await executeMigrationStep(targetDir, step);
151
209
  }
152
210
  } catch (rollbackErr) {
153
211
  console.error('[chati] migration rollback failed:', rollbackErr?.message);
@@ -0,0 +1,86 @@
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { execSync } from 'child_process';
4
+
5
+ /**
6
+ * Framework file patterns that should never be committed to git.
7
+ * Each entry is a path (relative to targetDir) that git ls-files will check.
8
+ */
9
+ const FRAMEWORK_PATTERNS = [
10
+ '.chati.dev/',
11
+ '.chati/session.yaml',
12
+ '.chati/teams/',
13
+ '.chati/timeline.json',
14
+ '.chati/memories/shared/session/',
15
+ '.chati/memories/shared/gotchas.json',
16
+ '.chati/memories/shared/error-log.json',
17
+ '.claude/commands/chati.md',
18
+ '.claude/rules/chati/',
19
+ '.claude/settings.local.json',
20
+ 'CLAUDE.local.md',
21
+ '.gemini/session-lock.md',
22
+ 'AGENTS.override.md',
23
+ '.gemini/commands/',
24
+ '.gemini/context/',
25
+ '.gemini/hooks/',
26
+ '.gemini/settings.json',
27
+ '.codex/',
28
+ '.agents/skills/chati/',
29
+ ];
30
+
31
+ /**
32
+ * Check whether targetDir is a git repository by verifying .git/ exists.
33
+ * @param {string} targetDir
34
+ * @returns {boolean}
35
+ */
36
+ function isGitRepo(targetDir) {
37
+ return existsSync(join(targetDir, '.git'));
38
+ }
39
+
40
+ /**
41
+ * Detect which framework-owned paths are currently tracked by git.
42
+ * Does NOT modify git state — read-only operation.
43
+ *
44
+ * @param {string} targetDir - absolute path to the project root
45
+ * @returns {string[]} array of tracked framework file paths (relative to targetDir)
46
+ */
47
+ export function detectTrackedFrameworkFiles(targetDir) {
48
+ if (!isGitRepo(targetDir)) {
49
+ return [];
50
+ }
51
+
52
+ const tracked = [];
53
+
54
+ for (const pattern of FRAMEWORK_PATTERNS) {
55
+ try {
56
+ const result = execSync(`git ls-files -- "${pattern}"`, {
57
+ cwd: targetDir,
58
+ stdio: ['ignore', 'pipe', 'ignore'],
59
+ timeout: 5000,
60
+ })
61
+ .toString()
62
+ .trim();
63
+
64
+ if (result.length > 0) {
65
+ tracked.push(pattern);
66
+ }
67
+ } catch {
68
+ // git ls-files failure means path is not tracked — skip
69
+ }
70
+ }
71
+
72
+ // Deduplicate in case patterns overlap
73
+ return [...new Set(tracked)];
74
+ }
75
+
76
+ /**
77
+ * Build the list of `git rm -r --cached <path>` commands for untracking.
78
+ * Deduplicates entries and returns one command string per path.
79
+ *
80
+ * @param {string[]} files - array of tracked framework paths
81
+ * @returns {string[]} array of git rm commands
82
+ */
83
+ export function formatUntrackCommands(files) {
84
+ const unique = [...new Set(files)];
85
+ return unique.map(f => `git rm -r --cached "${f}"`);
86
+ }
@@ -0,0 +1,94 @@
1
+ import { formatUntrackCommands } from './tracked-files-detector.js';
2
+
3
+ /**
4
+ * Render a plain-text warning when framework files are tracked by git.
5
+ *
6
+ * Non-blocking — the caller must decide whether to halt the upgrade.
7
+ * No ANSI color codes: output is suitable for piping and log files.
8
+ *
9
+ * @param {string[]} files - tracked framework paths detected by detectTrackedFrameworkFiles()
10
+ * @returns {string} warning text ready to print to stdout/stderr
11
+ */
12
+ export function renderTrackedFilesWarning(files) {
13
+ if (!files || files.length === 0) return '';
14
+
15
+ const bullet = files.map(f => ` - ${f}`).join('\n');
16
+ const commands = formatUntrackCommands(files);
17
+ const cmdBlock = commands.join(' \\\n ');
18
+
19
+ return [
20
+ '',
21
+ '================================================================',
22
+ 'WARNING: Chati framework files are tracked by git',
23
+ '================================================================',
24
+ '',
25
+ 'The following Chati-owned paths are currently committed to git:',
26
+ '',
27
+ bullet,
28
+ '',
29
+ 'Why this matters:',
30
+ ' - Framework internals (agents, hooks, session state) are not',
31
+ ' project source code and should not be versioned alongside it.',
32
+ ' - Committing them risks leaking internal IP and stale runtime',
33
+ ' state (session locks, memory caches, team mailboxes) to',
34
+ ' remote repositories or collaborators.',
35
+ ' - Backup directories inside .chati.dev/ can be multi-megabyte',
36
+ ' and will bloat your git history permanently.',
37
+ '',
38
+ 'What to do:',
39
+ ' Run the commands below to stop tracking these paths.',
40
+ ' The files remain on disk — only git tracking is removed.',
41
+ '',
42
+ ' ' + cmdBlock,
43
+ ' git commit -m "chore: untrack Chati framework files from git"',
44
+ '',
45
+ 'You can run these commands now or after the upgrade completes.',
46
+ 'The upgrade will continue regardless.',
47
+ '================================================================',
48
+ '',
49
+ ].join('\n');
50
+ }
51
+
52
+ /**
53
+ * Render a banner shown at the start of `chati upgrade`.
54
+ *
55
+ * @param {string} fromVersion - current installed version (e.g. "4.2.3")
56
+ * @param {string} toVersion - target version (e.g. "4.3.0")
57
+ * @returns {string}
58
+ */
59
+ export function renderUpgradeBanner(fromVersion, toVersion) {
60
+ return [
61
+ '',
62
+ '================================================================',
63
+ `Chati.dev Upgrade: v${fromVersion} -> v${toVersion}`,
64
+ '================================================================',
65
+ '',
66
+ 'Steps: backup -> migrations -> sync files -> validate -> done',
67
+ '',
68
+ ].join('\n');
69
+ }
70
+
71
+ /**
72
+ * Render a success message shown after a completed upgrade.
73
+ *
74
+ * @param {string} fromVersion - version upgraded from
75
+ * @param {string} toVersion - version upgraded to
76
+ * @returns {string}
77
+ */
78
+ export function renderUpgradeSuccess(fromVersion, toVersion) {
79
+ return [
80
+ '',
81
+ '================================================================',
82
+ `Chati.dev successfully upgraded: v${fromVersion} -> v${toVersion}`,
83
+ '================================================================',
84
+ '',
85
+ 'Next steps:',
86
+ ' 1. Review any warnings printed above.',
87
+ ' 2. If framework files were tracked by git, run the provided',
88
+ ' git rm --cached commands and commit the result.',
89
+ ' 3. Type /chati in your project to resume where you left off.',
90
+ '',
91
+ 'Release notes: https://github.com/chati-dev/chati.dev/releases',
92
+ '',
93
+ ].join('\n');
94
+ }
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { existsSync, readFileSync } from 'fs';
10
10
  import { join } from 'path';
11
+ import { resolveFrameworkDir } from './framework-dir.js';
11
12
 
12
13
  /**
13
14
  * Parse provider configuration from config.yaml.
@@ -16,7 +17,7 @@ import { join } from 'path';
16
17
  * @returns {{ primary: string, enabled: string[], raw: string|null }}
17
18
  */
18
19
  export function parseProviderConfig(projectDir) {
19
- const configPath = join(projectDir, 'chati.dev', 'config.yaml');
20
+ const configPath = join(projectDir, resolveFrameworkDir(projectDir), 'config.yaml');
20
21
  if (!existsSync(configPath)) {
21
22
  return { primary: 'claude', enabled: ['claude'], raw: null };
22
23
  }
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { existsSync, readFileSync } from 'fs';
12
12
  import { join } from 'path';
13
+ import { resolveFrameworkDir } from './framework-dir.js';
13
14
 
14
15
  /**
15
16
  * All known feature flags with their default values.
@@ -49,7 +50,7 @@ export function isFeatureEnabled(projectDir, featureName) {
49
50
  * @returns {Record<string, boolean>} Map of feature name to enabled status
50
51
  */
51
52
  export function getEnabledFeatures(projectDir) {
52
- const configPath = join(projectDir, 'chati.dev', 'config.yaml');
53
+ const configPath = join(projectDir, resolveFrameworkDir(projectDir), 'config.yaml');
53
54
  if (!existsSync(configPath)) {
54
55
  return { ...DEFAULTS };
55
56
  }